> Source: https://builder.ema.ai/v2/core-concepts/conditions-and-expressions
> Title: Conditions and Expressions

# 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:

```json
{
  "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.

> [TIP]
> **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.

```json
{
  "type": "field",
  "field": "output.priority",
  "operator": "eq",
  "value": "HIGH"
}
```

### Fixed condition

A constant boolean — used for an edge that is always or never taken.

```json
{ "type": "fixed", "value": true }
```

### Group condition

Combines child conditions with an `all` (AND) or `any` (OR) combinator. Groups nest, and evaluation short-circuits.

```json
{
  "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.** `eq` on `"HIGH"` matches `"high"`.
-   **Numbers coerce.** Integer and floating-point values compare consistently, so `gt` with `5` works whether the field is `5.0` or `5`.
-   **Existence vs. equality.** Use `exists` / `not_exists` to test presence; use `eq` to test a value. `not_exists` is 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_of` operators take an array in `value`, 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](/builder/v2/core-concepts/workflows#runs-and-steps).

## A worked branching example

Route high-priority tickets from US or Canada to escalation, and everything else to auto-resolve.

```text
                    ┌─[ 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](/builder/v2/core-concepts/workflows) — edges, branching, and the run trace.
-   [Agents](/builder/v2/core-concepts/agents) — producing the outputs you branch on (for example, intent classification).
-   [Human in the Loop](/builder/v2/core-concepts/human-in-the-loop) — `{{...}}` references are re-resolved when a paused run resumes.
