Skip to main content

Conditional Branching in Orchestrations

This tutorial shows how to build a branching orchestration using condition nodes. When a run completes, every node that was not reached is recorded with status: "skipped" — giving you a complete execution trace regardless of which path ran.

You will:

  1. Create a project.
  2. Define an orchestration with a condition node that routes to one of two transform branches based on run input.
  3. Start two runs — one for each branch — and inspect the node_executions to confirm which nodes ran and which were skipped.

No AI provider is required — this tutorial uses only condition and transform nodes.

Prerequisites

export SOAT_BASE_URL=http://localhost:5047

Step 1 — Log in as admin

See Users for authentication details.

ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN

Step 2 — Create a project

All orchestrations belong to a project.

PROJECT_ID=$(soat create-project --name "triage-demo" | jq -r '.id')
echo "Project: $PROJECT_ID"

Step 3 — Create the orchestration

The graph has three nodes:

NodeTypePurpose
routeconditionEvaluates urgent from input state and emits "alert" or "queue"
send_alerttransformRuns only on the alert branch
queue_tasktransformRuns only on the queue branch

Edges carry condition: "<label>" to select which branch the engine traverses after route completes. A node whose only incoming edge is not traversed is recorded as skipped when the run finishes. See Orchestrations — Node Types for the full condition node reference.

ORCH_ID=$(soat create-orchestration \
--project-id "$PROJECT_ID" \
--name "content-triage" \
--nodes '[
{
"id": "route",
"type": "condition",
"expression": {"if": [{"var": "urgent"}, "alert", "queue"]}
},
{
"id": "send_alert",
"type": "transform",
"expression": {"cat": ["ALERT: ", {"var": "topic"}]},
"output_mapping": {"result": "state.result"}
},
{
"id": "queue_task",
"type": "transform",
"expression": {"cat": ["QUEUED: ", {"var": "topic"}]},
"output_mapping": {"result": "state.result"}
}
]' \
--edges '[
{"from": "route", "to": "send_alert", "condition": "alert"},
{"from": "route", "to": "queue_task", "condition": "queue"}
]' | jq -r '.id')
echo "Orchestration: $ORCH_ID"

Step 4 — Run the alert branch

Pass urgent: true. The engine evaluates the route condition, emits "alert", traverses only the send_alert edge, and records queue_task as skipped. See Orchestrations — Node Executions for the full execution trace schema.

ALERT_RUN=$(soat start-orchestration-run \
--orchestration-id "$ORCH_ID" \
--input '{"urgent": true, "topic": "Server is down"}')

echo "$ALERT_RUN" | jq '{status, result: .output, executions: .node_executions | map({node_id, status})}'

Expected output:

{
"status": "succeeded",
"result": null,
"executions": [
{ "node_id": "route", "status": "completed" },
{ "node_id": "send_alert", "status": "completed" },
{ "node_id": "queue_task", "status": "skipped" }
]
}

The queue_task node appears in the trace with status: "skipped", output: null, and started_at: null — confirming it was never executed.


Step 5 — Run the queue branch

Pass urgent: false. Now route emits "queue", send_alert is skipped, and queue_task runs.

QUEUE_RUN=$(soat start-orchestration-run \
--orchestration-id "$ORCH_ID" \
--input '{"urgent": false, "topic": "Update documentation"}')

echo "$QUEUE_RUN" | jq '.node_executions | map({node_id, status})'

Expected output:

[
{ "node_id": "route", "status": "completed" },
{ "node_id": "send_alert", "status": "skipped" },
{ "node_id": "queue_task", "status": "completed" }
]

What you built

A branching orchestration where a single condition node decides which downstream path executes:

input
└─▶ route (condition)
├─▶ send_alert (condition: "alert") ← runs when urgent = true
└─▶ queue_task (condition: "queue") ← runs when urgent = false

Key takeaways:

  • A condition node evaluates a JSON Logic expression and emits a string label. Downstream edges carry condition: "<label>" to match that label.
  • Nodes whose incoming edges are never traversed are recorded as skipped once the run completes — every declared node is always visible in the node_executions trace.
  • The branching logic lives in the graph definition, not inside each individual node. Nodes stay single-responsibility.

To apply this pattern to a real pipeline, replace the transform nodes with agent nodes pointing at specialized agents for each branch. See Orchestrate a Sonnet and Multi-Agent Orchestration for examples that wire agents into an orchestration graph.