Condition expressions

expr-eval expressions for branching on context variables.

A condition node evaluates a small expression against the current variables map and routes to 'true' or 'false' based on the result. Use it to branch on data already in the run, such as an upstream output, an initial context value, or a derived value, without paying for an AI call. This page covers the expression language, what is in scope, and the pitfalls authors hit most often.

The shape of a condition node#

check-env:
  type: condition
  label: Production?
  expression: env == 'production'
  on:
    'true': deploy
    'false': stage

Every condition node has:

  • expression — the expression to evaluate.
  • on — must declare exactly 'true' and 'false' keys.

The result of the expression is coerced to boolean: any truthy value routes to 'true', any falsy value (false, 0, null, undefined, empty string) routes to 'false'.

What is in scope#

Expressions are evaluated against the workflow's variables map, with each top-level key in scope as a bare identifier. So if the run has variables.env === 'production' and variables.score === 92, you can write:

expression: env == 'production' and score >= 80

You cannot access process, require, or any Node.js global. Expressions run in a sandboxed evaluator, not in vm.runInNewContext. This is intentional: condition expressions are part of the workflow file, which may be authored or edited by less-trusted humans or models.

Operators and syntax#

The evaluator (built on expr-eval) supports a useful subset of JavaScript- like syntax, but not the full language:

Category Supported syntax
Arithmetic +, -, *, /, %, ^ (power)
Comparison ==, !=, <, <=, >, >= (not === / !==)
Logic and / or / not, or equivalently && / || / !
Member access foo.bar and foo[0] for object/array fields
String literals Single or double quoted
Numeric literals Integers and floats
Function calls A small built-in set (length, min, max, abs, etc.); custom functions are not exposed

What you cannot do:

  • Define variables (let, const, var).
  • Call methods on objects (foo.toUpperCase() does not work).
  • Use the spread operator, destructuring, or templates.
  • Access globals like Math, JSON, Date.

If you need something more expressive, run a script node and branch on the value it returns, or call an ai_judge.

Worked examples#

Branch on an initial context variable#

name: env-branch
version: 1
initial: pick
states:
  pick:
    type: condition
    label: Is this production?
    expression: env == 'production'
    on:
      'true': prod
      'false': stage
  prod:
    type: end
    label: Production path
    outcome: success
  stage:
    type: end
    label: Staging path
    outcome: success

Run with roscoe run env-branch --var env=production.

Branch on an upstream score#

score-gate:
  type: condition
  label: Is the score high enough?
  expression: judge.score >= 80
  on:
    'true': publish
    'false': revise

This assumes an upstream ai_judge node with id judge ran first using a confidence validator (which writes judge.score). See Output chaining for the field reference.

Compound conditions#

gate:
  type: condition
  label: Production AND high confidence?
  expression: env == 'production' and judge.score >= 90
  on:
    'true': deploy
    'false': hold

Empty-list check#

any-failures:
  type: condition
  label: Any failures?
  expression: length(failures) == 0
  on:
    'true': pass
    'false': fail

length(...) is one of the built-in functions and works on strings and arrays.

Pitfalls#

Undefined variables#

If you reference a variable that has not been set yet, typically because the upstream node has not run or did not produce that field, the identifier evaluates to undefined. Most comparisons against undefined produce false, which silently routes to 'false'. This can make a bug look like an intentional path.

Fix: make sure the condition node runs after the producer. Look at the run trace in the web UI to confirm the field is in variables at the moment the condition fires.

String vs number comparisons#

Variables seeded from the CLI (--var key=value) are always strings.

roscoe run my-flow --var threshold=80
# This compares the string "80" to the number 80 — false!
expression: threshold == 80

Fix: quote the literal, or pass the variable through a producing node that emits a number.

expression: threshold == '80'

For numeric comparisons, prefer pulling the value from a node that produces typed output (e.g. ai_judge with confidence validator → judge.score is a real number).

Boolean variables#

Same caveat: --var debug=true gives you the string "true", not the boolean. Use expression: debug == 'true'.

Accidental assignment#

A single = parses as expr-eval's assignment operator, not equality: x = 5 sets x to 5 and returns 5 (truthy), so the condition silently routes to 'true' instead of failing to parse. The assignment itself doesn't stick around: Roscoe rebuilds variables from node outputs after every step, so nothing downstream sees the change. The routing bug is real though, and easy to miss because the expression evaluates without an error. Always use == for equality.

Quoting in YAML#

YAML interprets some unquoted strings specially. If your expression starts with { or contains :, wrap it in single quotes or use a block scalar:

expression: 'env == "production"'
# or
expression: |
  env == "production"

What the node writes back#

A condition node writes <id>.result to variables: the boolean it routed on. You can reference this downstream:

log-decision:
  type: script
  label: Log decision
  script: |
    console.log(`Gate decided: ${process.env.ROSCOE_OUT_check_env_result}`);
    export default true;
  validator:
    kind: boolean
  on:
    'true': done
    'false': done

Where to next#

  • Condition node — the per-field reference.
  • Output chaining — what fields each upstream node writes for you to reference here.
  • Validators — when an AI judge is a better fit than a condition.

View this page as Markdown

Predictable workflows from unpredictable AI