Multi-node run flows¶
Reference for the fields and rules that govern a multi-node headless run — a run that walks a graph of prompt nodes connected by routing edges. For the mental model, read How multi-node automations work; for a worked build, Build a branching automation. Triggers (manual / schedule / webhook) are documented under the CLI's Headless runs.
Routing-edge selection¶
After a node runs, the walker collects that node's outgoing edges, keeps only the routing edges, sorts them, and takes the first whose condition fires.
Which edges route¶
An edge routes if its routing name is exactly next or begins with
next.. The routing name is the edge's explicit name, or — if unnamed —
the first :-delimited segment of its derived loc after the source-loc
prefix. Every non-next* edge (documents, depends-on, abstract-of,
…) is ignored by the walker.
Reference (non-routing) edges whose target has content are inlined into the running node's prompt; routing edges are not inlined — the next step's prompt never appears in the current step.
Selection order¶
Eligible routing edges are sorted by three keys, in order:
| Key | Direction | Notes |
|---|---|---|
Edge.priority |
ascending — lower fires first | The schema-wide priority convention. |
numeric <index> from next.<index> |
ascending, numeric | next.2 sorts before next.10 (not lexical). A next with no index, or a non-numeric index, sorts last. |
loc |
lexical | Final tiebreak. |
The walker evaluates the sorted edges in order and takes the first whose condition is true. A routing edge with no condition always fires — put it last in the order to make it the fall-through. If no routing edge fires, the run completes (see How flows end).
Conditions use the same JSONLogic grammar and operators as chatbot edges —
see Edge conditions. An unevaluable condition
does not silently fall through; it fails the run with
EDGE_CONDITION_ERROR.
The message.data.* scope in headless runs¶
Conditions and templates in a headless run read a single merged view under
message.data.*, composed of two sources:
| Source | What it is |
|---|---|
| Trigger payload | The run's input — webhook args, or a schedule's eventData. |
| Run envelope | Structured fields extracted by nodes so far (see extractionSpec), accumulated on the run. |
On a key collision the envelope wins — a fact the flow extracted
overrides a hint the trigger supplied. Keys beginning with __ are
stripped from the view (internal plumbing never reaches conditions or
templates). The stored envelope keys are bare (confidence), but they are
read at the message.data. prefix: a condition references
message.data.confidence, a template renders {{confidence}}.
This extends the chatbot variable scopes documented in
Edge conditions → variable scopes:
in a headless run, message.data.* is the trigger-payload-plus-envelope
view rather than a single chat turn's extraction.
data.extractionSpec¶
A field on a flow node declaring the structured fields to pull from the model's response. Shape:
{
"extractionSpec": [
{ "field": "intent", "description": "what the report is about" },
{ "field": "confidence", "shape": "number" }
]
}
| Key | Required | Meaning |
|---|---|---|
field |
Yes | The envelope key to write. |
description |
No | Guides the model on what to extract. |
shape |
No | The value's type. |
Shape vocabulary (v1):
shape |
Extracted as |
|---|---|
number |
number |
boolean |
boolean |
string[] |
array of strings |
| (anything else, or absent) | string |
Fields are best-effort — none is required, so an extraction may come
back with a field missing if the model didn't find it. Conditions
downstream should tolerate an absent field (test with missing).
Reserved field names, rejected at load time (NODE_LOAD_FAILED):
exceptionandexceptions(and their.sub/[index]paths) — reserved for a future agent-exceptions surface. Same reservation as in Edge conditions → reserved names.- Any name beginning with
__— reserved for runner plumbing.
A node with an extractionSpec cannot also declare
tools — the two are mutually exclusive per node.
data.tools¶
A field listing the tools a flow node may call. A node with tools runs a
bounded tool loop instead of a single text completion.
v1 vocabulary:
| Tool | Cost | Purpose |
|---|---|---|
hadron_read_node |
0 | Read a node the run may access. |
hadron_create_node |
1 | Create a node. |
hadron_update_node |
1 | Update a node (merges data shallowly). |
hadron_log (always on) |
0 | Append a line to the hop's logs. |
hadron_fail (always on) |
0 | Author's deliberate stop → AUTHOR_FAIL. |
hadron_log and hadron_fail are injected automatically; you don't list
them. An unknown tool name fails the node at load (NODE_LOAD_FAILED)
before any model call.
Write-tool arguments. The run-tool write surface is its own schema —
it overlaps the MCP node tools but is not identical, so
follow this list rather than the MCP one. The accepted arguments are
urn, name, content, nodeType (default record), data, seq,
tags, description, abstract (≤ 2000 chars), properties, and
isRunnable. hadron_create_node requires urn + name;
hadron_update_node requires urn.
Two differences from the MCP tools of the same name worth calling out:
- Node addressing is by
urn(a fully-qualifiedorg::memory::loc), not the MCP tools'loc+memoryId. hadron_update_nodemergesdatashallowly — the patch's top-level keys win, unmentioned keys survive. (The MCPhadron_update_nodereplaces the wholedatabag; the shallow-merge behavior there lives in the separatehadron_update_node_datatool.)
Access constraints (each call is authorized when it happens):
- Same organization only — a tool cannot reach another org's memory.
- Encrypted memories are denied to run tools.
- Personal / private memories are unreachable unless the run acts on
behalf of a user (
--as-self); an App-identity run has no user and can't pass the class gate.
Loop and cost:
- Each call is a
tool.<name>action; writes cost 1 action, reads and logs cost 0. A denied call fails the runACTIVATION_DENIEDmid-hop. - A hop runs at most
RUN_MAX_TOOL_STEPS_PER_HOPtool steps (default 8). Exceeding the cap isn't an error — the loop ends and the last text stands. - The tool loop runs on the Anthropic, OpenAI, and Bedrock providers. A
provider without verified tool support fails the hop
NO_TOOL_SUPPORT.
A side-effecting tool call is never retried: any hop that has run one is terminal on failure, so a re-delivered run cannot repeat a side effect.
data.output¶
Controls where a node writes its result mid-flow.
| Key | Default | Meaning |
|---|---|---|
writeTo |
runs:<runId> |
Target node loc, in the flow node's memory. Created if it doesn't exist (name from the last loc segment); updated in place if it does. |
mode |
replace |
replace overwrites the target's content; append folds the new content on with a \n\n---\n\n separator. |
Write rules:
- A node writes mid-flow only if it declares
data.output. - The last node to run (the one where no routing edge fires)
always writes — with no
outputspec it writes the default run-record atruns:<runId>beside itself, exactly as a single-node run does. - Written nodes are
nodeType: record,createdBy: run:<runId>. writeTointo an encrypted or cross-org memory is denied.
Budgets, limits, and failure codes¶
Defaults, each overridable by the named env var (a value ≤ 0 falls back to the default):
| Limit | Env var | Default |
|---|---|---|
| Token budget (whole run) | RUN_DEFAULT_TOKEN_BUDGET |
100,000 |
| Action budget (whole run) | RUN_DEFAULT_ACTION_BUDGET |
50 |
| Run timeout (per attempt) | RUN_DEFAULT_TIMEOUT_MS |
120,000 ms |
| Max hops | RUN_MAX_HOPS |
50 |
| Tool steps per hop | RUN_MAX_TOOL_STEPS_PER_HOP |
8 |
Budget accounting:
- The first hop's LLM call is free (covered at run mint); every additional hop costs 1 action.
- An output write costs 1 action; each write-tool call costs
1. So one interior hop that runs the model and writes output can
spend 2 actions. The action budget (default 50) is the real ceiling;
RUN_MAX_HOPSguards against zero-LLM cycles. - Each LLM call is capped at
min(remaining token budget, 32,000)tokens; the run's token budget decrements by actual spend (tokensIn + tokensOut) per hop.
Because the timeout defaults to the message-bus ack window (120 s), keep a flow within it — roughly 3–10 hops in v1. Don't raise the run timeout above the bus ack window.
Failure codes (on AppRun.failure.code):
| Code | Cause |
|---|---|
EDGE_CONDITION_ERROR |
A routing edge's condition was unevaluable. The run never guesses a branch. |
NODE_LOAD_FAILED |
A node's spec was invalid (unknown tool, reserved extraction field, tools+extractionSpec together, …). |
MAX_HOPS_EXCEEDED |
The walk exceeded RUN_MAX_HOPS. |
ACTIVATION_DENIED |
An action or tool call wasn't permitted by the run's policy chain / budget. |
AUTHOR_FAIL |
A node called hadron_fail — a deliberate author stop. |
TOOL_FAILED |
A tool handler threw. Terminal — never retried. |
NO_TOOL_SUPPORT |
The run's provider doesn't support the tool loop. |
OUTPUT_WRITE_FAILED |
The result write failed (e.g. denied target memory). |
LLM_FAILED |
The model call failed non-transiently. |
TIMEOUT |
The run exceeded its timeout (status TIMED_OUT). |
Terminal run statuses: COMPLETED, FAILED, CANCELLED, TIMED_OUT.
The run record¶
A run carries its live position and a per-hop trail, both visible mid-run
(via hadron run get <id> — see Headless runs).
curNodeUrn— the checkpoint: the next node to execute while the run is live; at completion it holds the terminal result reference. Cancel a run mid-walk and this points at the hop that would have run next.data— the accumulated envelope (extracted fields).hops— the trail; one element per completed hop:
{
"node": "org::mem::flow:classify", // node executed
"edgeOut": "next.1.high", // routing edge taken; null on the last hop
"startedAt": 1720000000000, // ms epoch
"finishedAt": 1720000000500,
"tokensSpent": 250, // tokensIn + tokensOut this hop
"toolCalls": [ // present only on tool hops
{ "name": "hadron_create_node", "ok": true, "ms": 42 }
],
"logs": ["task node created"] // present only when hadron_log was called
}
toolCalls and logs are omitted on hops that produce none. Budgets
(budgetTokens, budgetActions), status, startedAt, finishedAt,
attempts, and failure round out the record.
Related¶
- How multi-node automations work — the mental model behind these fields.
- Build a branching automation — a worked classify-then-branch flow.
- Edge conditions — the JSONLogic grammar, operators, and scopes conditions share with chatbot edges.
- Headless runs (CLI) — triggering, auditing, and cancelling runs.