Skip to content

Flow Language Reference

The exhaustive grammar behind Values & Expressions and The Shape of a Flow — every interpolation form, every built-in function, every type tag, and every validation code. Read those pages first for the tutorial version; come here for the complete table.

Every string value in flow YAML supports ${...} templates. There are three forms, and which one applies depends on what you write and where:

Form As the entire value Inside a string
${path} Reference — preserves the original type (array, object, number, boolean) Stringified for interpolation
${= expression} Typed expression result Stringified for interpolation
${secret:path} / ${secret:path:key} Pre-resolved secret Error — secrets cannot be embedded in strings
No ${...} Literal Literal
$${...} Escapes to the literal ${...} Escapes to the literal ${...}
Terminal window
# Entire value — preserves type:
items: "${step1.rows}"
# Inside a string — stringified:
message: "Hello ${step1.body.name}, you have ${= len(step1.rows)} rows"
# Expression, entire value — typed result:
total: "${= step1.a + step2.b}"
# Secret — must be the whole value, never embedded:
ApiKey: "${secret:prod/api-keys}"
# header: "Bearer ${secret:prod/token}" # ERROR — secrets can't embed in a string
# Escaped — renders literally as ${not.a.ref}:
literal: "$${not.a.ref}"

Implicit .result fallback. ${step1.name} also tries step1.result.name for backwards compatibility — prefer the connector’s actual output field name (body, rows, data, …) for clarity.

Unresolved references fail loudly. A bad path halts the step with a structured error naming the path, the input field, and the available roots — no silent fallback to an empty string or a marker substring.

Where expressions are not wrapped. Conditions in if:, filter.where:, and switch.cases: are already an expression context — they take no ${= …} wrapper:

Terminal window
- if: count > 0 # correct
# - if: "${= count > 0}" # wrong — double-evaluated
# …

Low to high:

Precedence Operators
1 (lowest) OR, ||
2 AND, &&
3 NOT, ! (prefix)
4 IN, NOT IN, BETWEEN … AND, IS NULL, IS NOT NULL, LIKE
5 =, !=, <>, <, >, <=, >=
6 +, - (arithmetic)
7 (highest) *, /, %

Plus the ternary: condition ? trueExpr : falseExpr.

38 functions, registered once and shared across every expression context (if:, ${= …}, filter.where:, switch.cases:).

Function Args Returns What it does
len(x) 1 number Length of a string, array, or object (key count). null or a missing path → 0.
str(x) 1 string Convert to string.
int(x) 1 number Convert to integer.
float(x) 1 number Convert to float.
bool(x) 1 boolean Strict parse — 'true'/'1' → true, 'false'/'0' → false, native 0/non-zero → false/true. An un-parseable value errors.
min(a, b) 2 number Smaller of the two.
max(a, b) 2 number Larger of the two.
lower(s) 1 string Lowercase.
upper(s) 1 string Uppercase.
trim(s) 1 string Trim leading/trailing whitespace.
contains(s, sub) / contains(arr, item) 2 boolean Case-insensitive substring match on a string; array-membership check on an array/list — resolved by the runtime type of the first operand.
startsWith(s, prefix) 2 boolean Case-insensitive prefix match.
endsWith(s, suffix) 2 boolean Case-insensitive suffix match.
isEmpty(x) 1 boolean true for an empty string, null, empty array, or empty object.
concat(a, b, …) 2+ string or array Concatenate strings, or arrays/lists if any argument is one.
split(s, delim) 2 array Split a string into an array on every (case-insensitive) occurrence of delim. Empty delim splits into individual characters.
join(arr, delim) 2 string Join an array/list to a string with delim.
replace(s, old, new) 3 string Replace every case-insensitive occurrence of old with new.
abs(n) 1 number Absolute value.
round(n) 1 number Round to the nearest integer.
shellEscape(s) 1 string Single-quote-escape a string for safe use as a shell argument.
path(data, jsonpath) 2 any Extract value(s) at a JSONPath expression from data — a JSON string, an already-structured value, or an XML string (auto-detected from a leading <). One match returns that value with its native type; several matches return an array; no match returns null; malformed input fails the step. http.*’s body (see Reaching into JSON) auto-parses on a JSON Content-Type, so dotted access (step1.body.field) already works there — path() is the tool for a non-JSON body, and for genuine JSONPath queries (multi-match, wildcards, filters) on a body that’s already parsed.
not(x) 1 boolean Logical negation — the function form of ! / NOT.
toJson(x) 1 string Serialize a value to a JSON string.
first(list) 1 any First item of a list/array, or null if empty — the safe alternative to list[0] indexing.
take(list, n) 2 array First n items of a list/array.
skip(list, n) 2 array Drop the first n items of a list/array.
omit(map, keys) 2 object Return map with the named keys (an array of strings) removed.
field(array, name) 2 array Pluck one field out of an array of objects — field(rows, "id") over [{id:"x"},{id:"y"}] → ["x","y"]. Missing fields are silently dropped; a null array returns [].
now() 0 string Current UTC instant as an ISO 8601 timestamp with offset.
today(tz?) 0 or 1 string Current date as YYYY-MM-DD. UTC by default; pass an IANA timezone id for the local date.
dateAdd(date, n, unit) 3 string Add n of unit (years|months|weeks|days|hours|minutes|seconds) to date. Output precision follows the input’s.
dateFormat(date, fmt) 2 string Format date with a .NET format string, invariant culture (locale-portable).
dateDiff(a, b, unit) 3 number Full-period count of b − a in unit. Sign matches b − a.
keys(map) 1 array Sorted array of a map’s keys — lexicographic order, not insertion order.
values(map) 1 array Array of a map’s values, in the same lexicographic key order as keys().
entries(map) 1 array Array of {key, value} objects, same ordering — use when you need both halves in one iteration.
zip(a, b, …[, mode]) 2+ array Pair parallel arrays positionally into an array of tuples: zip(['a','b'], [1,2]) → [['a',1],['b',2]]. Optional trailing mode reconciles unequal lengths: strict (default — errors on a length mismatch), pad (fills the tail with null), truncate (stops at the shortest).

Pluck one column out of an array of objects:

Terminal window
$query:
invoke: redshift.query
with: { Sql: "select id, name from products" }
return:
ids: "${= field(query.rows, \"id\")}" # → ["x", "y"]
csv: "${= join(field(query.rows, \"id\"), \",\")}" # → "x,y"

Iterate a map’s entries in one for-each:

Terminal window
$dump:
for-each: $e in "${= entries(headers)}"
do:
- log: "${e.key} = ${e.value}"

Expressions coerce values to the type each connector needs:

  • To boolean — "true"/"false" (case-insensitive); empty string → false, non-empty → true; 0 → false, non-zero → true; null → false.
  • To number — strings parsed as decimal. A boolean is not a number: true/false do not coerce to 1/0 in arithmetic, int()/float(), or abs()/round()/min()/max(). A boolean operand of +, -, *, /, % is a type error. To count a boolean, convert it explicitly with a ternary: ${= (flag ? 1 : 0) + total}.
  • To string — booleans become "true"/"false"; numbers use invariant-culture formatting; null becomes "".

Strict cross-type comparison. =, !=, <, >, <=, >=, and BETWEEN require operands of the same primitive type for the boolean category — comparing a boolean to a non-boolean is a type error (a compile error when both sides are literals, otherwise a runtime step error). Convert explicitly with bool(...), or type the source column with detect-types: true on the load: step.

Strict null semantics. null is a distinct, non-comparable sentinel:

Expression Result
null = null true
null = 'X' false
null = 0 false — null does not equal zero
field IS NULL true only for a genuine null; "" is not null
field IS NOT NULL true for "", 0, false, and any non-null value

Most string/numeric builtins reject a null argument outright (lower, upper, trim, str, split, replace, concat, shellEscape, join — guard with IS NOT NULL or a ternary). The deliberate exceptions: len(null) → 0, isEmpty(null) → true (both commonly used as the null guard), and the collection builtins (first/take/skip/field/keys/values/entries/omit), which return null/[] on a null input by design.

Intermediate nulls propagate. A real null at any intermediate position of a path inside ${= …} propagates as null through the rest of the chain, and so does a missing key on a real object — ${= product.attrs.optional[0].data} returns null rather than erroring when optional is absent. Strict resolution is preserved for two failure classes that indicate a real bug: a typo’d root (${= triggre.name} still fails — triggre is not in scope), and descent into a non-object primitive — ${= weather.body.temp} still fails when body hasn’t parsed (a non-JSON response: body is then a plain string, not an object); use path() instead.

Expression failures at runtime (unknown function, type mismatch, unresolved root) halt the step with a structured error containing the expression source and a human-readable reason — no marker substrings are embedded into the payload.

Parse failures inside ${= ...} are caught at compile time as V2004 and reject the flow before it ever runs. A typo’d root does not silently evaluate to 0/""/false — it fails the step, so a real bug can’t hide behind a falsy default.

Tag Type Example
!str / !str? String Name: !str
!int / !int? Integer Count: !int 10
!float / !float? Float Rate: !float? 0.5
!bool / !bool? Boolean DryRun: !bool? false
!obj / !obj? Object Config: !obj
!any / !any? Any type Data: !any?
!str-list String list Tags: !str-list
!int-list Integer list Ids: !int-list
!enum(a|b|c) Enumeration Mode: !enum(fast|slow) fast
  • Append ? for optional (defaults to null if not provided).
  • A value after the tag is the default.
  • There is no !arr/!arr? tag — use a typed *-list tag, or !any? for a heterogeneous array.

Semantic string tags carry a built-in validator:

Tag Validates
!email Pragmatic shape (local@host.tld)
!phone Digits, spaces, dashes, parens, optional leading +; 3–20 chars
!filepath No null bytes / control characters; length ≤ 4096 (existence is not checked)
!url Absolute URL with an explicit scheme
!regex Value compiles as a valid regex

Constraint annotations. Numeric tags accept an inclusive range; !str accepts a regex pattern — checked at flow-load (literal defaults, literal with: arguments) and at runtime (assignment, persist-load, connector-input resolution, output: return):

Terminal window
input:
Port: !int (1..65535) 8080 # range with default
Timeout: !int (100..) 5000 # min only
Slug: !str /^[a-z][a-z0-9-]*$/ # regex-validated string

!enum vs !str /pattern/. Use !enum(a|b|c) for a finite known set of alternatives; use !str /pattern/ for open-ended shapes (slugs, SKUs, free-form IDs).

Code Severity Description
V1001 Error Unsupported schema major version
V1002 Error Invalid version format
V1003 Error Invalid cron expression
V1010 Error Duplicate binding name or write to an immutable variable
V1020 Error Duplicate step name
V1030 Warning Unknown connector
V1031 Error Unknown action for connector
V1032 Error Missing action (required for multi-action connectors)
V1033 Warning Unknown input field for connector/action
V1034 Error Connector not declared in using: block
V1035 Error Flow invokes connectors but has no using: block
V1040 Error Trigger missing name
V1041 Error Duplicate trigger name
V1042 Error Trigger missing source
V1043 Warning Unknown trigger source
V1044 Error Trigger missing a required field for its source
V1050 Error Unrecognized top-level field
V1051 Warning Unrecognized step field
V1053 Error fields: and exclude-fields: cannot both be specified on the same step
V1054 Error Invalid pattern in fields: / exclude-fields:
V1060 Error retry: max-attempts < 1
V1061 Error retry: initial-delay-ms < 0
V1062 Error retry: max-delay-ms < initial-delay-ms
V1063 Error retry: backoff-multiplier < 1.0
V1067 Error Trigger with: references a key that is not a declared flow input
V1069 Error Trigger with: value has the wrong type (or is not a literal) for the flow input
V1100 Error Invalid filter WHERE expression
V1101 Error Invalid filter ORDER BY syntax
V1102 Error Filter source connector empty
V1202 Error Retired per-step binding keyword (environment:/connection:) — use the step’s on: keyword (not the run-level environment= parameter)
V1314 Error A step’s on: <env>/<alias> qualifier names an environment not declared in using:, or a bare on: <alias> is used on a flow that declares no environments
V1315 Error A step’s on: names an alias the resolved environment does not bind
V1316 Error A flow input: field uses a reserved framework payload key (_run-mode, _flow-name, source, items, …) — on and environment are no longer reserved by this rule (ZEN-4597)
V1317 Error Trigger on: names an environment not in the flow’s declared set
V1318 Warning Trigger has no on: on a flow declaring more than one environment — it fires only against the first declared
V2004 Error Parse failure inside ${= ...}
V2006 Error Statically-known type mismatch in a comparison (e.g. boolean vs number/string)

See the Error Catalog for how these surface over REST and GraphQL.