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. Optional affects two different situations.
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.
A reference cannot be resolved at all, because the field it names does not exist in the run input or in the upstream node's output:
- A required input fails the run, reporting the reference it could not resolve.
- An optional input is omitted and the run continues. The node receives no value for that key, and the run is still recorded as successful.
An optional input hides a misspelled reference. Because an unresolvable optional reference is omitted rather than reported, a reference to a field that never existed produces a run that succeeds with data quietly missing. If a node's output looks like it ignored some of its input, compare each reference against the fields the run actually provides. Marking an input required is the quickest way to find out: the run then names the reference it cannot resolve.
The single-field fallback described above applies only when the run input has exactly one field. When a run starts with several fields, such as a run started by an inbound email, every reference must name a field that exists. See Triggering AI Employees for that input's field names.
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"] }
]
}
Sequence-only connections
A step becomes eligible when any one of its incoming connections is satisfied. Incoming connections are combined with OR, never AND. One connection that carries no condition is therefore enough to start the step on every run, and any conditions on the connections beside it stop having an effect.
That is usually a surprise rather than an intent. It shows up as "both branches ran" or "the escalation step fires even when the request was in scope" — the conditions are correct, and out-voted.
Marking a connection Sequence only resolves it. The step still waits for the marked connection's source to finish, but is no longer started by it. Use it for a connection that exists purely to enforce order — the source produces something the step needs, or simply has to happen first — while leaving the conditional connections to decide whether the step runs at all.
Set it in the AI Employee builder: select the connection, open Trigger when, and choose Sequence only. The connection's label changes to Sequence only so the behavior is visible on the diagram. In the DAG, the connection carries "type": "ordering".
A connection cannot be both Sequence only and conditional — a connection that takes no part in routing has nothing for a condition to gate — so choosing one clears the other. Saving a workflow that mixes conditional and unconditional connections into one step reports an advisory warning naming the step and the unconditional sources; the save succeeds, and the workflow keeps over-firing until the wiring is corrected.
Deleting the connection is not the equivalent fix. Run order comes from connections alone, so removing one lets the step start immediately and race the source it used to wait for. If the step reads that source's output through a required input, the run fails with a missing-output error instead. Mark the connection Sequence only rather than deleting it.
Check the steps downstream too. Once conditions take effect, branches that used to run every time start being skipped. A required input whose source was skipped stops its own step from running, so a step that collects from several branches can go quiet. Review required inputs on any step downstream of a branch you have just gated, and make the ones that are genuinely optional optional.
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.