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:

FormAs the entire valueInside a string
${path}Reference — preserves the original type (array, object, number, boolean)Stringified for interpolation
${= expression}Typed expression resultStringified for interpolation
${secret:path} / ${secret:path:key}Pre-resolved secretError — secrets cannot be embedded in strings
No ${...}LiteralLiteral
$${...}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:

PrecedenceOperators
1 (lowest)OR, ||
2AND, &&
3NOT, ! (prefix)
4IN, 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:).

FunctionArgsReturnsWhat it does
len(x)1numberLength of a string, array, or object (key count). null or a missing path → 0.
str(x)1stringConvert to string.
int(x)1numberConvert to integer.
float(x)1numberConvert to float.
bool(x)1booleanStrict parse — 'true'/'1'true, 'false'/'0'false, native 0/non-zero → false/true. An un-parseable value errors.
min(a, b)2numberSmaller of the two.
max(a, b)2numberLarger of the two.
lower(s)1stringLowercase.
upper(s)1stringUppercase.
trim(s)1stringTrim leading/trailing whitespace.
contains(s, sub) / contains(arr, item)2booleanCase-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)2booleanCase-insensitive prefix match.
endsWith(s, suffix)2booleanCase-insensitive suffix match.
isEmpty(x)1booleantrue for an empty string, null, empty array, or empty object.
concat(a, b, …)2+string or arrayConcatenate strings, or arrays/lists if any argument is one.
split(s, delim)2arraySplit a string into an array on every (case-insensitive) occurrence of delim. Empty delim splits into individual characters.
join(arr, delim)2stringJoin an array/list to a string with delim.
replace(s, old, new)3stringReplace every case-insensitive occurrence of old with new.
abs(n)1numberAbsolute value.
round(n)1numberRound to the nearest integer.
shellEscape(s)1stringSingle-quote-escape a string for safe use as a shell argument.
path(data, jsonpath)2anyExtract 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)1booleanLogical negation — the function form of ! / NOT.
toJson(x)1stringSerialize a value to a JSON string.
first(list)1anyFirst item of a list/array, or null if empty — the safe alternative to list[0] indexing.
take(list, n)2arrayFirst n items of a list/array.
skip(list, n)2arrayDrop the first n items of a list/array.
omit(map, keys)2objectReturn map with the named keys (an array of strings) removed.
field(array, name)2arrayPluck 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()0stringCurrent UTC instant as an ISO 8601 timestamp with offset.
today(tz?)0 or 1stringCurrent date as YYYY-MM-DD. UTC by default; pass an IANA timezone id for the local date.
dateAdd(date, n, unit)3stringAdd n of unit (years|months|weeks|days|hours|minutes|seconds) to date. Output precision follows the input’s.
dateFormat(date, fmt)2stringFormat date with a .NET format string, invariant culture (locale-portable).
dateDiff(a, b, unit)3numberFull-period count of b − a in unit. Sign matches b − a.
keys(map)1arraySorted array of a map’s keys — lexicographic order, not insertion order.
values(map)1arrayArray of a map’s values, in the same lexicographic key order as keys().
entries(map)1arrayArray of {key, value} objects, same ordering — use when you need both halves in one iteration.
zip(a, b, …[, mode])2+arrayPair 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; 0false, non-zero → true; nullfalse.
  • 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:

ExpressionResult
null = nulltrue
null = 'X'false
null = 0false — null does not equal zero
field IS NULLtrue only for a genuine null; "" is not null
field IS NOT NULLtrue 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.

TagTypeExample
!str / !str?StringName: !str
!int / !int?IntegerCount: !int 10
!float / !float?FloatRate: !float? 0.5
!bool / !bool?BooleanDryRun: !bool? false
!obj / !obj?ObjectConfig: !obj
!any / !any?Any typeData: !any?
!str-listString listTags: !str-list
!int-listInteger listIds: !int-list
!enum(a|b|c)EnumerationMode: !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:

TagValidates
!emailPragmatic shape ([email protected])
!phoneDigits, spaces, dashes, parens, optional leading +; 3–20 chars
!filepathNo null bytes / control characters; length ≤ 4096 (existence is not checked)
!urlAbsolute URL with an explicit scheme
!regexValue 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).

CodeSeverityDescription
V1001ErrorUnsupported schema major version
V1002ErrorInvalid version format
V1003ErrorInvalid cron expression
V1010ErrorDuplicate binding name or write to an immutable variable
V1020ErrorDuplicate step name
V1030WarningUnknown connector
V1031ErrorUnknown action for connector
V1032ErrorMissing action (required for multi-action connectors)
V1033WarningUnknown input field for connector/action
V1034ErrorConnector not declared in using: block
V1035ErrorFlow invokes connectors but has no using: block
V1040ErrorTrigger missing name
V1041ErrorDuplicate trigger name
V1042ErrorTrigger missing source
V1043WarningUnknown trigger source
V1044ErrorTrigger missing a required field for its source
V1050ErrorUnrecognized top-level field
V1051WarningUnrecognized step field
V1053Errorfields: and exclude-fields: cannot both be specified on the same step
V1054ErrorInvalid pattern in fields: / exclude-fields:
V1060Errorretry: max-attempts < 1
V1061Errorretry: initial-delay-ms < 0
V1062Errorretry: max-delay-ms < initial-delay-ms
V1063Errorretry: backoff-multiplier < 1.0
V1067ErrorTrigger with: references a key that is not a declared flow input
V1069ErrorTrigger with: value has the wrong type (or is not a literal) for the flow input
V1100ErrorInvalid filter WHERE expression
V1101ErrorInvalid filter ORDER BY syntax
V1102ErrorFilter source connector empty
V1202ErrorRetired per-step binding keyword (environment:/connection:) — use on:
V1314Erroron: names an environment not declared in using:
V1315Erroron: names an alias the resolved environment does not bind
V1316ErrorA flow input: field is named on — reserved as the run binding parameter
V1317ErrorTrigger on: names an environment not in the flow’s declared set
V2004ErrorParse failure inside ${= ...}
V2006ErrorStatically-known type mismatch in a comparison (e.g. boolean vs number/string)

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