Conditions and Expressions
Two related mechanisms let a workflow move data around and make decisions:
- Expressions —
{{...}}references that pull a value from the run context into a node's input or an agent's Instructions. - Conditions — boolean tests on a workflow edge that decide whether the downstream branch runs.
Both read from the same run context, so once you understand the namespaces, both feel the same.
Passing data with variables
A node declares its inputs with an input_mapping. Each value is either a literal string or a {{...}} reference. References resolve at run time against the run context.
| Reference | Resolves to |
|---|---|
{{workflow_input.field}} | A field from the run's input_params (the input the run started with). |
{{workflow_input.field.subfield}} | A nested field, following the dot path into the input object. |
{{node_id.output}} | The entire output object of an upstream node. |
{{node_id.output.field}} | A field within an upstream node's output, following the dot path. |
For example, mapping a classifier's input to the original message and a responder's input to the classifier's label:
{
"classify": { "input_mapping": { "text": "{{workflow_input.message}}" } },
"respond": { "input_mapping": { "intent": "{{classify.output.intent}}",
"query": "{{workflow_input.message}}" } }
}
A value that doesn't match the {{...}} pattern is treated as a literal — so you can mix constants and references freely.
Chat convenience. When a run's input has exactly one field (the common chat case, {"message": "…"}), a reference like {{workflow_input.query}} that doesn't match falls back to that single value. This keeps chat workflows working when the field name differs.
Optional inputs and skip propagation
An input can be marked optional. This matters when an upstream node was skipped because its branch condition was false:
- A required input whose source node was skipped causes this node to be skipped too. Skips propagate down the branch.
- An optional input whose source node was skipped is simply omitted, and this node still runs.
Use optional inputs when a node can produce a useful result even if one of its data sources didn't run.
You can also reference resolved values inside an agent's Instructions with the same {{...}} syntax, so the model sees the actual upstream data in context.
Conditions on edges
A condition is attached to an edge in the workflow DAG. When the edge's source node completes, the platform evaluates the condition; if it is true (or absent), the target node becomes eligible, and if it is false, that branch is skipped. A node with no incoming condition that survives runs unconditionally.
Edge conditions may only reference the source node's output.* namespace. References to workflow_input.* or other namespaces are rejected when you save the workflow, so a misplaced reference surfaces in the builder rather than silently evaluating to false on every run.
A condition is one of three shapes.
Field condition
Compares a resolved field value against a constant using an operator.
{
"type": "field",
"field": "output.priority",
"operator": "eq",
"value": "HIGH"
}
Fixed condition
A constant boolean — used for an edge that is always or never taken.
{ "type": "fixed", "value": true }
Group condition
Combines child conditions with an all (AND) or any (OR) combinator. Groups nest, and evaluation short-circuits.
{
"type": "group",
"combinator": "all",
"conditions": [
{ "type": "field", "field": "output.priority", "operator": "eq", "value": "HIGH" },
{ "type": "field", "field": "output.region", "operator": "in", "value": ["US", "CA"] }
]
}
Operator catalog
Field conditions use one of the following operators. The operator's behavior depends on the runtime type of the resolved value; an operator applied to the wrong type evaluates to false rather than erroring, because field types aren't known until the value is resolved at run time.
| Category | Operators | Notes |
|---|---|---|
| Equality | eq, neq | Strings compare case-insensitively. |
| String | contains, not_contains, substring_of, contains_any_of, contains_none_of | Case-insensitive substring tests. substring_of checks whether the field is contained in the value. |
| Numeric | gt, gte, lt, lte | Greater/less than comparisons. |
| Membership | in, not_in | Tests whether the field is (not) one of the values in a list. |
| Existence | exists, not_exists | The only operators that ignore the comparison value. exists is true when the field resolves to a non-null value. |
| Array | array_is_empty, array_is_not_empty, array_contains, array_not_contains, array_contains_any_of, array_contains_all_of | Operate on a field whose value is an array. array_is_empty and array_is_not_empty ignore the comparison value. |
A few rules worth knowing:
- String comparisons are case-insensitive.
eqon"HIGH"matches"high". - Numbers coerce. Integer and floating-point values compare consistently, so
gtwith5works whether the field is5.0or5. - Existence vs. equality. Use
exists/not_existsto test presence; useeqto test a value.not_existsis true both when the field is absent and when it resolves to null. - List operators accept a single value too. Membership and
*_any_of/*_all_ofoperators take an array invalue, but a lone scalar is treated as a one-element list, so a condition that saved a single value still evaluates correctly.
Evaluation and traces
Group conditions evaluate with short-circuit semantics: an all group stops at the first false child, and an any group stops at the first true child. An empty all group is vacuously true; an empty any group is false. An unknown combinator evaluates to false rather than guessing, so a typo never silently miscomputes a branch.
When a node completes normally, the platform records one edge-evaluation trace per outgoing edge on that step (edge_evaluations). Each trace mirrors the condition tree and captures the field value seen, the operator, and the result at every level — so the run history shows exactly why each branch was taken or skipped. See Workflows.
A worked branching example
Route high-priority tickets from US or Canada to escalation, and everything else to auto-resolve.
┌─[ all(priority == "HIGH", region in ["US","CA"]) ]─▶ escalate
classify ──────────┤
└─[ fixed: true (fallback) ]──────────────────────────▶ auto_resolve
On a ticket with output.priority = "HIGH" and output.region = "US", the group condition is true and the escalate branch runs. On any other ticket the group is false, that branch is skipped, and the fixed: true fallback edge sends the run to auto_resolve.
Where to go next
- Workflows — edges, branching, and the run trace.
- Agents — producing the outputs you branch on (for example, intent classification).
- Human in the Loop —
{{...}}references are re-resolved when a paused run resumes.