Flows

A flow is a typed, durable graph of work. Each node is an agent, a reviewed action, a pure data step or another flow; edges carry typed values between named ports. The daemon compiles the graph, runs it, checkpoints it, and can stop, resume or replay it after a restart, so long jobs (plan a feature, implement every bead, land the PRs) run without someone babysitting them.

The example flow lighthouse.triage: the typed input request goes to collect, a configured action pinned to node vega, which returns a list of error groups. A map edge fans the list out to up to three ephemeral fix agents at once, one per group, and gathers their results into a list for review. Review is an action that suspends: it sends a request to the Operator Inbox and waits up to three days for a decision, then emits a checkpointed summary and a Markdown report artifact.
A small flowAn action collects error groups, a map fans them out to three fix agents at a time, and review waits for your decision before emitting a checkpointed summary and report.

The example, in full

This is the flow in the diagram. It compiles as written with caco flow validate --file triage.flow.yaml.

flow: lighthouse.triage
imports:
  t: ./types.jtd.yaml
  map: caco://std.map
in:
  request: t.Request
out:
  summary: {type: t.Summary, checkpoint: true}
  report: {artifact: text/markdown, checkpoint: true}
nodes:
  collect:
    action: {name: lighthouse.collect-errors, node: vega}
    in: {request: t.Request}
    out: {groups: t.ErrorGroupList}
  fix:
    agent: {mode: ephemeral, timeout: 1h}
    instructions: Reproduce and fix one error group in your own worktree.
    in: {group: t.ErrorGroup}
    out: {result: t.FixResult}
  review:
    action: lighthouse.review-fixes
    in:
      results: t.FixResultList
      decision: t.Decision
    out:
      request: t.ReviewRequest
      summary: t.Summary
      report: {artifact: text/markdown}
    suspend: {request: request, resume: decision, policy: park, ttl: 3d}
edges:
  - from: in.request
    to: collect.request
  - use: map.map
    from: collect.groups
    through: {node: fix, in: group, out: result}
    concurrency: 3
    all: review.results
  - from: review.summary
    to: out.summary
  - from: review.report
    to: out.report

Types are JSON Type Definition in a separate module (types.jtd.yaml), imported under a short alias. A map source must be a named list type (ErrorGroupList: {elements: {ref: ErrorGroup}}), and all: gathers the results into another one.

Nodes

Every node has named in and out ports and exactly one executor:

ExecutorWhat runsUse it for
agentAn ordinary managed agent with its own worktree, profile and lifecycle. It returns values with caco flow emit.Judgement: planning, implementing, reviewing. mode: ephemeral for one turn; persistent/session for a long-lived worker reused across items or iterations.
actionA reviewed configured action, optionally pinned to a node ({name, node}), with durable receipts and idempotence.I/O and deterministic computation: fetch, publish, join a merge queue.
dataA pure operation: identity, literal, select, merge or digest. No shell, files, network, clock or randomness.Reshaping values and stable hashes without starting an agent.
flowAnother flow, imported by name; its ports must match exactly.Composing larger pipelines from tested pieces.
loopAn acyclic iteration flow run repeatedly over a typed state.Repeat-until-done work with explicit bounds (see loops).

A node can also suspend: it emits a request port and waits, up to a TTL, for a typed response, which arrives through the Operator Inbox or caco flow input. An agent node can be bound to a bead with assignment, which claims the bead when the agent starts and withholds its outputs until normal reintegration succeeds; recurring repository work without a bead uses lifecycle: {completion: reintegrated} instead.

Edges and values

A real pipeline: std.feature.delivery

The standard library ships whole journeys built from these parts. std.feature.delivery takes a feature request to merged pull requests: plan, review, create beads, implement them layer by layer, and land the results through Cara.

std.feature.delivery runs two nested flows. Planning: compose builds a planning request, an ephemeral planner agent writes a plan and a Markdown artifact, and review suspends for an operator approve, revise or cancel decision. Delivery: an approved plan becomes a dependency-correct bead graph, a burndown loop implements each newly ready layer of beads with workers until every tracked bead is closed, published pull requests are routed one at a time into Cara, a run-resident integrator agent handles each admission in order, and summarize combines the bead graph, burndown state and integration receipts into the final delivery receipt.
std.feature.deliveryTwo nested flows: planning stops at your review; only an approved plan creates beads, which burndown implements and integration lands one PR at a time.

Nothing mutates the board until you approve the plan: a revise or cancel decision is rejected before any bead is created. Workers claim their beads natively, so they show up in Agents and Beads like any other work, and the target project's reintegration policy (not the flow) decides how each change lands.

Loops

Edges are always acyclic. Iteration is a loop node around an acyclic flow that takes and returns the same typed state plus a decision; the engine starts a new child run for each continue and emits the state on done.

std.feature.burndown initializes state from the bead graph receipt and hands it to a loop node. Each iteration is a child run of std.feature.burndown-iteration: it reads the tracked bead IDs, snapshots the board, assigns the ready beads to up to four worker agents in parallel, snapshots again and advances the state. Advance returns continue, which starts the next child run, or done, which emits the final state; if every remaining bead is blocked it suspends for an operator decision. The loop is bounded to 100 iterations and seven days.
Burndown loopEach iteration is a separate child run; advance decides whether another is needed, and a fully blocked board suspends for you.

Loops are bounded by max_iterations and ttl. A long-lived service instead sets until_cancelled: true with an interval, so an empty queue waits for the next round rather than busy-polling. Iteration count, state, child runs and decisions are checkpointed, so a loop resumes where it was after a restart.

Runs

A run is one execution of a flow. The daemon on its owner node compiles the exact revision, stores the run and its events in SQLite, and checkpoints every accepted value, so a run survives restarts and can be inspected long after it finishes.

You start a run with a request ID pinned to an owner node. The owner reserves the request fingerprint, compiles the revision and commits the run with its acceptance receipt before dispatching anything. It invokes a pinned action on another node over mTLS and records a durable receipt, starts an ordinary agent for an agent node, and validates and checkpoints each value the agent emits. A suspending node files a decision in the Operator Inbox; your answer resumes it. When every output is emitted the run succeeds. After a daemon restart the run is restored from its stored snapshot and events: a started action is replayed only if its contract says the request is idempotent, and a suspended node is not re-armed.
One run, end to endThe owner commits the run before dispatching anything, validates every emitted value, files suspensions in the Operator Inbox, and restores from its own store after a restart.

Run and node states

A run starts running. While any node waits for input the run is waiting for input, and it returns to running when the typed response arrives. caco flow stop moves any non-terminal run to stopped, keeping its agents, sessions and checkpoints, and caco flow resume returns it to running. A run succeeds when every output has been emitted and nothing is active, fails when a node failure is not routed, and caco flow cancel ends any non-terminal run. Succeeded, failed and cancelled are terminal.
Run statesStop is a reversible pause that keeps agents and sessions; cancel is final and keeps whatever outputs and receipts already exist.
Each node invocation waits until all of its inputs have arrived, becomes ready, and runs once leased. A suspending node moves to waiting for input until its response arrives. An agent bound to a bead or a lifecycle contract moves to waiting for lifecycle after emitting its outputs, and only succeeds once Caco observes the agent's successful reintegration; until then no output is delivered downstream. Any active invocation can fail or be cancelled.
Node statesA node bound to a bead or lifecycle contract holds its outputs until its agent has actually reintegrated.

Scheduled and event-triggered runs

A flow can declare activations that start runs for it: cron on a schedule, or feed_event when a matching event appears in the feed (for example a critical log_error).

activations:
  nightly:
    kind: cron
    schedule: "0 7 * * *"
    node: atlas
    input: activation
    policy: {overlap: skip, replay: retry, max_concurrency: 1}

Each activation names one exact node, so replicated registries never start the same run twice, and the daemon records which event or schedule minute started which run against which compiled revision. caco flow activations --project NAME lists those receipts. To wait for something inside a run that already exists, use std.event.await instead.

Writing a flow

  1. caco flow scaffold --name team.task --output-dir /tmp/task writes a minimal flow and type module.
  2. caco flow validate --file /tmp/task/task.flow.yaml type-checks ports, imports and topology; fix every reported path rather than loosening a type.
  3. caco flow explain --file … lists executors, ports and suspensions; caco flow graph --file … --format mermaid (or dot) draws it.
  4. caco flow simulate --request-file sim.json runs it in memory with fixture outputs: the real compiler and runtime, but agents and actions only record what they would do.
  5. caco flow run --file … --project NAME runs it for real. Move it under .cacophony/flows/ once it should become a reviewed project flow.

The caco binary embeds the Cacophony project's .cacophony/flows/ and its flow rollout inventory at build time, so a node without the Cacophony checkout still has those flows, governed by the embedded inventory. When a checkout's flow root exists it replaces the embedded set as a whole, so one registry never mixes revisions.

Before running a generated flow, check that every map is bounded, every loop has limits (or a paced until_cancelled), every human decision has a TTL, and every action exists in reviewed configuration. Agents can also propose follow-up flows for review with caco flow continuation propose; nothing starts until you approve that exact revision.

Command reference

CommandPurpose
caco flow list · show · compileRegistered flows with their revisions, one resolved graph, or its compiled IR and JSON Schemas.
caco flow validate · explain · graph · simulate · scaffoldAuthor and check a flow without running it.
caco flow runStart a run (keyed starts: --request-id + --owner-node).
caco flow runs · status · watch · detailList runs, show one, stream its events (--until-terminal), or fetch its timestamps, compiled definition and source.
caco flow stop · resume · cancelPause and resume with the same agents, or end the run.
caco flow input · emitAnswer a suspended node; return a value from an agent node.
caco flow artifactDownload an artifact referenced by a run, verified against its digest.
caco flow activations · reportsActivation receipts; maintenance reports from the standard maintenance flows.
caco flow continuation …Propose, review, approve and start agent-generated follow-up flows.

Every command is also an MCP tool. Flows and live run graphs appear in the TUI (Project › Flows), the web dashboard, the Android app and the macOS app.