Use this when the routing decision is a rule, not a judgement: you already
have the answer in a context variable or an upstream node's output, and you
want to fork the pipeline accordingly. condition nodes evaluate a JS
expression against the run context and pick a branch. enum validators on
script or AI nodes do the same thing one level lower, by parsing the node's
own output into a routing key.
This page covers both, plus when to choose which.
Branching on a context variable with condition#
The simplest form: a single condition node forks on whether
variables.tier == 'vip'.
name: ticket-router
version: 1
description: Route a support ticket to an account manager or the general queue based on account tier.
initial: route
states:
route:
type: condition
label: Is this a VIP account?
expression: "variables.tier == 'vip'"
on:
'true': escalate
'false': queue
escalate:
type: script
label: Escalate to account manager
script: |
import { $ } from 'bun';
const res = await $`./scripts/notify-manager.sh`.nothrow();
export default res.exitCode === 0;
validator:
kind: boolean
on:
'true': done
'false': failed
queue:
type: script
label: Add to general support queue
script: |
import { $ } from 'bun';
const res = await $`./scripts/add-to-queue.sh`.nothrow();
export default res.exitCode === 0;
validator:
kind: boolean
on:
'true': done
'false': failed
done:
type: end
label: Done
outcome: success
failed:
type: end
label: Failed
outcome: failureRun it with the variable set:
roscoe run ticket-router --var tier=vip # → escalate
roscoe run ticket-router --var tier=standard # → queuecondition always exposes exactly two transitions, 'true' and 'false'.
Both are required by the integrity validator. Leaving one out fails
validation.
Branching on an upstream output with enum#
When the value to branch on is something an earlier script or AI node
produced, you don't need a separate condition node. Put an enum
validator on the producing node and route directly.
name: severity-router
version: 1
description: Classify a support ticket then route by severity.
initial: classify
inputs:
- name: ticketText
type: string
description: The customer message to classify.
states:
classify:
type: ai_judge
label: Classify severity
model: claude-haiku-4-5
prompt: |
Classify the severity of this support ticket. Answer with the JSON
shape requested below; the result must be exactly one of: urgent,
standard, low.
Ticket: {{ ticketText }}
validator:
kind: enum
routes: [urgent, standard, low]
maxRetries: 2
on:
urgent: notify-manager
standard: auto-reply
low: log-only
notify-manager:
type: script
label: Page the on-duty manager
script: |
import { $ } from 'bun';
const res = await $`./scripts/notify-manager.sh`.nothrow();
export default res.exitCode === 0;
validator:
kind: boolean
on:
'true': done
'false': done
auto-reply:
type: script
label: Send an acknowledgement reply
script: |
import { $ } from 'bun';
const res = await $`./scripts/send-reply.sh`.nothrow();
export default res.exitCode === 0;
validator:
kind: boolean
on:
'true': done
'false': done
log-only:
type: end
label: Logged
outcome: success
done:
type: end
label: Done
outcome: successticketText is declared under inputs: because the prompt reads it as a bare
{{ ticketText }}. An undeclared bare reference fails validation even when you
plan to supply it with --var; declaring it is what makes the reference
resolve.
enum validators require routes: [] to be non-empty. Each route name must
appear as a key in on. The integrity validator catches missing transitions
when at least one is defined.
Choosing between condition and enum#
| You want… | Use |
|---|---|
Branch on a value already in variables |
condition |
| Branch on a node's own output | enum |
| Branch on a fixed boolean (command succeeded, model said yes) | boolean |
| Branch by confidence threshold | confidence |
condition cannot run script or AI work: it's pure routing. enum /
boolean / confidence validators are properties of script,
ai_judge, and consensus nodes, applied to whatever those nodes produced.
Available fields in the expression#
The expression is evaluated with two variables in scope:
variables— the merged bag of all upstream node outputs and any--var/contextvalues you passed in. Read-only.context— alias for the same bag (kept for forward compatibility).
You can use any side-effect-free JS:
expression: "variables.tier == 'vip' and variables.priority == 'high'"
expression: "Number(variables.score) >= 70"
expression: "variables.errors && variables.errors.length > 0"Functions, require, network access, and timers are not available. The
expression runs in a sandboxed evaluator.
Variations#
For a three-way condition, chain two condition nodes: the first picks
vip-vs-not, the second splits standard from low-priority. Each node only sees
'true' and 'false', so a deeper decision tree means more nodes, not a
wider one.
For a confidence threshold, switch an ai_judge validator to
kind: confidence with threshold: 70. The model returns
{ score: 0..100 } and routes 'true' when score >= threshold, else
'false'. Useful for soft gating without enumerating every case.
To loop until done, route a node's 'false' branch back to itself or to an
upstream node, and set maxIterations on the node the back-edge targets
(required on any automated cycle). See
/docs/recipes/closed-loop-feedback
for the cap and stall-detection rules that apply.
See also#
- condition node — full reference for the node type and its expression language
- ai_judge node — validator kinds (
boolean,enum,confidence) in detail - Human approval flow — when the answer is human, not rule-based