Fan out: one event per item
scout/nodes.yaml
identity names the stable per-item key; max_items bounds the fan-out. Record the member
list (and a window key, such as a batch id) in entity fields — the join below reads them. A
window groups arrivals into one batch — here, every result carrying the same batch_id
counts toward the same join.
One instance per item
The receiving flow declares its identity once, and instances mint themselves on first delivery — there is no creation call:flows/matcher/schema.yaml
instance:; the pin’s instance key
derives from it. Each instance materializes its own copy of the flow’s declared agents —
one live agent per (agent declaration, instance) pair — so one instance finishing never
affects a sibling’s agent, and memory: true agents get one conversation per instance.
Fan in: the barrier join
Convergence is owned by two declarations on the collecting flow: a fan-in resolution pin (which events count, how they deduplicate, which window they belong to) and a stagedjoin:
row (membership, completion, timeout):
scout/schema.yaml (input pin)
scout/nodes.yaml (join row)
The canonical runnable version of this whole shape is
examples/routing/fan-in/barrier.
The singleton coordinator
When many template instances need shared state — a rate budget, an aggregate score, a cross-instance learning table — that state gets one owner: amode: singleton coordinator
flow whose entity holds it as typed contained map/list fields. Instances report to the
coordinator through ordinary events (often the fan-in pin above); the coordinator’s handlers
update the contained state deterministically.
- Items in that contained state have no lifecycle of their own — no timers, retries, or terminal stage. If an item needs those, promote it to a template instance.
- The coordinator role is explicit — a plain
mode: staticflow does not qualify.
Bounded loops: declared revision cycles
When work legitimately cycles — draft, review, revise, review again — the cycle is declared, capped, and given an explicit exit. Declare the cycle withloops: rather than hand-rolling
a counter in a guard:
nodes.yaml
start creates the loop’s durable record at attempt 1, admit
accepts work for the current revision, repeat increments the attempt and mints a new
revision (or, at the cap, atomically takes the declared escape), and close exits the
region. max_attempts is evaluated once at start and stamped into the activation — editing
policy mid-run never changes an active loop’s cap, and replay sees the same cap the run did.
Every operation declares its exact from: stage; the runtime accepts no inferred source.
Verify checks the loop’s shape statically: one start from outside the cycle, at least one
repeat and close, an escape target outside the cycle, and every handler inside the cycle
declaring its loop operation. Nested and unbounded loops are rejected.
Stage timers: time as a lifecycle edge
Both shapes above compose with stage timers — one-shot temporal rows declared on the stage itself:schema.yaml
advances_to is a real lifecycle edge — reachability analysis and describe --graph see it.
