Data and expressions
Every transition in a Flow hands one JSON value from a Step to its successor.
That stream of values — Step inputs and outputs, the value a Return emits, the
value a Result carries — is the data plane. This page shows how to observe
and shape it.
Expressions
A string value in the definition is one of exactly two things: a literal, used
as written, or an expression, when one {{ ... }} pair spans the whole
value:
"path": "/granules",
"collection": "{{ vars.collection }}"The text inside the delimiters is a CEL expression,
evaluated at runtime against the live execution state. The result has whatever
type the expression computes; the surrounding quotes are JSON syntax for an
expression-bearing value, not a claim that the result is a string. If
vars.cacheTTL is a number, "ttl": "{{ vars.cacheTTL }}" yields a number.
There is no in-string templating: an expression always occupies an entire value,
and building a string from parts is the expression’s own work,
"{{ '/granules/' + vars.collection }}". Object- and array-valued fields may
carry expressions at any string leaf, each leaf independently a literal or a
whole-value expression.
Expressions read a small set of binding roots, plain names with no sigil.
Three are in scope everywhere: vars (the Flow’s variables), frame (the
current frame’s input and metadata), and execution (the run’s identity and
timing). Others appear where their construct does: step inside a Step, call
inside a call object, match in a Match clause, middleware in a middleware
phase, failure while a failure is being handled. Each page of this tour
introduces the roots its constructs bring.
Shaping with input and output
Two Step fields shape the data crossing its boundary. input produces the value
the action consumes; output produces the value the Step emits on success. Both
default to passthrough: an absent input delivers the received value unchanged,
and an absent output emits the action’s product unchanged (for a Call, the
value its Result carries).
"fetch-items": {
"action": "Call",
"input": "{{ step.input.order }}",
"call": {
"provider": "mwl:provider.call/example/http/v1",
"with": { "method": "GET", "path": "/items" }
},
"output": "{{ {'order': step.input, 'items': step.result.value.items} }}",
"next": "process"
}Here input narrows the incoming value to its order member before the call
machinery sees it, and output builds a new object pairing the received value
with part of the response. Note the two reads: step.input is always the value
the Step received, untouched by input shaping, and step.result.value is
the value the Call’s Result carried. A Step never mutates anything; each field
produces a new value, and the bindings keep the originals readable.
When a Step exists only to reshape data, use Pass, the action that does
nothing but run its shaping fields:
"wrap": {
"action": "Pass",
"output": "{{ {'features': step.input} }}",
"next": "register"
}A few CEL notes
CEL is a small, deliberately bounded language: navigation
(step.input.items[0].id), operators (+, ==, &&, ? :), macros
(filter, map, has()), and a standard library, with no loops and no side
effects. Three things to know early:
- Data-plane numbers are doubles. Every number read through a binding
arrives as a CEL
double, and CEL does not mix numeric types in arithmetic:1 + vars.ratiois an error because1is anint. Write1.0 + vars.ratio, and prefer double literals (1000.0) when comparing against data. Equality is the exception:vars.count == 1works across numeric types. - Absent members fault. Reading a key that isn’t there is an evaluation
error, not a null. Guard uncertain shapes with
has()and the ternary:{{ has(step.input.label) ? step.input.label : 'unlabeled' }}. - Serialization is explicit. Turning structured data into a string is
toJson(value); parsing isfromJson(string). Nothing coerces values to text behind your back.
An expression that does fault produces a structured failure
(System.ExpressionEvaluationError) that routes like any other failure, which
is the subject of Handling failures.
Where the spec covers this
- Expressions — the embedding, the evaluation contract, and the full CEL profile, including the number rules.
- The data model — what values are: strict RFC 8259 JSON, one number type, the temporal formats.
- Data flow:
inputandoutput— the shaping fields and their defaults. - Data flow — the end-to-end synthesis of how values move.