# Conditional branching

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'`.

```yaml
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:

```bash
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.

```yaml
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:

```yaml
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](/docs/recipes/closed-loop-feedback)
for the cap and stall-detection rules that apply.

## See also

- [condition node](/docs/nodes/condition) — full reference for the node
  type and its expression language
- [ai_judge node](/docs/nodes/ai-judge) — validator kinds (`boolean`, `enum`,
  `confidence`) in detail
- [Human approval flow](/docs/recipes/approval-flow) — when the answer is
  human, not rule-based
