Build a branching automation¶
Goal: a headless flow that classifies an incoming report, then branches on how confident the classification was. High confidence routes to an act node that creates a task; low confidence falls through to a defer node that files it for human review.
flow:classify ──next.1.high (confidence > 0.8)──▶ flow:act (creates a task node)
│
└────────next.2.low (no condition)────────▶ flow:defer (files for review)
This mirrors the flow in hadron-server's appRunFlow.test.ts, the
faithful source for every step. For the mental model behind graphs,
routing edges, and the envelope, read
How multi-node automations work;
for the field-level rules, Multi-node run flows.
Conditions are JSON today
A portal condition builder for headless flows is a follow-up. Until it ships, you author routing conditions as JSONLogic JSON, shown inline below. The rules are the same ones the chatbot condition builder emits — see Edge conditions.
Prerequisites¶
- The
hadronCLI installed and authenticated. - An App in your org to run under, and a memory the App can read
and write — this guide uses App
acme.com:opsand memoryacme.com::ops. See Building an agent for the App/memory setup. - An LLM provider configured for the App (or pass
--ai-config <name>at trigger time).
Step 1: Create the classify node¶
The entry node runs first. It carries an extraction spec that pulls two
fields out of the model's response — an intent string and a numeric
confidence — into the run envelope. Its prompt renders {{topic}} from
the trigger payload.
hadron node create -m acme.com::ops \
--loc flow:classify \
--name "Classify report" \
--content 'Classify the report about {{topic}}. Score your confidence from 0 to 1.' \
--data '{"extractionSpec":[
{"field":"intent","description":"what the report is about"},
{"field":"confidence","shape":"number"}
]}'
shape: "number" makes confidence a number the condition can compare;
without it, extracted values default to strings. Fields are best-effort —
design the branch to tolerate a missing one.
Step 2: Create the act and defer nodes¶
The act node handles the high-confidence branch. It declares a
tool so it can create a task node rather than just emit text (a node
that declares tools cannot also carry an extractionSpec):
hadron node create -m acme.com::ops \
--loc flow:act \
--name "Act on report" \
--content 'The {{intent}} report about {{topic}} is urgent. Create a task node at loc tasks:investigate-{{topic}} titled "Investigate {{topic}}" describing the first thing to check.' \
--data '{"tools":["hadron_create_node"]}'
The defer node handles the fall-through branch — plain text, no tools:
hadron node create -m acme.com::ops \
--loc flow:defer \
--name "Defer report" \
--content 'Save the {{topic}} report for human review. Summarize why it was low-confidence.'
Step 3: Wire the routing edges¶
Two routing edges leave flow:classify. Edge names decide that they
route (next*); among edges of equal priority, the numeric index
orders them — next.1 before next.2. These edges use the default
priority, so the index governs here; if you set explicit priority values,
those sort first (see
routing-edge selection).
The high-confidence edge carries a JSONLogic condition reading the
extracted score from message.data:
hadron edge create -m acme.com::ops \
--from flow:classify --to flow:act \
--name next.1.high \
--condition '{">":[{"var":"message.data.confidence"},0.8]}'
The low-confidence edge has no condition, so it always fires — the
fall-through. Because it's next.2, it's only reached after the next.1
condition is evaluated and found false:
That's the whole branch: the walker sorts the two edges (next.1 before
next.2), takes next.1.high when confidence > 0.8, and otherwise falls
through to next.2.low.
Attach a playbook without routing into it
To give the act node reference material, add a non-routing edge —
e.g. --name documents — from flow:act to a playbook node. Its
content is inlined into the act node's prompt, but the walker never
treats it as a step. Only next* edges route.
Step 4: Trigger the run¶
Trigger the flow at its entry node, passing topic as the trigger
payload. --wait blocks until the run reaches a terminal status and exits
non-zero if it didn't complete:
hadron run trigger --app acme.com:ops \
--entry acme.com::ops::flow:classify \
--arg topic=outage \
--wait --json
The command prints the run id (e.g. run_123). A schedule or webhook
pointed at the same entry node runs the identical walk — see
Headless runs.
Step 5: Read the result¶
Inspect the run to see the path it walked:
Look for:
data— the envelope, e.g.{ "intent": "alert", "confidence": 0.92 }. These are the extracted fields the branch read.hops— the trail. The first element isflow:classifywithedgeOut: "next.1.high"(the edge it took); the second isflow:actwithedgeOut: null(no edge fired — the flow ended). On a tool hop, the element carriestoolCalls: [{ "name": "hadron_create_node", "ok": true }]and anylogs.curNodeUrn— the checkpoint. At completion it references the result record.
Then confirm the two writes the flow made:
# The task node the act node created:
hadron node get acme.com::ops::tasks:investigate-outage
# The run's default result record (written by the last node):
hadron node ls -m acme.com::ops --prefix runs:
Trigger it again with a topic your model scores below 0.8 (or lower the
threshold to test) and the run walks flow:classify → next.2.low →
flow:defer instead — no task created, the report filed for review.
Common issues¶
| Symptom | What to check |
|---|---|
Run fails EDGE_CONDITION_ERROR. |
The condition references something unevaluable (a misspelled scope, malformed JSON). The walker never guesses a branch — fix the JSONLogic. |
| High branch never fires even on urgent input. | confidence came back as a string (no shape: "number" on the extraction field), so > 0.8 compares wrong. Add the shape and re-run. |
Run fails NODE_LOAD_FAILED on the act node. |
A node can't declare both tools and extractionSpec. Split classification and action into separate nodes (as here). |
Run fails ACTIVATION_DENIED at the act node. |
The App's policy chain doesn't grant tool.hadron_create_node, or the action budget is exhausted. Grant the action / raise the budget. |
| Task node created in the wrong memory, or denied. | Run tools are same-org only and can't reach encrypted or (without --as-self) personal memories. Target a memory the run can write. |
| Both branches skipped; run completes with no action. | Neither routing edge fired — the next.1 condition was false and there's no fall-through. Add a condition-less next.2 edge as the default. |
What you can build from here¶
The classify-then-branch shape generalizes:
- Multi-way triage — several
next.<n>.<label>edges with mutually exclusive conditions and a final condition-less fall-through. - Consult while acting — give an action node
hadron_read_nodeso it can read other nodes during its own turn, shaping the text it writes or the node it creates. (Reads don't populate the envelope — only anextractionSpecnode does that — so branch on extracted fields, and use reads for in-turn context.) - Mid-flow outputs — give an interior node a
data.outputspec to write a record partway through, not just at the end.
Each stays in the same model: prompt nodes, next* routing edges with
JSONLogic conditions, an accumulating envelope. See
Multi-node run flows for every field.
Related¶
- How multi-node automations work — the mental model this how-to assumes.
- Multi-node run flows — the field-level
reference for
extractionSpec,tools,output, and the run record. - Edge conditions — the JSONLogic conditions the routing edges use.
- Headless runs (CLI) — scheduling or webhook-triggering the same flow.