14 KiB
Workflow Config Targets and Sources
Date: 2026-06-03
Status: design note
Related:
- Long-lived workflow API boundary
- Persisted run/resume contract
- Runtime source lifecycle
- Current roadmap
Purpose
Define the next workflow config shape before remote CLI targeting and source provider config expand. The key distinctions are:
client.targetchooses where a client sends workflow operations by default.serverdeclares what a long-lived process can host.server.sourcesdeclare capability providers available to that server.
These must not be collapsed into one concept.
Client Target
client.target.kind answers: where should this client send workflow
operations by default?
It is a control-plane setting for clients such as wf. It should be
overridable by CLI flags, for example:
wf --config wf.json --url http://127.0.0.1:8765/rpc run start demo.default
wf --config wf.json --local run start demo.default
CLI overrides should affect only the current invocation. They should not mutate the config file unless a dedicated config-write command is used.
Example local target:
{
"client": {
"target": {
"kind": "local"
}
}
}
This means the CLI builds an in-process WorkflowApi from local stores and
local source providers.
Example remote target:
{
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
"timeout_seconds": 30
}
}
}
This means the CLI should call the long-lived JSON-RPC server. It should not
construct local runtime services for execution unless --local or an equivalent
override is supplied.
Recommended Pydantic shape:
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class LocalTargetConfig(BaseModel):
kind: Literal["local"] = "local"
class RpcHttpTargetConfig(BaseModel):
kind: Literal["rpc_http"]
url: str
timeout_seconds: float = 30.0
TargetConfig = Annotated[
LocalTargetConfig | RpcHttpTargetConfig,
Field(discriminator="kind"),
]
Do not add client.target.kind = "mcp" for the workflow CLI path. MCP can be
hosted by the server as one transport, but wf should use JSON-RPC for
first-class workflow operations.
Server Hosting
server answers: what can this process host if started as a long-lived
workflow server?
A server can host multiple transports at the same time. This is required so a
user can point wf at JSON-RPC while another client points MCP at the same
server process.
Recommended shape:
{
"server": {
"store": {
"kind": "filesystem",
"root": ".wf_store"
},
"transports": [
{
"kind": "rpc_http",
"host": "127.0.0.1",
"port": 8765,
"path": "/rpc"
},
{
"kind": "mcp_http",
"host": "127.0.0.1",
"port": 8765,
"path": "/mcp"
}
],
"sources": [
{"kind": "stdlib", "id": "wf.std"}
]
}
}
Recommended Pydantic shape:
class RpcHttpTransportConfig(BaseModel):
kind: Literal["rpc_http"]
host: str = "127.0.0.1"
port: int = 8765
path: str = "/rpc"
class McpHttpTransportConfig(BaseModel):
kind: Literal["mcp_http"]
host: str = "127.0.0.1"
port: int = 8765
path: str = "/mcp"
ServerTransportConfig = Annotated[
RpcHttpTransportConfig | McpHttpTransportConfig,
Field(discriminator="kind"),
]
class ServerConfig(BaseModel):
store: StoreConfig = StoreConfig(kind="filesystem", root=Path(".wf_store"))
transports: list[ServerTransportConfig] = []
sources: list[SourceConfig] = []
Transport configs describe what the server hosts. They are not workflow sources.
Store
server.store answers: where does the server persist workflow platform state?
The current implementation uses one default filesystem store root:
{
"server": {
"store": {
"kind": "filesystem",
"root": ".wf_store"
}
}
}
That single root fans out internally into role-specific files/directories:
- workflow records: artifacts, deployments, draft workspaces, runs, and traces
- source registry desired state
- source catalog/cache snapshots
- auth records for local/dev MCP-compatible credentials
Recommended Pydantic shape for the first slice:
class FilesystemStoreConfig(BaseModel):
kind: Literal["filesystem"]
root: Path = Path(".wf_store")
class SqliteStoreConfig(BaseModel):
kind: Literal["sqlite"]
url: str
StoreConfig = Annotated[
FilesystemStoreConfig | SqliteStoreConfig,
Field(discriminator="kind"),
]
Only filesystem needs implementation immediately. The tagged union exists so
the config does not bake the store concept into a single store_root field.
Relative filesystem paths should resolve relative to the config file directory. SQL-backed stores are future work.
Store Roles
server.store is the default store for every role. Future configs should allow
optional role-specific overrides without breaking existing files:
{
"server": {
"store": {
"kind": "filesystem",
"root": ".wf_store"
},
"stores": {
"workflow": {
"kind": "filesystem",
"root": ".wf_store"
},
"auth": {
"kind": "filesystem",
"root": ".wf_auth"
},
"source_registry": {
"kind": "filesystem",
"root": ".wf_sources"
},
"catalog_cache": {
"kind": "filesystem",
"root": ".wf_catalog"
}
}
}
}
Implementation status: first filesystem-only slice implemented. Role overrides
are optional and fall back to server.store. MCP auth and catalog/cache
storage now have separate file-store adapters; server.store still remains
the fallback for missing roles.
Resolution rule:
effective_store(role) = server.stores[role] if present else server.store
The first implementation should keep all role overrides optional and filesystem only. This preserves the current single-root config while making the boundary ready for secret-manager auth stores, database-backed workflow records, and separate catalog/cache storage.
The store layer should own mutable workflow platform registries:
- artifact records
- deployment records
- draft workspaces
- run/checkpoint records
- source registry entries
- source catalog snapshots
- source liveness/status metadata
- auth records or auth references
This matters for daemon/server mode. A source added dynamically through an admin tool or UI should survive process restart because it was written to the store, not only held in process memory.
Sources
sources[].kind answers: what type of capability provider is this?
Sources are execution/catalog configuration owned by the server runtime. They are not client target configuration and not transport configuration.
server.sources are bootstrap source declarations. They are loaded at server
startup and should be treated as desired startup state, not the only possible
source registry.
Long-term mutable sources should live in a store-backed source registry. The effective source catalog is:
effective sources = config bootstrap sources + stored source registry entries
Conflict rules must be explicit:
- duplicate source ids between config and store should fail fast in the first implementation
- config bootstrap sources can be treated as immutable unless copied into the store by an explicit admin action
- store-backed sources can be added, disabled, refreshed, or deleted by future admin/UI flows
- server startup validates the effective source set before accepting workflow runs
Recommended shape:
{
"server": {
"sources": [
{
"kind": "stdlib",
"id": "wf.std"
},
{
"kind": "mcp",
"id": "github.work",
"server": "mcp-github",
"account": "work",
"transport": {
"kind": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
},
"auth": {
"kind": "env",
"name": "GITHUB_WORK_TOKEN"
}
},
{
"kind": "mcp",
"id": "github.personal",
"server": "github-2",
"account": "personal",
"transport": {
"kind": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
},
"auth": {
"kind": "env",
"name": "GITHUB_PERSONAL_TOKEN"
}
}
]
}
}
Recommended Pydantic shape:
class StdlibSourceConfig(BaseModel):
kind: Literal["stdlib"]
id: Literal["wf.std", "wf.recipes"]
class McpStdioTransportConfig(BaseModel):
kind: Literal["stdio"]
command: str
args: list[str] = []
cwd: str | None = None
env: dict[str, str] = {}
class AuthEnvRef(BaseModel):
kind: Literal["env"]
name: str
class McpSourceConfig(BaseModel):
kind: Literal["mcp"]
id: str
server: str
account: str | None = None
transport: McpStdioTransportConfig
auth: AuthEnvRef | None = None
class OpenApiSourceConfig(BaseModel):
kind: Literal["openapi"]
id: str
spec_url: str | None = None
spec_path: str | None = None
auth: AuthEnvRef | None = None
SourceConfig = Annotated[
StdlibSourceConfig | McpSourceConfig | OpenApiSourceConfig,
Field(discriminator="kind"),
]
Exact transport/auth subtypes can expand later. The important decision is that
the top-level source union is discriminated by kind.
Identity Rules
id is the primary source identity.
Rules:
sources[].idmust be unique across the config.- Artifact and deployment bindings refer to source ids, not
serveroraccount. serverandaccountare provider-specific metadata/config, not identity.- Same
serverwith differentaccountis allowed. - Same
serverand sameaccountwith differentidis allowed only when the implementation can prove or report why the entries differ. - No code should split a source id to infer
server,account, provider, or version. - Dotted ids are display-friendly names, not parseable structure.
This matters for cases like:
github.work -> MCP source using server "mcp-github", account "work"
github.personal -> MCP source using server "github-2", account "personal"
Both are valid even though their display ids share a prefix. Runtime validation must use the explicit struct fields.
Deployment Binding Relationship
Artifacts describe required logical sources. Deployments bind those logical sources to concrete source ids available on the execution target.
Example:
{
"required_sources": ["github.work"],
"deployment": {
"bindings": [
{
"logical_source": "github.work",
"concrete_source": "github.personal"
}
]
}
}
Before run, validation should check:
- the concrete source id exists on the selected execution target
- the concrete source exposes the required capability keys
- the pinned artifact/deployment dependencies are still runnable
If client.target.kind = "rpc_http", the remote server is the source of truth
for execution-time source availability. The local CLI config should not pretend
its local server.sources prove remote runnability unless the same process is
also serving that target.
Config File Direction
The current wf_mcp.config.json shape is MCP-specific:
{
"store_root": ".wf_mcp_store",
"connections": []
}
The workflow platform needs a neutral config shape:
{
"version": 1,
"client": {
"target": {
"kind": "local"
}
},
"server": {
"store": {
"kind": "filesystem",
"root": ".wf_store"
},
"transports": [],
"sources": []
}
}
Compatibility can keep loading old MCP configs for MCP commands, but new workflow CLI/server config should move toward the neutral shape.
Relative paths should resolve relative to the config file directory.
Non-Goals
- Do not redesign artifact/deployment binding models in this note.
- Do not implement source provider lifecycle here.
- Do not store production secrets directly in JSON config.
- Do not make
client.target.kindinfluence workflow source identity. - Do not derive identity by splitting dotted source ids.
- Do not use MCP as the first-class
wfCLI target.
Implementation Status
First slice implemented:
- neutral
wf_configmodels and loader - filesystem server store config
- stdlib source bootstrap config is parsed and fail-fast limited to currently
wired source ids (
wf.std,wf.recipes) - local and JSON-RPC client targets
wfroot overrides for--local,--url, and--timeout- remote JSON-RPC client support now covers capability, draft workspace, artifact, deployment, and run CLI commands
- draft/artifact/deploy commands no longer fail fast for
rpc_httptargets wf-rpc-server --configsupport for server store and RPC HTTP transport, including configured RPC path
Still future:
- store-backed mutable source registry
- MCP/OpenAPI source config
- arbitrary stdlib source aliases
/mcphosting from neutral server config- auth and SQL stores
Next Implementation Slice
First config implementation should be small:
- Add neutral config models for
version,client.target, andserver. - Support
client.target.kind = "local"andclient.target.kind = "rpc_http". - Support CLI overrides for
--localand--urlwithout mutating config. - Support
server.transports.kind = "rpc_http"forwf-rpc-server. - Support
server.store.kind = "filesystem"and map it to existing file stores. - Support
server.sources.kind = "stdlib"only, plus a compatibility bridge for the existing MCP config where needed. - Update
wf_cli.contextso local/remote target selection is explicit. - Keep store-backed source registry, full MCP source config migration, SQL
stores, and
/mcphosting for later slices.