7.6 Transforms with JSONata
A transform step reshapes the in-flight JSON body using JSONata, a compact expression language for querying and transforming JSON. It's how you turn a service's response into the exact shape the next step — or the caller — needs.
Two forms
Template object — a JSON object whose string leaves are each evaluated as a JSONata expression (recursively; non-string scalars pass through untouched):
{ "transform": { "orderId": "$order.id", "total": "$sum($order.lines.price)", "charged": "$_ok" } }Bare string template — a single expression evaluated against the whole body:
{ "transform": "$.items[status = 'open']" }
The input and the variables
- The transform's input (the JSONata
$) is the current in-flight body. - Captured variables are available by name:
$order,$customer, … (7.5). - Runtime values:
$_status,$_ok,$_headers,$_rawBody(the raw request bytes, for signature checks). - Caller and URL:
$_user/$principalcarries the authenticated caller's email, roles, and configured extra JWT claims;$_urlcarries the triggering request's path segments and query.$_useris unbound for an anonymous caller, so test it with$existsbefore reading it.
So a transform can combine the current body, earlier captures, and the response status into one new object.
Host functions
Beyond stock JSONata, the host registers a few functions that need primitives
JSONata doesn't expose. All fail closed — bad input yields "" or false
rather than an error — so a gate built on them denies by default.
$hmac(algo, key, message)→ lowercase hex MAC (algoissha256orsha512).$hmacVerify(algo, key, message, signatureHex)→ bool, constant-time. The webhook-gating pattern:$hmacVerify('sha256', $secret, $_rawBody, $sig) ? $ : $error('bad signature'), with the signing key bound as a secret-backed variable.$hashPassword(password)→ an argon2id PHC string — the same scheme theauthservice mints on login. A provisioning pipeline can hash a plaintext password before PUTting a user record, so seeded users log in unchanged without a separate hashing step.$verifyPassword(password, hash)→ bool. Accepts argon2id PHC strings and legacy bcrypt hashes.
What JSONata buys you
JSONata handles the reshaping work you'd otherwise write code for: projecting and
renaming fields, filtering arrays (items[price > 100]), aggregating ($sum,
$count, $average), mapping, conditionals (cond ? a : b), string functions,
and building new object/array structures. A transform step keeps that logic
declarative and inside the pipeline rather than pushing you to a custom service.
Every expression leaf is parse-checked when the pipeline is stored — including
untaken branches and nested pipelines. Syntax errors are therefore a
422 validation_failed on the spec PUT. Errors that need runtime values, such
as an unbound variable or misspelled function, still surface when evaluated.
Transforms are materialization points
One behavior to keep in mind for later: a transform materializes the body (it needs the JSON in hand to evaluate). That makes it a natural segment boundary for retries (7.8) — which is usually what you want, since a transform is a clean checkpoint between a fetch and an effect. Place a transform before an unsafe call to give that call its own retryable segment.
When a transform isn't enough
JSONata covers a lot, but if you need real control flow, external libraries, or
non-trivial algorithms, that's the signal to write a custom service (Part 8)
and call it from the pipeline instead. Transforms are for shaping data, not for
arbitrary computation.
Next: making pipelines resilient — retries, effect classes, and idempotency.
← Previous: 7.5 Conditions, interpolation, variables · Manual home · Next: 7.7 Retries, effect classes, idempotency →