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, done
  • output — the node's work output, passed downstream as $<nodeId>.output
  • slots — structured fields matching the node's outputs declaration, referenced downstream as $<nodeId>.slots.<field>

Verdict-to-case matching runs in order:

  1. Exact (case-insensitive)
  2. Same familyok/pass/success/done/clean/fixed form one family; error/fail/reject/revise/abort the other — when the cases contain exactly one key of that family
  3. 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, default false during migration) — when true, a switch node without a valid FlowReport verdict fails the node
  • lenient (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 $return before 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 set maxLoop: 1
  • maxFanout (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.