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.
Interpolation forms
Section titled “Interpolation forms”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 ${...} |
# 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:
- if: count > 0 # correct# - if: "${= count > 0}" # wrong — double-evaluatedOperator precedence
Section titled “Operator precedence”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.
Built-in functions
Section titled “Built-in functions”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). |
Common patterns
Section titled “Common patterns”Pluck one column out of an array of objects:
$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:
$dump: for-each: $e in "${= entries(headers)}" do: - log: "${e.key} = ${e.value}"Type coercion and null semantics
Section titled “Type coercion and null semantics”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/falsedo not coerce to1/0in arithmetic,int()/float(), orabs()/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;nullbecomes"".
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 diagnostics
Section titled “Expression diagnostics”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.
Type tags
Section titled “Type tags”| 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 tonullif not provided). - A value after the tag is the default.
- There is no
!arr/!arr?tag — use a typed*-listtag, or!any?for a heterogeneous array.
Semantic string tags carry a built-in validator:
| Tag | Validates |
|---|---|
!email | Pragmatic shape ([email protected]) |
!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):
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).
Validation error codes
Section titled “Validation error codes”| 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 on: |
| V1314 | Error | on: names an environment not declared in using: |
| V1315 | Error | on: names an alias the resolved environment does not bind |
| V1316 | Error | A flow input: field is named on — reserved as the run binding parameter |
| V1317 | Error | Trigger on: names an environment not in the flow’s declared set |
| 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.