Skip to content

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 hadron CLI 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:ops and memory acme.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:

hadron edge create -m acme.com::ops \
  --from flow:classify --to flow:defer \
  --name next.2.low

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:

hadron run get run_123 --json

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 is flow:classify with edgeOut: "next.1.high" (the edge it took); the second is flow:act with edgeOut: null (no edge fired — the flow ended). On a tool hop, the element carries toolCalls: [{ "name": "hadron_create_node", "ok": true }] and any logs.
  • 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_node so 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 an extractionSpec node does that — so branch on extracted fields, and use reads for in-turn context.)
  • Mid-flow outputs — give an interior node a data.output spec 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.