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': stageEvery 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 >= 80You 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: successRun 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': reviseThis 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': holdEmpty-list check#
any-failures:
type: condition
label: Any failures?
expression: length(failures) == 0
on:
'true': pass
'false': faillength(...) 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 == 80Fix: 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': doneWhere 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.