Flow
Fixed DAG pipelines: flow.json, routing, structured output contracts, and parallel execution.
A Flow is a fixed, repeatable DAG pipeline that runs a structured multi-agent process. Flows are the right tool when the same sequence of agent handoffs must run consistently, every time.
A flow is worth creating when the collaboration pattern is enumerable in advance: ≥2 role-separated steps, the same input shape on every call, and expected reuse. A single-node "flow" is invalid — use an agent instead.
Directory Layout
flows/<name>/
├── flow.json # DAG definition (required)
└── agents/ # Flow-scoped agents (optional)
└── <agent>/ # agent definition used by nodes of this flow
flow.json
{
"name": "code-review",
"description": "Multi-dimensional code review pipeline with automated checks, 3-dimension scoring, and check-fix loop. Use when you need a thorough review of code changes or a PR.",
"entry": "scanner",
"maxLoop": 5,
"nodes": {
"scanner": {
"agent": "scanner",
"input": "$task",
"outputs": { "issues": "array", "diff_summary": "string" },
"onComplete": {
"switch": "$scanner.verdict",
"cases": { "ok": "reviewer", "error": "$return" },
"default": "$return"
}
},
"reviewer": {
"agent": "reviewer",
"input": "Review these code changes:\n$scanner.output",
"onComplete": {
"switch": "$reviewer.verdict",
"cases": { "pass": "$return", "fix": "fixer" },
"default": "$return"
}
},
"fixer": {
"agent": "fixer",
"input": "Fix the issues identified by the reviewer:\n$reviewer.output",
"onComplete": "reviewer"
}
}
}
Nodes are a map keyed by node id. See Flow Definition for the full schema.
Routing
onComplete accepts three forms:
| Form | Meaning |
|------|---------|
| "nodeId" | Go to that node |
| "$return" | Finish the flow and return the node's output |
| {"switch": ..., "cases": {...}} | Branch on the node's verdict |
| {"parallel": [...]} | Fan out to multiple nodes concurrently |
Case values in a switch are themselves routes — a case may fan out or return.
Verdicts and FlowReport
Flow agents end their turn by calling the FlowReport tool exactly once:
verdict— for switch nodes, exactly one of the declared case keys; for sequential nodes,doneoutput— the node's work output, passed downstream as$<nodeId>.outputslots— structured fields matching the node'soutputsdeclaration, referenced downstream as$<nodeId>.slots.<field>
Verdict-to-case matching runs in order:
- Exact (case-insensitive)
- Same family —
ok/pass/success/done/clean/fixedform one family;error/fail/reject/revise/abortthe other — when the cases contain exactly one key of that family - Substring containment (logs a warning — a legacy fallback, not a design tool)
Declare canonical case keys and have agents report exactly those. Two migration controls govern how strictly this is enforced:
strictVerdict(flow-level, defaultfalseduring migration) — whentrue, a switch node without a valid FlowReport verdict fails the nodelenient(switch-level, explicit opt-in) — allows guessing the verdict from output text; for legacy un-migrated flows only, never set it on new flows
Parallel Fan-out and Barrier Joins
"onComplete": { "parallel": ["r1", "r2"] }
Every fan target starts concurrently. A node with in-degree ≥ 2 is a Barrier: it fires exactly once, after all upstreams have arrived.
Fan failure policy via onFail:
| Value | Behavior |
|-------|----------|
| abort (default) | Fail-fast — one branch fails, siblings are cancelled, flow fails |
| collect | Failed branch yields a placeholder output; the barrier still releases |
Parallel branches must converge at a join before $return — a branch that returns mid-flight is rejected at load time.
Load-time Validation
A flow is rejected at load when:
- It has fewer than 2 nodes
- No node routes to
$return(a pure cycle can only end in "max loop exceeded") - A parallel branch can reach
$returnbefore converging at a join - A barrier join mixes parallel and serial arrivals (a serial in-edge would corrupt the barrier count)
Guardrails
maxLoop(default 10) — caps back-edge traversals per run; single-pass pipelines setmaxLoop: 1maxFanout(default 4) — caps parallel fan-out width
There is no wall-clock timeout — node liveness is guaranteed by the agent's internal LLM/tool timeouts. Keep single-node workloads bounded; split heavy steps instead.
Failure Handling
onError chooses what happens when a node fails:
| Value | Behavior |
|-------|----------|
| stop (default) | Abort the flow |
| restart | Re-execute the node; combine with maxRetries for transient failures (network, tooling) |
| resume | Log a warning and continue routing |
Input Templating
Node input templates substitute from the flow context:
| Variable | Meaning |
|----------|---------|
| $task | The flow's input task |
| $<nodeId>.output | Output of a completed node |
| $<nodeId>.slots.<field> | Structured slot of a completed node |
Each node's input should explicitly list all upstream outputs it needs — only templated values cross edges (each node runs in a fresh session). If a template references a node that has not run yet, the engine injects the literal placeholder [output of <nodeId> not found]; the template text should tell the agent what that means.
Flow Agents
A node's agent resolves to the flow-local directory flows/<name>/agents/<agent>/ first; when absent, it falls back to the global agent of the same name. Prefer the fallback over copying a global agent's definition into the flow.
Keep flow agents single-purpose and deterministic: they run the same way every time, driven by the flow definition.
Triggering
Agents trigger flows with the FlowTrigger tool, gated by the flows whitelist in their agent.json (see Delegation). The flow runs in the background; its result is delivered when it completes.