Skip to content

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):

  • exception and exceptions (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.

{ "tools": ["hadron_create_node"] }

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-qualified org::memory::loc), not the MCP tools' loc + memoryId.
  • hadron_update_node merges data shallowly — the patch's top-level keys win, unmentioned keys survive. (The MCP hadron_update_node replaces the whole data bag; the shallow-merge behavior there lives in the separate hadron_update_node_data tool.)

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 run ACTIVATION_DENIED mid-hop.
  • A hop runs at most RUN_MAX_TOOL_STEPS_PER_HOP tool 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.

{ "output": { "writeTo": "flow:alerts", "mode": "append" } }
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 output spec it writes the default run-record at runs:<runId> beside itself, exactly as a single-node run does.
  • Written nodes are nodeType: record, createdBy: run:<runId>.
  • writeTo into 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_HOPS guards 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.