Conditional Branching in Orchestrations
A branching orchestration with condition nodes; unreached nodes are recorded with status: "skipped". You route a condition node to one of two transform branches, run both, and list each run's node executions. No AI provider is required.
Prerequisites
- SOAT running locally (Quick Start); Key Concepts; Orchestrations module; Configuration.
- CLI or SDK; server at
http://localhost:5047.
- CLI
- SDK
- curl
export SOAT_BASE_URL=http://localhost:5047
import { SoatClient } from '@soat/sdk';
export SOAT_BASE_URL=http://localhost:5047
Step 1 — Log in as admin
See Users for authentication.
- CLI
- SDK
- curl
ADMIN_TOKEN=$(soat login-user --username admin --password Admin1234! | jq -r '.token')
export SOAT_TOKEN=$ADMIN_TOKEN
const soat = new SoatClient({ baseUrl: 'http://localhost:5047' });
const { data: login } = await soat.users.loginUser({
body: { username: 'admin', password: 'Admin1234!' },
});
const adminSoat = new SoatClient({
baseUrl: 'http://localhost:5047',
token: login!.token,
});
ADMIN_TOKEN=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"Admin1234!"}' | jq -r '.token')
Step 2 — Create a project
All orchestrations belong to a project.
- CLI
- SDK
- curl
PROJECT_ID=$(soat create-project --name "triage-demo" | jq -r '.id')
echo "Project: $PROJECT_ID"
const { data: project } = await adminSoat.projects.createProject({
body: { name: 'triage-demo' },
});
const PROJECT_ID = project!.id;
PROJECT_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/projects" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"triage-demo"}' | jq -r '.id')
Step 3 — Create the orchestration
The graph has three nodes:
| Node | Type | Purpose |
|---|---|---|
route | condition | Evaluates urgent from input state and emits "alert" or "queue" |
send_alert | transform | Runs only on the alert branch |
queue_task | transform | Runs only on the queue branch |
Edges carry condition: "<label>"; a node whose only incoming edge is not traversed is recorded skipped. See Orchestrations — Node Types.
- CLI
- SDK
- curl
ORCH_ID=$(soat create-orchestration \
--project-id "$PROJECT_ID" \
--name "content-triage" \
--nodes '[
{
"id": "route",
"type": "condition",
"expression": {"if": [{"var": "input.urgent"}, "alert", "queue"]}
},
{
"id": "send_alert",
"type": "transform",
"expression": {"cat": ["ALERT: ", {"var": "input.topic"}]},
"state_mapping":{"state.result":{"var":"output.result"}}
},
{
"id": "queue_task",
"type": "transform",
"expression": {"cat": ["QUEUED: ", {"var": "input.topic"}]},
"state_mapping":{"state.result":{"var":"output.result"}}
}
]' \
--edges '[
{"from": "route", "to": "send_alert", "condition": "alert"},
{"from": "route", "to": "queue_task", "condition": "queue"}
]' | jq -r '.id')
echo "Orchestration: $ORCH_ID"
const { data: orch } = await adminSoat.orchestrations.createOrchestration({
body: {
project_id: PROJECT_ID,
name: 'content-triage',
nodes: [
{
id: 'route',
type: 'condition',
expression: { if: [{ var: 'input.urgent' }, 'alert', 'queue'] },
},
{
id: 'send_alert',
type: 'transform',
expression: { cat: ['ALERT: ', { var: 'input.topic' }] },
state_mapping: { 'state.result': { var: 'output.result' } },
},
{
id: 'queue_task',
type: 'transform',
expression: { cat: ['QUEUED: ', { var: 'input.topic' }] },
state_mapping: { 'state.result': { var: 'output.result' } },
},
],
edges: [
{ from: 'route', to: 'send_alert', condition: 'alert' },
{ from: 'route', to: 'queue_task', condition: 'queue' },
],
},
});
const ORCH_ID = orch!.id;
ORCH_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/orchestrations" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"project_id": "'"$PROJECT_ID"'",
"name": "content-triage",
"nodes": [
{"id":"route","type":"condition","expression":{"if":[{"var":"input.urgent"},"alert","queue"]}},
{"id":"send_alert","type":"transform","expression":{"cat":["ALERT: ",{"var":"input.topic"}]},"state_mapping":{"state.result":{"var":"output.result"}}},
{"id":"queue_task","type":"transform","expression":{"cat":["QUEUED: ",{"var":"input.topic"}]},"state_mapping":{"state.result":{"var":"output.result"}}}
],
"edges": [
{"from":"route","to":"send_alert","condition":"alert"},
{"from":"route","to":"queue_task","condition":"queue"}
]
}' | jq -r '.id')
Step 4 — Run the alert branch
urgent: true makes route emit "alert"; send_alert runs and queue_task is skipped. Trace schema: Orchestrations — Node Executions.
- CLI
- SDK
- curl
ALERT_RUN=$(soat start-orchestration-run \
--orchestration-id "$ORCH_ID" \
--input '{"urgent": true, "topic": "Server is down"}' \
--wait true)
ALERT_RUN_ID=$(echo "$ALERT_RUN" | jq -r '.id')
echo "$ALERT_RUN" | jq '{status, result: .output}'
soat list-orchestration-run-node-executions \
--orchestration-run-id "$ALERT_RUN_ID" | jq '.data | map({node_id, status})'
Expected output:
{
"status": "completed",
"result": null
}
[
{ "node_id": "route", "status": "completed" },
{ "node_id": "send_alert", "status": "completed" },
{ "node_id": "queue_task", "status": "skipped" }
]
const { data: alertRun } = await adminSoat.orchestrations.startOrchestrationRun({
body: {
orchestration_id: ORCH_ID,
input: { urgent: true, topic: 'Server is down' },
wait: true,
},
});
const { data: alertExecutions } =
await adminSoat.orchestrations.listOrchestrationRunNodeExecutions({
path: { orchestration_run_id: alertRun!.id },
});
// send_alert ran; queue_task was skipped
console.log(alertExecutions!.data.map(e => ({ id: e.node_id, status: e.status })));
ALERT_RUN_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/orchestration-runs" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"orchestration_id":"'"$ORCH_ID"'","input":{"urgent":true,"topic":"Server is down"},"wait":true}' \
| jq -r '.id')
curl -s "$SOAT_BASE_URL/api/v1/orchestration-runs/$ALERT_RUN_ID/node-executions" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
| jq '.data | map({node_id, status})'
queue_task shows status: "skipped", output: null, started_at: null.
Step 5 — Run the queue branch
urgent: false makes route emit "queue"; send_alert is skipped and queue_task runs (Node Executions).
- CLI
- SDK
- curl
QUEUE_RUN=$(soat start-orchestration-run \
--orchestration-id "$ORCH_ID" \
--input '{"urgent": false, "topic": "Update documentation"}' \
--wait true)
QUEUE_RUN_ID=$(echo "$QUEUE_RUN" | jq -r '.id')
soat list-orchestration-run-node-executions \
--orchestration-run-id "$QUEUE_RUN_ID" | jq '.data | map({node_id, status})'
Expected output:
[
{ "node_id": "route", "status": "completed" },
{ "node_id": "send_alert", "status": "skipped" },
{ "node_id": "queue_task", "status": "completed" }
]
const { data: queueRun } = await adminSoat.orchestrations.startOrchestrationRun({
body: {
orchestration_id: ORCH_ID,
input: { urgent: false, topic: 'Update documentation' },
wait: true,
},
});
const { data: queueExecutions } =
await adminSoat.orchestrations.listOrchestrationRunNodeExecutions({
path: { orchestration_run_id: queueRun!.id },
});
console.log(queueExecutions!.data.map(e => ({ id: e.node_id, status: e.status })));
QUEUE_RUN_ID=$(curl -s -X POST "$SOAT_BASE_URL/api/v1/orchestration-runs" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"orchestration_id":"'"$ORCH_ID"'","input":{"urgent":false,"topic":"Update documentation"},"wait":true}' \
| jq -r '.id')
curl -s "$SOAT_BASE_URL/api/v1/orchestration-runs/$QUEUE_RUN_ID/node-executions" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
| jq '.data | map({node_id, status})'
Next Steps
Replace the transform nodes with agent nodes per branch: Orchestrate a Sonnet, Multi-Agent Orchestration.