Conditional branching

Route a workflow on context variables or upstream outputs.

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: failure

Run it with the variable set:

roscoe run ticket-router --var tier=vip       # → escalate
roscoe run ticket-router --var tier=standard  # → queue

condition 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: success

ticketText 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 / context values 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#

View this page as Markdown

Predictable workflows from unpredictable AI