Appearance
Workflow Schema
Workflow definitions can be provided as:
- Markdown files with YAML frontmatter (
.md) - JSON files (
.json)
The parser expects key, title, and steps at minimum.
Minimal examples
Markdown:
md
---
key: minimal-demo
title: Minimal Demo
steps:
- key: plan
kind: task
taskSpec:
init:
skills: [planning]
mcps: [filesystem]
systemPrompts: [Keep the plan concise]
---JSON:
json
{
"key": "minimal-demo",
"title": "Minimal Demo",
"steps": [
{
"key": "plan",
"kind": "task",
"taskSpec": {
"init": {
"skills": ["planning"],
"mcps": ["filesystem"],
"systemPrompts": ["Keep the plan concise"]
}
}
}
]
}Top-level fields
key(required): unique workflow identifiertitle(required): default run objectivedescription: optional summaryobjectives: optional list of run-level objectivesinputSchema: optional JSON schema-like shape for inputsoutputSchema: optional JSON schema-like shape for outputsdefaultRetryPolicy.maxAttempts: fallback retry attempts for stepssteps(required): ordered list of step definitions
Step fields
key(required): step idkind(required):task | approval | systemobjective: optional step-level objectivedependsOn: list of prerequisite step keysretryPolicy.maxAttempts: step-level retry overridevalidation.mode:none | human | external | agent— see Validation below for theagentmode fields and routing semanticsvalidation.required,validation.autoConfirmtaskSpec.adapterKey: optional; omitted task adapters run withpi-agent. Explicit values arepi-agent | mock | acp | opencode | codex | claude-code.taskSpec.init.contexttaskSpec.init.skillstaskSpec.init.mcps(http(s) endpoints are passed to ACP agents as session MCP servers)taskSpec.init.systemPromptstaskSpec.init.modeltaskSpec.payload.mockResult:success | retry | rollback | restart | yield | failtaskSpec.payload.requiredEnv: optional list of environment variables required before a real adapter can run- Subprocess payload fields (for
pi-agent, and the deprecated bespokeclaude-code/opencodeexecutors):taskSpec.payload.command: executable to spawn; defaults topiforpi-agent(override withWFM_PI_AGENT_COMMANDor this field)taskSpec.payload.args: extra argv entries prepended before the adapter's own flags (e.g.--print, model/skill flags, the prompt)taskSpec.payload.runDir: directory holdinginput.json/output.json/prompt.txtfor the step; defaults to a fresh temp directory per attempttaskSpec.payload.timeoutMs: milliseconds WFM waits before sendingSIGTERMto the child and failing the step. Must be a positive finite number or it falls back to the adapter default. Defaults:600000(10 minutes) forpi-agent,120000for the legacyclaude-codeexecutor. A step that legitimately runs long (e.g. a real API-backed collector script) should set this explicitly rather than relying on the default.- When the timer fires, the step's
mutated_payloadcarriestimedOut: trueandterminationSignal: "SIGTERM"in addition to the capturedstdout/stderr, so a WFM-side timeout is distinguishable from the child exiting on its own ("<command> exited with status N", notimedOutfield) or from a child that fails and reports its own error out-of-band.
- ACP payload fields (for
acpand the ACP-routedclaude-code | opencode | codex):taskSpec.payload.useRealAdapter: foracp,claude-code, andcodex, settrueto run the agent through ACP (otherwise the step mocks).opencodeis the exception — it runs real by default through ACP; setfalseto opt out to the mock executor.taskSpec.payload.acpCommand/acpArgs: explicit ACP agent command and argstaskSpec.payload.acpAgent: a preset name (claude-code | opencode | gemini | codex) whenadapterKeyisacptaskSpec.payload.acpPermissions:allow(default) |deny|reads-onlytaskSpec.payload.acpAuthMethod: ACP auth method id when the agent requires authenticationtaskSpec.payload.legacyExecutor:claude-codeonly — settrueto use the deprecated bespokeclaudesubprocess executor instead of ACP
approvalSpec.autoApprove,approvalSpec.validation
Validation
validation.mode gates how a step's execution result is confirmed before the run proceeds:
none: no confirmation requiredhuman: requires--confirm stepKey,--auto-confirm-all, or step-levelautoConfirmexternal: resolved by an outside system (a webhook, another process) calling the attach API's approve/resume endpointsagent: a second, independent agent call validates the step's output against explicit criteria (see below)
Common fields: validation.required (whether confirmation is mandatory at all) and validation.autoConfirm (skip confirmation and treat the step as pre-approved).
mode: "agent" and AgentValidationSpec
Set validation.agent to configure the validator call:
agent.adapterKey: optional; one of the supported adapters (pi-agent | mock | acp | opencode | codex | claude-code). Defaults to the validated step's owntaskSpec.adapterKeywhen omitted.agent.criteria: optional natural-language acceptance criteria the validator checks the step's output against. Prefer criteria phrased as a checkable fact ("tests pass," "no unrelated files changed") over subjective taste calls.agent.init: optionalTaskInitConfig(model,skills,mcps,systemPrompts,context) for the validator's own agent call — independent of the validated step'staskSpec.init.agent.payload: optional adapter payload, e.g.{ mockResult: "success" }to drive the mock adapter through agent validation during a dry run.
yaml
validation:
mode: agent
required: true
autoConfirm: false
agent:
criteria: "tests pass, the fix addresses a root cause, and no unrelated files changed"
init:
model: openrouter/anthropic/claude-sonnet-4Routing semantics: the validator's verdict is folded into the same QA routing the engine uses for a step's own self-reported result — an execution status (SUCCESS, FAILED, QA_REJECTED, YIELD_EXTERNAL) plus an action: PROCEED continues to the next step, RETRY_CURRENT reruns the validated step, ROLLBACK_PREVIOUS reruns an earlier step, RESTART_ALL restarts the run. Retries from an agent-validation rejection are bounded by the step's retryPolicy.maxAttempts (or defaultRetryPolicy.maxAttempts) like any other rejection. The engine emits step.validation_started and step.validation_finished events around the validator call.
mode: agent runs unconditionally once a step's own execution finishes — --auto-confirm-all and step-level autoConfirm never skip it, because it is a QA gate on the output, not a human sign-off gate.
Restriction: mode: agent is not allowed on an approval step's approvalSpec.validation — the parser rejects it (Approval step <key> cannot use agent validation). Use a dedicated kind: task step with validation.mode: agent upstream of the approval instead.
Approval-step gotcha: an unset step-level validation defaults to { mode: "none", required: false, autoConfirm: true } — including on approval steps. Because canConfirm checks the step's own validation.autoConfirm before approvalSpec.validation.autoConfirm, an approval step that only sets approvalSpec.validation (and leaves top-level validation unset) will silently auto-approve instead of waiting for a human. Always set both validation and approvalSpec.validation on an approval step, with matching mode/required/autoConfirm. See Authoring Workflows With an Agent for a worked example.
Validation rules enforced by the CLI
- step keys must be unique
- every dependency must reference an existing step
- dependencies must not form a cycle
kindmust be one oftask,approval,system- adapter key must be one of the supported adapters
- validation mode must be
none,human,external, oragent mode: agentis not allowed on an approval step'sapprovalSpec.validation- when
validation.modeisagentandvalidation.agent.adapterKeyis set, it must be one of the supported adapters
Runtime preflight
wfm run also checks host runtime requirements before execution starts:
- omitted task adapters use
pi-agentand require the configuredpicommand (override withWFM_PI_AGENT_COMMANDortaskSpec.payload.command) - ACP-routed steps require the resolved ACP agent command on
PATH(acpCommand/acpAgentpreset /WFM_ACP_COMMAND);opencodesteps require it by default since they run real unless opted out withpayload.useRealAdapter: false - legacy
claude-codesteps (withpayload.legacyExecutor: true) require theclaudeCLI - known provider models require
OPENROUTER_API_KEY,OPENAI_API_KEY, orANTHROPIC_API_KEY(not enforced forpi-agentor ACP steps, since those agents manage their own auth) - custom LLM clients can declare required keys in
taskSpec.payload.requiredEnv
Use wfm doctor to inspect host setup. Use wfm doctor <workflow> to run schema validation and runtime preflight without executing any steps.
Current adapter implementation status:
pi-agent: real host adapter driving thepicoding agent CLI; default for omittedtaskSpec.adapterKeymock: deterministic in-process simulatoracp: Agent Client Protocol adapter; connects to any ACP agent over JSON-RPC/stdio whenuseRealAdapteris true and an agent command resolves. Verified againstgemini --experimental-acp; setpayload.acpAgent: gemini(oracpCommand/acpArgs).opencode: runs real by default through ACP (opencode acp); requires theopencodeCLI on the host unlesspayload.useRealAdapter: falseopts the step out to a mockcodex: routed through ACP via thecodex-acpbridge whenuseRealAdapteris true (codexitself has no native ACP). Install the bridge withnpm install -g @agentclientprotocol/codex-acp; it reuses the codex CLI's own auth (codex loginor API key).claude-code: routed through ACP via theclaude-code-acpbridge whenuseRealAdapteris true; bespoke executor deprecated (payload.legacyExecutor)
When a step explicitly selects a non-pi adapter but the real path is not enabled (no useRealAdapter, or no resolvable ACP agent command), the step runs as a mock. wfm doctor <workflow> reports this under "Adapter warnings" and wfm run prints a warning before execution, so the fallback is never silent. opencode is the one adapter this doesn't apply to by default — it runs real without any opt-in flag, so an unresolvable agent command is reported as a warning rather than a silent fallback; an explicit payload.useRealAdapter: false opts out to the mock intentionally, with no warning.