Expressions and Templates
Expressions are pure computations over bindings. They produce Workfile values and can appear in interpolations, projections, action arguments, construct fields, guards, and templates.
Templates use the same expression language to produce strings under a declared escaping context.
Expression Model
Section titled “Expression Model”An expression yields exactly one value. The language has no statements, assignment, user-defined functions, or implicit access to an implementation’s host language.
The Expression Grammar is normative.
References and Resolution
Section titled “References and Resolution”A reference begins with a binding name visible in the expression’s scope. Member access uses .name; a map key that does not satisfy the name syntax is reached with a string index.
List indexes are zero-based integers, and a negative index counts from the end. Every root binding reference MUST resolve statically.
An absent binding, an out-of-scope binding, or a forward reference in a sequential scope is a validation error. These conditions do not produce runtime null.
Accessing an absent map member, or an absent additional member through a valid open-object index, yields null; an out-of-range list index also yields null. Member access or indexing on null also yields null, so a permitted reference through a skipped, failed, or unexecuted step can be composed with default.
Member access and indexing are the only operations that propagate null this way; every other operator and filter follows its own signature. A map or object index MUST be a string and a list index MUST be an int.
A statically inferred incompatible index type is a validation error; any other incompatible runtime index type causes an evaluation fault. Access to a file is limited to the metadata members defined by the Data Model.
Literals
Section titled “Literals”Expression literals are null, true, false, integers, floats, strings, durations, lists, and maps. Their exact spelling is defined by the Expression Grammar.
Integer and float literals MUST be representable in the corresponding Data Model domain, and duration literals MUST satisfy the Duration Grammar. A timestamp has no expression-literal token; a string in a position declared timestamp is validated and converted as specified for that typed position.
List and map literal members are expressions evaluated from left to right. A map key is a name or string literal and MUST be unique within that literal.
A literal does not permit computed keys. Timestamp values are created from serialized typed values or the timestamp conversion filter.
Operators
Section titled “Operators”Operators bind from tightest to loosest as follows:
| Precedence | Operators |
|---|---|
| 1 | grouping ( ), member ., index [ ], filter | |
| 2 | unary not, unary - |
| 3 | *, /, //, % |
| 4 | +, - |
| 5 | <, <=, >, >= |
| 6 | ==, !=, in, not in |
| 7 | and |
| 8 | or |
| 9 | a if condition else b |
Parentheses override precedence. Relational and equality operators are non-associative; an expression such as a < b < c is invalid.
The word operators MUST be separated from adjacent terms by whitespace. Arithmetic, equality, ordering, and boolean operators use the Data Model rules.
Unary - accepts an int, float, or duration, preserves its runtime type, and faults on overflow or any other operand type. and and or accept booleans and short-circuit.
in accepts a list, map, object, or string on its right: it tests equality with list members, key presence in a map or object, including every additional property present at runtime, or code-point substring presence in a string. The left operand MUST be a string when the right operand is a map or object. not in is the negation of in.
Any other right operand causes an evaluation fault.
Conditional Expressions
Section titled “Conditional Expressions”a if condition else b evaluates condition first and requires a boolean. It then evaluates and returns exactly one of a and b.
The unselected expression MUST NOT be evaluated or cause a fault or observable work. Conditional expressions associate from right to left.
Thus a if b else c if d else e means a if b else (c if d else e). The alternative grouping requires parentheses.
Where the containing position requires a specific static type, each result expression MUST satisfy that type. A condition whose statically inferred type is incompatible with bool is a validation error; an incompatible runtime type causes an evaluation fault.
Filter Chains
Section titled “Filter Chains”A filter applies as value | name or value | name(arguments). Filters associate from left to right and bind more tightly than comparison.
Arguments are expressions evaluated before the filter is applied, except for the per-element expressions accepted by each and keep. A bare filter name identifies a built-in filter.
The built-in filter namespace is closed: implementations MUST implement every built-in filter and MUST NOT add a bare filter name. New bare filters can be added by an additive Standard revision.
A dotted filter name identifies a filter from a resolved filter package. The resolved filter’s manifest defines its signature; the filter MUST be validated, pure, deterministic, and pinned as specified by Filter Package Manifests and Catalog and Packaging. Calling a filter with a statically incompatible receiver, argument type, or arity is a validation error.
If a value typed json prevents that determination, the same incompatibility at runtime is an evaluation fault.
Purity
Section titled “Purity”An expression MUST NOT invoke an action, perform I/O, read content referenced by a file, read a clock, obtain credentials or ambient deployment state, or produce a random or otherwise nondeterministic value. Given identical bindings and equal resolved filter packages, conforming implementations MUST produce equal values or the same evaluation fault.
These purity constraints apply to built-in and extension filters. An expression’s result MUST derive solely from its literals, bindings, and resolved filter packages.
Nondeterministic values MUST be obtained through an appropriate action or profile-defined construct and recorded as bindings.
Interpolation
Section titled “Interpolation”An interpolation is an expression enclosed by {{ and }} in a YAML scalar. YAML decoding occurs first; the expression parser then removes leading and trailing whitespace inside the delimiters.
Keys MUST NOT contain interpolations.
Whole-Value Interpolation
Section titled “Whole-Value Interpolation”A scalar consisting of exactly one interpolation and no other characters yields the expression’s value without converting its type. For example, "{{ inputs.limit }}" yields an int when inputs.limit is an int, despite the YAML quoting required to carry the interpolation.
String Interpolation
Section titled “String Interpolation”A scalar with text outside an interpolation, or with more than one interpolation, yields a string. Each interpolated value is converted using the string filter; null contributes the empty string.
Conversion failure is an evaluation fault. A scalar with no interpolation is a literal governed by the serialization rules.
To produce the literal characters {{ where they would otherwise begin an interpolation, an author can interpolate a string literal containing them, as in "{{ '{{' }}".
Interpolation Typing
Section titled “Interpolation Typing”A validator SHOULD warn when a string scalar consists of one interpolation plus only whitespace outside its delimiters, because that scalar is string interpolation rather than whole-value interpolation.
The type of a whole-value interpolation is the expression’s type. The type of every string interpolation is string.
Each expression in a string interpolation MUST have a static type assignable to scalar or json. A different inferred type is a document.type validation error.
A value typed as json faults during conversion when its runtime value is outside the string filter’s domain. A validator MUST apply the resulting type when checking an action argument, construct field, projection, guard, or other typed position.
Expressions at the leaves of a list or map are evaluated recursively while the surrounding structure remains data. Interpolations in keys are invalid because keys are not expression positions.
Template interpolations always contribute to a rendered string; escaping is governed by the context rules under Templates and wf.render.
Built-in Filters
Section titled “Built-in Filters”The following tables are the complete built-in filter set and use the signature notation defined under Static Typing and Assignability. A numeric or enumerated bound stated for an argument in the normative table entry of a specification-defined filter is part of that argument’s declared domain. A literal argument outside that domain is invalid with document.type; a computed argument outside it causes fault.expression.
A bracketed argument is optional. Unless stated otherwise, filters preserve input order and do not mutate their receiver.
Selection and Projection
Section titled “Selection and Projection”| Filter | Signature and behavior |
|---|---|
where(path: string, op: string, value: any) |
list[T] -> list[T], where T is assignable to object; keeps elements whose filter path satisfies op value. |
keep(expr: bool) |
list[T] -> list[T]; keeps elements for which expr is true. |
each(expr: U) |
list[T] -> list[U]; yields the result of expr for each element. |
A filter path is a nonempty string literal containing one or more Workfile names separated by .. Each segment selects an object member.
A filter path contains no list index or escape syntax and therefore cannot address a member whose name is not a Workfile name, including one containing .. A path invalid against a statically inferred object shape is a validation error.
Encountering a non-object before the final segment in any other case causes an evaluation fault. where takes a filter path and a string literal containing exactly one of ==, !=, <, <=, >, >=, in, and not_in.
where uses the comparison rules; not_in is the negation of in. For <, <=, >, and >=, where(path, op, value) is equivalent to keep(current.<path> != null and current.<path> op value), with <path> denoting the member chain selected by the filter path.
For ==, !=, in, and not_in, it is equivalent to keep(current.<path> op value) and follows the ordinary comparison rules, including their treatment of null. This equivalence defines behavior; where is not subject to the nesting restriction on keep and each.
For keep and each, current is bound to the current element while their expression is evaluated; enclosing bindings remain visible.
keep requires a boolean result. These per-element expressions MUST NOT contain another keep or each.
current is reserved and resolves only in these expressions. Elements are processed in source order.
Collections
Section titled “Collections”| Filter | Signature and behavior |
|---|---|
length |
list[any] | map[string, any] | object | string -> int; counts list members, map entries, every object member present at runtime including additional properties, or string code points. |
default(v: U) |
T? -> join(T, U); returns the fallback only when the receiver is null. |
required |
T? -> T; returns a non-null receiver unchanged and causes fault.type when the receiver is null. A receiver with the static type null is a validation error. |
first, last |
list[T] -> T?; return null for an empty list. |
slice(a: int, b: int) |
list[T] -> list[T]; half-open range. Each negative index is first increased by the list length, and each resulting index is then clamped to the range from zero through the list length. If the resulting start is not less than the resulting end, the result is empty. |
reverse |
list[T] -> list[T]. |
keys |
map[string, T] | object -> list[string]; includes every member present at runtime, including additional properties, and uses map key order. |
values |
map[string, T] -> list[T], or object -> list[U]; includes every member present at runtime, including additional properties, and uses map key order. For an object, U is the join of its declared non-null property types and, unless additionalProperties: false, its additional-properties type. For a closed object with no declared properties, the result is polymorphic in the same manner as an empty list literal. |
items |
map[string, T] -> list[object{key: string, value: T}], or object -> list[object{key: string, value: U}]; includes every member present at runtime, including additional properties, and uses map key order. For an object, U is determined as for values; for a closed object with no declared properties, the result is polymorphic in the same manner as an empty list literal. |
flatten |
list[list[T]] -> list[T]; removes one nesting level. |
batch(n: int) |
list[T] -> list[list[T]]; contiguous chunks of at most positive n. |
group_by(path: string) |
list[T] -> map[string, list[T]], where T is assignable to object; groups by a string-valued filter path. |
index_by(path: string) |
list[T] -> map[string, T], where T is assignable to object; retains the first element for each string-valued filter path. |
sort |
list[T] -> list[T]; stable; uses standard ordering, where T is number, string, timestamp, or duration. |
sort_by(path: string) |
list[T] -> list[T], where T is assignable to object; stable ordering by the filter-path value. |
unique |
list[T] -> list[T]; retains the first equal occurrence. |
sum |
list[T] -> T, where T is int or float; sums members as defined below. |
min, max |
list[T] -> T?; use standard ordering and yield null for an empty list, where T is number, string, timestamp, or duration. |
join(sep: string) |
list[string] -> string. |
A sort, min, or max receiver with any other statically inferred element type is invalid with document.type. The path argument of group_by, index_by, and sort_by is a filter path.
For group_by and index_by, an absent, null, or non-string final member causes an evaluation fault. For sort_by, an absent, null, unordered, or mutually incomparable final member causes an evaluation fault.
An incompatibility established from the statically inferred receiver shape is instead a validation error. sum of an empty list is the int value 0.
A list containing only int values is summed exactly and produces an int; overflow causes an evaluation fault. If any member is a float, every member is promoted to binary64 and the values are added in source order, producing a float; a non-finite result causes an evaluation fault.
group_by preserves source list order within each group. A computed nonpositive batch size causes fault.expression.
Other runtime signature failures are governed by Evaluation Faults. For values and items, an object with no properties produces an empty list whose element type is polymorphic in the same manner as an empty list literal.
Strings
Section titled “Strings”| Filter | Signature and behavior |
|---|---|
split(sep: string, [limit: int]) |
string -> list[string]; positive limit bounds element count and the last element holds the remainder. |
lower, upper |
string -> string; simple lowercase or uppercase mapping under Unicode Version. |
trim |
string -> string; removes leading and trailing characters with the Unicode White_Space property. |
starts_with(s: string), ends_with(s: string) |
string -> bool. |
matches(re: string) |
string -> bool; true when an unanchored search finds a match. |
extract(re: string) |
string -> string?; first capture group, or whole match if none, and null when unmatched. |
extract_all(re: string) |
string -> list[string]; corresponding value for each non-overlapping match. |
replace(a: string, b: string) |
string -> string; replaces every literal occurrence of a. |
html_escape, json_escape, url_encode |
string -> string; apply the named escaping or encoding. |
sha256 |
string -> string; digest UTF-8 bytes and emit lowercase hexadecimal. |
base64 |
string -> string; encode UTF-8 bytes using the padded standard alphabet. |
Regular-expression filters use the Regular Expression Grammar and Matching Semantics. The separator of split and the search string of replace must be nonempty. A literal empty argument is invalid with document.type; a computed empty argument causes fault.expression.
An invalid pattern is a validation error with syntax.expression when literal and fault.expression when computed. html_escape replaces &, <, >, ", and ' with &, <, >, ", and '.
json_escape applies RFC 8259 string-content escaping without surrounding quotes and uses lowercase four-digit hexadecimal escapes for other control characters. url_encode preserves only RFC 3986 unreserved bytes and percent-encodes every other UTF-8 byte using uppercase hexadecimal.
Numbers and Conversion
Section titled “Numbers and Conversion”| Filter | Signature and behavior |
|---|---|
abs |
T -> T, where T is int or float; preserves numeric type and faults on abs(INT64_MIN). |
round([n: int]) |
T -> T, where T is int or float; n defaults to 0 and is an integer from 0 through 15. An int receiver is unchanged. For a float, it exactly scales the receiver’s binary64 value by 10^n, rounds the exact scaled value to an integer with halfway cases away from zero, rescales exactly, and returns the nearest binary64 value. |
floor |
number -> int; rounds toward negative infinity. |
ceil |
number -> int; rounds toward positive infinity. |
trunc |
number -> int; rounds toward zero. |
int |
string | float | int -> int; string input accepts exactly signed-int-string, and float input must be integral and in range. |
float |
string | int | float -> float; string input accepts exactly signed-float-string. |
string |
scalar -> string; uses the conversions below. |
bool |
bool | string -> bool; string input MUST be exactly true or false. |
timestamp |
string -> timestamp; parses an RFC 3339 value with mandatory offset. |
duration |
string -> duration; parses the duration grammar. |
json |
any -> string; produces canonical JSON. |
from_json |
string -> json; parses one RFC 8259 document and rejects repeated object names. |
string maps null to the empty string, booleans to true or false, numbers according to Numeric Operations and Conversion, timestamps according to Timestamps and Durations, and durations to canonical duration syntax. It faults on lists, maps, objects, and files.
floor, ceil, and trunc return an int input unchanged. Note: The exact binary64 value represented by the literal 2.675 is slightly less than 2.675, so 2.675 | round(2) produces 2.67.
For a float, they produce the mathematically rounded integer specified in the table and cause an evaluation fault when that integer is outside the signed 64-bit range.
json accepts values in the JSON domain and serializes them using Canonical JSON Value Serialization. At any nesting depth, it converts timestamps as defined under Timestamps and Durations and durations to the canonical duration string produced by the string filter. It faults if the receiver or any recursively contained value is a file or is otherwise outside that domain after those conversions. from_json decodes serialized timestamps and durations as strings; this conversion does not reconstruct their original runtime types.
from_json assigns numeric types according to Values Typed json. Text that would decode to an unpaired surrogate causes fault.expression. Failed, ambiguous, non-finite, or out-of-range conversions are evaluation faults.
The signed numeric string productions deliberately exclude a leading +, surrounding whitespace, leading zeros, a missing digit on either side of a decimal point, Infinity, NaN, and the empty string.
| Filter | Signature and behavior |
|---|---|
plus(value: duration), minus(value: duration) |
timestamp -> timestamp; adjust the instant and preserve display zone. |
diff(value: timestamp) |
timestamp -> duration; receiver minus argument. |
format_time(fmt: string) |
timestamp -> string; format in the display zone. |
to_timezone(tz: string) |
timestamp -> timestamp; change only the display zone to an IANA identifier or fixed offset. A fixed offset uses the offset production of the Timestamp Grammar. |
format_time recognizes only %Y, %m, %d, %j, %H, %M, %S, %I, %p, %L, %z, %:z, %b, %B, %a, %A, and %%. Numeric fields are zero-padded to their conventional widths; %Y has at least four digits. %p emits AM or PM. %z and %:z emit +HHMM and +HH:MM.
Month and weekday names are English, with three-character abbreviations. Other characters are copied.
An unknown directive is a validation error with document.type in a literal format and an evaluation fault with fault.expression in a computed format. An unknown computed time zone causes fault.expression. A time operation outside the timestamp domain, or an operation whose display-zone civil time is outside years 0000 through 9999, causes an evaluation fault.
An invalid literal time zone is a validation error with document.type. An unavailable required time-zone-data version makes the resolved workflow unsupported before execution.
Templates and wf.render
Section titled “Templates and wf.render”Use of wf.render structurally activates the wf.template-rendering capability. Base validation of wf.render covers its step-level shape, its with projection, source-visible literal path rules, and capability inference under Validation Ownership.
Validation of template content, including tag and block grammar, expressions within tags, bidirectional variable checking, partial containment and context, partial nesting, and referenced-set part derivation, is the validator obligation of the wf.template-rendering capability. A validator that does not claim wf.template-rendering MUST NOT claim document or resolved validity when that result depends on template-content validation; it reports capability validation as incomplete and identifies wf.template-rendering as required.
A valid Workfile that uses it is subject to the unsupported-workflow prohibition under Static Validation when the executor does not claim the capability. wf.render renders a workflow-owned template and binds either one string or a map of named strings.
| Key | Role | Required | Type or domain | Default |
|---|---|---|---|---|
wf.render |
Subject | Yes | Inline template map or literal project-relative template/template-set path; see the forms below. | — |
with |
Configuration | No | Binding projection evaluated in the step’s enclosing scope. | {}. |
Rendering is pure: template selection and content are fixed before execution, and the body can observe only its explicit context.
The bindings produced by with, plus bindings introduced by template blocks, are the template’s entire scope. No other workflow binding is ambient inside a template.
Template interpolations use the expression language and built-in or resolved extension filters. Templates also support {{#each list as name}}...{{/each}}, which requires a list and introduces name, and {{#if condition}}...{{else}}...{{/if}}, whose else arm is optional and whose condition requires bool.
Blocks can nest. current has no template meaning.
A validator MUST infer referenced top-level variables from the template and its partials and check with in both directions. An unsupplied variable is invalid with document.template unless every reference is protected by default; an omitted optional variable binds null.
A validator SHOULD warn when a supplied variable is never referenced. Every interpolation is escaped for the template context except that an interpolation whose value has type safe_html is emitted unchanged in HTML context.
HTML context performs HTML escaping on every other value. JSON context emits JSON string content.
Text and unrecognized file-extension contexts perform no escaping and discard any HTML-safety designation. An intrinsic template evaluation or rendering failure is an evaluation fault of the wf.render step with fault.template; a fault from an embedded expression or filter retains its more specific code.
Inline Templates
Section titled “Inline Templates”An inline wf.render value contains exactly one key from this table.
| Key | Role | Required | Type or domain | Default |
|---|---|---|---|---|
text, html, json |
Member | One of these or parts |
Template string; selects the named escaping context and binds one rendered string. | — |
parts |
Member | One of these keys | Map from distinct part names to inline templates. | — |
Each part MUST use exactly one of text, html, or json.
The step binds a map from each part name to its rendered string. Part names use the Workfile name syntax.
Multi-part Rendering
Section titled “Multi-part Rendering”A multi-part render produces several related strings, such as a subject, a plain-text body, and an HTML body. Each part renders independently with the same with projection.
The step binds all part results atomically as one map. Inline parts names each result explicitly.
A referenced template set derives each part name from the final path segment without its extension. If multiple entries share a stem, each colliding name appends _ and its extension without the dot. Failure of this derivation to produce unique names is invalid with document.template.
After derivation and collision handling, each resulting name MUST satisfy Workfile name syntax, MUST NOT be reserved, and MUST NOT conflict with another visible binding; a violation is invalid with document.name. Result maps follow Map Order.
Referenced Templates
Section titled “Referenced Templates”A string value of wf.render is a literal project-relative path. It names either one template or a template set and MUST NOT contain an interpolation or be absolute.
A single template causes the step to bind one string. A template set consists of the immediate, non-partial entries beneath the named path and causes the step to bind a map of parts.
A reference that identifies both a single entry and a template set is invalid. Template content MUST be valid UTF-8.
A .html extension selects HTML context, .json selects JSON context, and every other extension selects text context. The referenced templates and their partials MUST resolve before execution and be pinned with the workflow definition.
Partial Inclusion
Section titled “Partial Inclusion”A referenced template whose final path segment begins with _ is a partial: a reusable source fragment that creates no part or binding. {{> path}} includes a partial using the bindings visible at the include site and the including template’s escaping context.
The include path is literal, is resolved relative to the including template, and MUST remain within the pinned project content after normalization. An include that escapes that content is invalid with document.template; an initial lookup whose valid contained path is absent from supplied project content uses dependency.unresolved. Missing resolution input or unavailable previously pinned content follows Resolution and Pinning. A partial’s context MUST match that of every template that includes it; a mismatch is invalid with document.template.
A partial MUST NOT include another partial, including itself; a violation is invalid with document.template. Inclusion depth is therefore at most one below a part or single template.
Parts and partials are workflow definition content, not runtime file data. This revision defines no reusable template-package format.
Evaluation Faults
Section titled “Evaluation Faults”An evaluation fault occurs when a valid expression cannot produce a value for its runtime inputs.
Faults include:
- arithmetic overflow, division, integer division, or remainder by zero, or a non-finite numeric result;
- an operator applied outside its runtime type domain;
- an invalid dynamic index or ordered comparison;
- a guard or
keepexpression that does not yieldbool; - a
requiredfilter whose receiver evaluates tonull; - a dynamically checked string that is not a member of its required enum;
- a filter applied outside its signature, including an invalid computed pattern, format, path, time zone, or conversion;
- failure to parse JSON, a timestamp, or a duration under the required grammar; and
- a template expression or block that cannot produce its required value.
A condition that the Static Facts rules classify for validation is a validation error, not an evaluation fault. Other failures in an otherwise valid expression are evaluation faults, even when evaluating the expression’s particular literal operands would reveal the failure before execution. Evaluation faults are deterministic. Every registered fault. code identifies a deterministic evaluation fault.
A fault while evaluating step arguments, a wf.value, a projection owned by a step, or a template fails that step with the registered fatal fault code and MUST NOT be retried as though reevaluation could change it. Step policy and enclosing recovery then apply normally. A fault in a step guard is handled as defined for conditional execution with when. A fault in top-level trigger configuration, guard, queue key, partition key, or trigger projection occurs before a run and prevents initiation as defined under Triggers. Configuration faults in a state subscription or wf.wait_for occur during a run and follow the rules of the applicable profile.
A fault while evaluating workflow outputs prevents a successful run result. Implementations MUST preserve a fault’s registered code and logical location in any run history required by an applicable profile. Replay MUST reproduce a recorded fault rather than reevaluate it against different ambient conditions.