Skip to content

Expressions

Expressions appear inside {{ }}.

{{ message.subject }}
{{ classify.category == "sales" }}
{{ message.attachments | length > 0 }}
{{ enrich.crm.lookup.deal_size | default(0) }}
{{ (classify.category == "sales" and enrich.deal_size > 50000)
or (classify.category == "sales" and enrich.sender.key_client) }}

The grammar annex is normative.

An expression is the text between {{ and }} in a YAML scalar, after YAML unescaping, with leading and trailing whitespace removed. YAML quoting applies before this language sees the text.

A scalar yields a value by the following rules.

  • A scalar that holds exactly one interpolation, and no other character, yields that expression’s value with its type. max: "{{ inputs.batch_size }}" yields an integer.
  • A scalar that holds text outside an interpolation, or more than one interpolation, yields a string. Each interpolated value converts to a string by the rules of the string filter. A null converts to the empty string.
  • A scalar that holds no interpolation is a literal, typed by the serialization rules.

The same rules apply inside a template body, where a template is always a string and every interpolation therefore converts to a string.

A literal {{ is written as a one-interpolation scalar over a string literal: "{{ '{{' }}".

Terms are literals (string, int, float, bool, null), paths, list literals, and map literals.

Operators, tightest binding first:

Operators
1 ( ) grouping · . member · [ ] index · | filter
2 unary not · unary -
3 * / %
4 + -
5 < <= > >=
6 == != in
7 and
8 or
9 a if c else b conditional

Parentheses group arbitrarily and are the only way to override precedence. and and or short-circuit. The conditional expression a if c else b yields a when c is true, and b otherwise. Implementations MUST evaluate only the selected arm.

The word operators not, and, or, in, if, and else MUST be separated from adjacent terms by whitespace. Whitespace around a symbol operator is optional.

Filters bind tighter than comparison, so {{ a | length > 0 }} parses as {{ (a | length) > 0 }}.

An expression yields exactly one value. There is no statement sequencing, no assignment, and no lambda.

Member access .name requires a name that matches ^[a-z][a-z0-9_]*$. A key outside that shape is reached by indexing with a string literal.

Indexing a map with an absent key, or a list with an index outside its range, yields null, so a table lookup composes with default. A negative list index counts from the end. This applies to dynamic indexing only: an unresolvable binding path remains a validation error.

Every path MUST resolve to a binding that is in scope at that point. An unresolvable path is a validation error, not a runtime null.

An expression MUST NOT invoke a connector action, perform I/O, read a clock, read ambient state, or produce a nondeterministic value. Two evaluations against the same bindings MUST produce the same result on every conforming implementation.

Replay, durable suspension, and validation before execution depend on these constraints. Logic that cannot be written under them belongs in run.