Skip to main content

Node reference

This is the precise JSON AST reference for Flux-Lang. Authors write native .flux text, while SDK callers and host tooling may construct this shape directly and sessions store it. Text and JSON are semantically identical: every .flux construct lowers to these nodes, every node kind has a native text spelling, and @json remains as the escape for rare AST shapes the text grammar cannot preserve exactly. Native array indexing is ordinary source — response.results[0] lowers to the dot-segment path .results.0 and formats back with brackets. Only a hand-built jq whose path string itself contains brackets needs @json, alongside shapes such as non-identifier symbol names and non-invertible expr formulas.

Top-level shape

A flow is a JSON object:

{
"name": "optional-name",
"params": [{"name": "ticket", "ty": {"named": "Ticket"}}],
"returns": {"named": "Result"},
"body": []
}

name, params, and returns are optional; body is the ordered list of statement nodes the runtime executes top to bottom. A node is tagged by its "kind".

Node kinds at a glance

This table is derived from the generated node catalog in the repository's language reference, which is produced from the interpreter's own AST definitions.

kinddescription
callInvoke a registered operation with argument expressions.
bindBind the result of an expression to a symbol.
whenConditional control flow.
repeatA bounded loop (max is required; the analyzer rejects unbounded loops).
eachMap a list value through a body (list-driven loop; repeat stays counter-driven). Each element is bound to as; an optional collect symbol gathers the per-iteration results into a list.
assertA boolean guard: aborts the flow with an error if the condition is falsey.
pipeA chain of calls where each step's output is fed as the first argument of the next.
seqA sequential block; runs its body in order. Optionally binds the block's final result.
memoCache a call result across turns in one session. The cache identity is the operation plus its canonical argument AST, not the destination symbol name. On a hit, rebind name to the cached immutable value; changing the operation or arguments recomputes. Unlike bind, value must be a call.
parallelConcurrent fan-out: run independent branches, binding each branch's result to its name.
awaitPause until an external event/input arrives.
retryRetry a body on failure with optional backoff. Fatal errors (policy denial, unknown op) are never retried. backoff may be "none" | "linear" | "exponential".
tryStructured error handling: run body; on failure bind the error string to catch and run handler. If the handler also errors, propagate that error.
confirmExplicit human-in-the-loop gate. Calls the existing Approver--yes and TUI modal handle it automatically. Body only runs on approval; on denial the node errors. risk may be "low" | "medium" | "high" | "critical".
loopTime-bounded iteration. for_ms is required (the analyzer rejects unbounded loops). every_ms is the inter-iteration sleep (0 = tight). until is an early-exit condition.
raceFirst-wins concurrency: run branches in parallel and return as soon as the first succeeds. timeout_ms is required; if no branch succeeds within it the node errors. bind names the symbol that receives the winning branch's result.
throttleRate-limit body execution: at most max dispatches per window_ms sliding window. The token bucket is tracked in the session store keyed by name; plan authors declare intent, runtime enforces. name must be unique within a session to avoid bucket collisions.
debounceCoalesce rapid re-invocations: wait wait_ms after the last trigger before running body. In a loop/watch context the body only executes when things have settled. name is used as a stable key so debounce state survives across turns.
unlessNegated conditional: run body only when cond is falsey. Sugar for when !cond; the body may contain any nodes (reads, writes, sub-plans — anything).
verifyRun a command and assert its output contains an expected substring; abort the flow with a structured error if it does not. cmd is any node that produces a string (typically a bash call); expect is the substring the output must contain.
returnEnd the flow with a value.
peekRead the current in-session value of a named symbol without any filesystem IO. Returns the symbol's stored value, or an empty string if the symbol is not yet bound.
varReference a bound symbol.
litA literal JSON value carried directly in the AST.
thingA reference to an external thing.
exprPure inline computation. formula is a safe whitelist expression over named variables: arithmetic (+ - * /, round(x,n), abs, min, max, sum), comparison (== != < <= > >=), boolean (&& || !, true/false, any, all, has), string functions (len/lower/upper/trim/replace/repeat/reverse/contains/concat/join/split), list helpers (first/last), string literals ('…'/"…"), lists, and objects. Dotted names such as it.author.name descend object fields leniently (missing/null/non-object hops read as ""). + adds when both sides are numeric and concatenates otherwise. Because it yields a bool, an expr is also a valid when/unless/until/assert condition. vars maps formula identifiers to Lit or Var nodes. No IO, no approval gate. Native source writes an invertible formula directly, for example doubled = $price * 2.
fmtPure string interpolation. template is a string with {name} placeholders substituted from already-bound session symbols (same {name}/{{name}} syntax as Lit interpolation). No IO, no approval gate. Example: fmt("BTC: {price} | Double: {doubled}").
jqPure JSON path extraction. path is a dot/index path (for example ".bitcoin.usd" or ".results.0.value") applied to the JSON content of a Var, Lit, Obj, or List input. Native first = response.results[0].value lowers its array index to the .0 AST segment; a quoted object key such as response.headers["content-type"] stays the unambiguous JSON-string segment .headers["content-type"]. JSON escaping keeps dots, brackets, quotes, backslashes, empty keys, Unicode, and numeric-looking object keys as data. No IO, no approval gate. optional selects the traversal-through-missing-data policy. When false — the default for native x.field sugar — an absent object key, an out-of-range index, or a field access on a non-object is a loud error, so a typo'd field name fails fast instead of silently reading empty. When true — native x.field? sugar, or a legacy JSON AST that omitted the flag — such a miss yields null. A present-but-null field is never an error in either mode.
parsePure type coercion over a literal, bound symbol, or object/list template. Bind a computed jq or fmt result before parsing it. as_type is one of "f64", "i64", "bool", "json", "string", or "form". "json" also serializes a structured value as canonical JSON; "form" serializes a flat record as application/x-www-form-urlencoded text. No IO, no approval gate. Example: number = parse(price_text, as: "f64").
ctxBuild a bounded, budgeted context pack from existing symbols. Resolves include (minus exclude) to its members, then — when budget is set — shrinks the pack at evaluation by visibility tier then declared order until within the char budget, recording any dropped members in the run trace. Produces a Ctx value bound to name. Pure: it selects and labels existing values, performing no IO.
ctx_appendAccrete more symbols into an existing context pack (the += marker). Creates a new immutable Ctx value, updates ctx to resolve to that version, preserves the earlier version in the audit trail, then re-applies the pack's budget. Pure.
matchMulti-way exhaustive branch: evaluate subject (a literal or bound symbol), then run the body of the first case whose value equals it — by JSON equality, so a string subject does not equal a numeric literal. If none match, run default. A deterministic replacement for chains of when. To branch on an op result or field access, bind it first, then match the resulting symbol; use route for a model-selected label. The analyzer requires at least one case; at runtime an unmatched subject with no default is an error — the exhaustiveness guard-rail.
routeModel-routed branch — the signature bounded non-determinism primitive. Run selector (typically a !model op) to produce a label, then run the case whose label it names. The cases are fixed and analyzer-validated: the model chooses which declared branch runs, never what. Falls back to default when the label matches no case (an error if default is empty).
fallbackOrdered "first that succeeds wins" selector: run each branch in branches in turn; the first that completes without error and yields a non-empty result wins and becomes the node's result. On a branch error (or empty result) the next is tried — so a side-effecting branch that returns empty will still fall through and the next branch also runs (attempts stream live, as in try/retry). If every branch errors, the last error propagates. Lighter than try for graceful degradation (cheap path → else expensive path). bind names the winning result.
timeoutBound the wall-clock of a sub-flow: run body with a ms deadline. If it does not finish in time the node errors (an enclosing try/retry may catch it). A general reliability guard-rail you can wrap around anything. bind names the body's result.
budgetCap the cost of a scope: run body but allow at most limit op dispatches within it (checked at statement boundaries; a nested statement can consume more than one dispatch before the next check). A first-class cost guard-rail; v1 counts dispatches (token/money budgets are a later refinement). bind names the body's result.
cap_scopeCapability scope: run body, but restrict op dispatch to the tool names in tools — a call to anything outside that allowlist fails closed at the runtime's dispatch gate, even when the outer session policy would allow it. Capabilities only ever narrow on descent: a nested with_tools is intersected with the scope it's nested in, so an inner block can never re-grant a tool an outer one removed. This is the runtime-enforced counterpart of an advisory tool restriction — the analyzer also flags a literal-op call here that provably names a tool absent from tools (a static echo of the same rule dispatch enforces dynamically). bind names the body's result. Native text: with_tools ["read", "grep"] + an indented body.
scopeRAII-style acquire → use → release with guaranteed cleanup. Optionally run acquire first (binding its result to bind, so body and finally can name the resource), then run body; finally always runs afterward — on normal completion, an early return, or an error — so a lock is freed / a handle closed / a temp removed no matter how the body exits. The body's result, return, or error then propagates; a finally failure surfaces only when the body itself succeeded (it never masks the body's own error). If acquire errors the resource was never taken, so finally does not run. The deterministic resource-lifecycle guard-rail (RAII for flows).
sagaSaga / compensating transaction: run each step in order; after a step's body succeeds, its undo is registered. If a later step fails, the runtime unwinds by running the registered undo bodies in reverse order (best-effort — an undo failure is recorded but does not stop the unwind), then propagates the original error. The strongest guard-rail for non-transactional external side effects (charge→refund, create→delete, reserve→release): partial work is rolled back rather than left dangling. A return inside a step is a successful early exit and does not compensate (use scope for guaranteed cleanup on every exit).
onceDurable de-duplication after completion is recorded — an effect-level memo. The durable identity combines the session, explicit label, and canonical body identity: an identical labeled body is skipped after its successful OnceCompleted record, while two different bodies may safely reuse a human label. A crash after an external effect succeeds but before that completion record is appended can repeat the effect on re-run; use an externally idempotent operation or transaction when duplicates are unacceptable. A failed body records nothing and is retried. bind optionally names the stored result. With no durable store wired (a throwaway interpreter) it degrades to running every time. Requires a non-empty literal label.
checkpointDurable resume point for long-running / resumable flows. A top-level-only marker (like await): the first time a run reaches it, the position is recorded durably; a later re-run of the same flow in the same session fast-forwards past the already-completed prefix (its symbols are still durably bound and its side effects are not repeated) and continues from here. The resume key is the session plus flow identity (declared name and canonical body hash); the label is human-readable event metadata for the phase it closes, not the key. Pairs with once for finer-grained idempotency; a no-op when no durable store is wired. Requires a non-empty literal label.
objBuild an object value from sub-expressions — the record constructor { k: expr, … }. Each field value is itself a node, so a record can mix literals and symbols: { ok: true, count, intent: extract.intent }. Pure: it assembles a value, performing no IO and no op dispatch. Leaves may be var/lit/jq/expr/fmt/parse/obj/list; a call or control-flow leaf is rejected by the analyzer so templates stay side-effect free. This is what lets return { … } assemble a result from computed symbols.
listBuild a list value from sub-expressions — the list constructor [ expr, … ]. Each item is itself a node ([a, b, 3]). Pure, same leaf rules as [Node::Obj]; the array twin of the record constructor.

Primitive and expression nodes

These produce values without side effects. The exact positions each node supports differ: for example, a var or literal can be a call argument or condition, while a computed jq, fmt, or parse value usually needs to be bound first. None is executable as a standalone statement.

lit

A literal JSON value. String literals support {symbol} interpolation at evaluation time (unbound tokens stay verbatim; interpolation recurses into strings inside arrays/objects).

{"kind": "lit", "value": {"key": "val"}}
fieldtyperequireddescription
valueany JSONyesthe literal value

var

A reference to a bound symbol, resolved to its stored value. Canonical text form: bare name.

{"kind": "var", "name": "draft"}
fieldtyperequireddescription
namestringyesthe symbol name (no leading $)

An unbound symbol is a hard error at evaluation time.

thing

A reference to an external object, resolved before execution begins. Text form: thing <kind> <selector> "<value>" — e.g. thing person name "John", thing file path "src/x.rs", thing custom "widget" key "w-1".

{"kind": "thing", "thing": {"kind": "person", "selector": {"name": "John"}}}
fieldtyperequireddescription
thing.kindThingKindyescontext / file / person / ticket / email / repo / dataset / calendar_event / url / secret / custom
thing.selectorSelectoryesid / name / path / query / key

Core statement nodes

call

Invoke a registered operation. In the AST, a multi-parameter op receives one object node naming its inputs; a sole-required-parameter op may receive one bare value. Canonical text projects that object as brace-free named inputs: op(name: value, …). A standalone call may use do op when it has no arguments.

{"kind": "call", "op": "write", "args": [
{"kind": "lit", "value": {"path": "out.txt", "content": "hi"}}
]}
fieldtyperequireddescription
opstringyesthe registered op name
argsNode[]noempty, one bare value, or one object naming each parameter (2+ bare values is rejected)

Every call goes through the dispatch envelope; a standalone call's result is discarded from the symbol table but still traced.

bind

Evaluate an expression and bind its result to a symbol. Text form: name = expression (optionally name: Type = expression, and optionally preceded by @effect(tag)). Unlike memo, a regular bind may contain any valid bind-value expression, not only an operation call.

{"kind": "bind", "name": "draft",
"value": {"kind": "call", "op": "read", "args": [{"kind": "lit", "value": "README.md"}]}}
fieldtyperequireddescription
namestringyessymbol to bind
valueNodeyesthe expression to evaluate
tyTypeRefnotype hint stored alongside the symbol
effectFlowEffectnodeclared semantic effect (drives risk + approval)

An errored evaluation aborts the flow — nothing is bound on error.

return

End the flow immediately with a value, unwinding all enclosing blocks. Text form: return [expr].

{"kind": "return", "value": {"kind": "var", "name": "draft"}}
fieldtyperequireddescription
valueNodeyesthe flow's return value

Rejected inside parallel branches.


Control flow

when

Conditional branch on truthiness. Text form: when <cond> / else.

{"kind": "when", "cond": {"kind": "var", "name": "ok"},
"then": [], "otherwise": []}
fieldtyperequireddescription
condNodeyesthe condition
thenNode[]nobody when truthy
otherwiseNode[]nobody when falsey

unless

Negated conditional — sugar for when !cond, no else branch. Text form: unless <cond>.

fieldtyperequireddescription
condNodeyesbody runs when this is falsey
bodyNode[]noany nodes

assert

Abort with an error if the condition is falsey. Text form: assert <cond>[, "message"].

{"kind": "assert", "cond": {"kind": "var", "name": "hits"},
"message": "grep returned no results"}
fieldtyperequireddescription
condNodeyesthe guard condition
messagestringnoerror detail shown on failure

match

Deterministic multi-way branch by JSON equality. Text form: match x + case <value> / default arms. The subject must be a literal or a bound symbol; bind an operation result or field access before matching it.

{"kind": "match", "subject": {"kind": "var", "name": "status"},
"cases": [{"value": {"kind": "lit", "value": "ok"}, "body": []}],
"default": []}
fieldtyperequireddescription
subjectNodeyesliteral or symbol reference
casesMatchCase[]yesat least one {value, body}
defaultNode[]noruns when no case matches (else: error)

route

Bounded model routing: a selector produces a label; the matching case runs. Text form: route <selector-call> + case "label" / default arms.

{"kind": "route",
"selector": {"kind": "call", "op": "classify", "args": [{"kind": "var", "name": "ticket"}]},
"cases": [{"label": "bug", "body": []}, {"label": "billing", "body": []}],
"default": []}
fieldtyperequireddescription
selectorNodeyesnode producing a label
casesRouteCase[]yesunique, non-empty labels with bodies
defaultNode[]noruns on an unknown label (else: error)

fallback

Ordered first-useful-success selector. Text form: fallback -> result + bare branch arms.

fieldtyperequireddescription
branchesFallbackBranch[]yesordered {body} branches
bindstringnosymbol for the winning result

A branch error or empty result falls through; all-empty keeps the first empty success; all-errored propagates the last error.


Iteration

repeat

Bounded counter loop. Text form: repeat N with optional until as the first body line.

{"kind": "repeat", "max": 5, "until": {"kind": "var", "name": "done"}, "body": []}
fieldtyperequireddescription
maxu32yesmaximum iterations
untilNodenostop-when-true guard, checked after each iteration
bodyNode[]noloop body
collectstringnosymbol bound to the list of per-iteration results

each

List-driven loop. Text form: each x in list [-> collected | -> flat collected].

{"kind": "each", "in": {"kind": "var", "name": "files"}, "as": "f",
"body": [], "collect": "contents"}
fieldtyperequireddescription
inNodeyesexpression yielding a list (anything else errors)
asstringyeselement symbol per iteration
bodyNode[]noper-element body
collectstringnosymbol bound to the per-iteration results (empty list ⇒ [])
flatboolnowhen collect is set, flatten each iteration's (list) result one level into the combined list

loop

Time-bounded iteration. Text form: loop for 30s, every: 2s, until: done -> result; every, until, and the result binding are optional.

{"kind": "loop", "for_ms": 10000, "every_ms": 1000,
"until": {"kind": "var", "name": "done"}, "bind": "last", "body": []}
fieldtyperequireddescription
for_msu64yeswall-clock deadline (ms)
every_msu64nointer-iteration sleep (default 0 = tight)
untilNodenostop-when-true guard after each iteration
bodyNode[]noloop body
bindstringnosymbol for the last iteration's result

A body error ends the loop immediately — use retry inside the body for per-iteration resilience.


Sequencing

seq

Sequential block, optionally binding its final result. Text form: seq [-> result].

fieldtyperequireddescription
bodyNode[]nostatements to run in order
bindstringnosymbol for the block's final result

pipe

Chain calls, each step's output fed as the next step's first argument. Text form: pipe [-> result] + one indented call per line.

{"kind": "pipe", "bind": "hits", "steps": [
{"kind": "call", "op": "read", "args": [{"kind": "lit", "value": "log.txt"}]},
{"kind": "call", "op": "grep", "args": [{"kind": "lit", "value": "ERROR"}]}
]}
fieldtyperequireddescription
stepsNode[] (call)nopipeline steps
bindstringnosymbol for the final step's result

memo

Like bind, but caches a call result across turns in one session. The cache identity is the called operation plus its canonical argument AST, not the destination symbol name. On a cache hit the stored value is bound to the authored name again. Text form: memo name[: Type] = <call> (optionally preceded by @effect(tag)). memo accepts only a call expression; use an ordinary bind for other pure expressions.

{"kind": "memo", "name": "survey",
"value": {"kind": "call", "op": "read", "args": [{"kind": "lit", "value": "big.log"}]}}
fieldtyperequireddescription
namestringyessymbol that receives the cached value
valueNode (call)yesthe call to run on a cache miss
tyTypeRefnotype hint
effectFlowEffectnodeclared effect

Concurrency

parallel

Concurrent fan-out; each branch's result binds to its name; results merge in declaration order. Text form: parallel + branch name arms. See Concurrency.

fieldtyperequireddescription
branchesBranch[]yes{name, body} — names unique; return and cross-branch binds rejected

race

First-success concurrency under a required deadline. Text form: race 5s [-> result] + branch name arms. See Concurrency.

fieldtyperequireddescription
timeout_msu64yeswall-clock deadline
branchesBranch[]yes{name, body} run concurrently
bindstringnosymbol for the winning result

Error handling and guards

try

Run body; on failure bind the error string to catch and run handler. Text form: try + body, then an optional catch [err] + handler block. See Reliability & guard rails.

fieldtyperequireddescription
bodyNode[]nothe guarded body
catchstringnosymbol bound to the error string
handlerNode[]noruns only on failure; its own error propagates

retry

Retry a body on transient failure. Text form: retry <max>, backoff: <strategy>, delay: <duration> [-> result].

fieldtyperequireddescription
maxu32yesmaximum attempts including the first
backoffstringno"none" (default) / "linear" / "exponential"
delay_msu64nobase delay in ms; defaults to 500 when omitted, so a bare retry 3 waits half a second between attempts
bodyNode[]nobody to retry
bindstringnosymbol for the successful result

Fatal errors (policy denial, unknown op, type errors) and denied confirms are never retried.

verify

Run a command node and assert its output contains a substring. Text form: verify <cmd> contains <expect>[: "message"].

fieldtyperequireddescription
cmdNodeyesnode producing a string
expectNodeyessubstring the output must contain
messagestringnoerror text on failure

confirm

Explicit human approval gate. Text form: confirm "message", risk: <level> + optional body. See Reliability & guard rails.

fieldtyperequireddescription
messagestringyeswhat will happen
riskstringno"low" / "medium" (default) / "high" / "critical"
bodyNode[]noruns only on approval; empty body = pure gate

Cost, rate, and capability control

timeout

Wall-clock deadline on a body. Text form: timeout 5s [-> result].

fieldtyperequireddescription
msu64yesnon-zero deadline
bodyNode[]nobody under the deadline
bindstringnosymbol for the body's result

budget

Dispatch cap on a body, checked at statement boundaries. Text form: budget <n> [-> result].

fieldtyperequireddescription
limitu32yesnon-zero max dispatch count
bodyNode[]nobody under the cap
bindstringnosymbol for the body's result

cap_scope

Runtime-enforced tool allowlist for a body; nested scopes intersect. Text form: with_tools ["a", "b"] [-> result].

fieldtyperequireddescription
toolsstring[]yesallowed tool names
bodyNode[]nobody under the capability scope
bindstringnosymbol for the body's result

throttle

At most max dispatches per sliding window, keyed per session by name; errors instead of blocking. Text form: throttle "name", max: <count>, per: <duration> + body.

fieldtyperequireddescription
namestringyesbucket key (survives across turns)
maxu32yesmax dispatches in the window
window_msu64yessliding window size
bodyNode[]nothe rate-limited body

debounce

Cross-turn coalescing: the body runs only after wait_ms of quiet for the key. Text form: debounce "name", wait: <duration> + body.

fieldtyperequireddescription
namestringyesstable key per (session, name)
wait_msu64yessettling window
bodyNode[]nobody to run once settled

Cross-turn and durability

peek

Read a symbol's in-session value without IO (empty if unbound). Text form: peek name — an expression, valid as a bind value or condition.

fieldtyperequireddescription
namestringyessymbol to look up (no leading $)

await

Suspend until an external event; resume binds the received value. Top-level only. Text form: await [received[: Type] =] "source". See Durability & cross-turn state.

fieldtyperequireddescription
sourcestringyesevent source identifier
bindingstringnosymbol for the received value
as_typeTypeRefnolenient coercion for the received value

scope / saga / once / checkpoint

The durability quartet — guaranteed cleanup, compensation, completed-effect de-duplication, and durable resume. Text forms: scope [resource = <acquire>] + body + finally + cleanup; saga with step / undo arms; once "label" [-> result] + body; checkpoint "label". Semantics, examples, and field-by-field behavior are on Durability & cross-turn state. Field summary:

kindfields
scopeacquire (Node, optional), bind (string, optional), body (Node[]), finally (Node[])
sagasteps (SagaStep[]) — each {body, undo?}
oncelabel (non-empty literal string), body (Node[]), bind (string, optional)
checkpointlabel (non-empty literal string)

Pure computation

expr / fmt / jq / parse

The pure computation nodes — full semantics, whitelists, and examples on Pure data shaping. Field summary:

kindfields
exprformula (string), vars (map name → lit/var node) — every formula variable must be declared
fmttemplate (string with {name} placeholders) — text form fmt("…")
jqpath (field/index path string), input (var/lit/obj/list node) — native value.items[0] lowers the index to .items.0; value.headers["content-type"] lowers the quoted object key to .headers["content-type"]
parsevalue (var/lit/obj/list node), as ("f64"/"i64"/"bool"/"json"/"string"/"form")

obj / list

Pure value constructors — the record/list templates. Text form: { k: expr } / [ expr ] when not plain JSON.

{"kind": "obj", "fields": {
"ok": {"kind": "lit", "value": true},
"count": {"kind": "var", "name": "n"}
}}
kindfields
objfields — map of field name → pure value node
listitems — ordered pure value nodes

Leaves may only be var/lit/jq/expr/fmt/parse/obj/list; effectful leaves are rejected. As bare top-level statements they error — a value by itself is not executable.

ctx / ctx_append

The context-pack nodes — full budget semantics on Context packs. Text forms: the ctx name block and pack += more.

kindfields
ctxname (string), purpose (string, optional), include/exclude (string[], optional), budget (u64, optional; 0 rejected)
ctx_appendctx (string — the pack to extend), add (string[], optional)

Key invariants

  • Every op goes through the dispatch envelope — policy, approval, and redaction are non-bypassable regardless of which node kind triggers the call.
  • return inside parallel is rejected by the analyzer — bind inside the branch, read the symbol after the join.
  • memo is session-scoped — identical operation-and-argument provenance reuses the cached value; a changed call or a new session recomputes.
  • retry does not retry fatal errors — policy denial, unknown op, and type errors propagate immediately.
  • throttle errors instead of blocking — the plan stays responsive; wrap with try or retry to wait.
  • debounce coalesces per name across turns — the settling window lives in the session store.
  • race picks the first success within its deadline; all-failed is a joined error distinct from a timeout, and losing branches' dispatched steps remain counted and traced.
  • await and checkpoint are top-level only — they need stable resume cursors.
  • obj/list are pure templates — they cannot contain call or control-flow leaves.