Skip to content

Build a branching automation

PortalMCPCLIAPIAdvanced~25 min

You can have Hadron sort incoming reports for you: a model reads each one, and if it is confident the report is urgent, a task is created on the spot; if it is not sure, the report is filed for a person to review.

This is a headless flow of three nodes: one classifies, then the flow 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.

Three ways to build it

The steps below use the CLI, because every value is visible in one place. The same flow can be built without a terminal:

  • In the portal. Open a node and go to its Flow tab: Extracted fields (with Add field, a Type and a Description) sets up the classify step. Open flow canvas draws the nodes and edges; selecting an edge opens Edit edge, with Relationship name, Priority and a visual Condition builder. A node's tool list has no form yet — enter data.tools as JSON on its Data tab. Run it from the node's ⋯ menu (Convert to task, then Run).
  • Through an agent. An agent connected to Hadron can build and start the flow: hadron_create_task for nodes that carry data, hadron_create_edge then hadron_update_edge to add a condition (hadron_create_edge takes none), and hadron_run_task to start the run. hadron_run_task needs the entry node's full urn and an appRef — without appRef it only hands the task back for the agent to carry out itself, and no run starts. An agent cannot yet read the run's result (hadron-server#1561); check it on the run's page in the portal, or with the GraphQL query in Step 5.

Either way, routing conditions are JSONLogic — the same rules the portal's condition builder writes. See Edge conditions.

Prerequisites

  • For the steps below: the hadron CLI installed and authenticated. (The portal and agent paths above don't need it.)
  • 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 urn hrn:node:acme.com:ops: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 hrn:node:acme.com:ops:flow:classify \
  --arg topic=outage \
  --wait --json

The command prints the run, including its id — a 32-character hex string. 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 (in the portal, the run's page shows the same fields):

hadron run get <run-id> --json

Look for:

  • data — the envelope, e.g. { "intent": "alert", "confidence": 0.92 }. These are the extracted fields the branch read.
  • curNodeUrn — the checkpoint. At completion it references the result record.

The path it walked — which edge each node took — is the run's hops trail. Neither hadron run get nor the portal shows it; ask GraphQL:

hadron api 'query($ref: ID!) { appRun(ref: $ref) { status hops } }' -F ref=<run-id>

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). A tool hop also carries toolCalls (e.g. { "name": "hadron_create_node", "ok": true }) and any logs.

Then confirm the two writes the flow made:

# The task node the act node created:
hadron node get hrn:node: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
Creating the edge fails INVALID_EDGE_CONDITION, or the CLI rejects the JSON. The condition is malformed or names a scope that doesn't exist. Both are caught when you save the edge, not at run time.
Run fails EDGE_CONDITION_ERROR. A saved condition could not be evaluated during the run. The walker never guesses a branch. (Encrypted memories fail earlier: an encrypted entry memory is rejected at launch with ENCRYPTED_MEMORY_UNSUPPORTED, and a downstream node in an encrypted memory fails with NODE_LOAD_FAILED. Headless runs can't use encrypted memories.)
High branch never fires even on urgent input. confidence came back as text the comparison can't read as a number ("high" rather than 0.92), so > 0.8 is silently false. Add shape: "number" to the extraction field 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.