7.3 Step kinds: call, transform, pipeline, split
Each step does exactly one of four things — call, transform, pipeline,
or split — plus optional modifiers (if, try, as/name, retry). This
section covers what each kind does to the in-flight message.
The in-flight message is the value flowing through the pipeline. A step
usually replaces it with its result; as and try change that, as noted below.
call — invoke another mount, or an external URL
The in-flight message is sent to the (interpolated) URL through full dispatch.
GET/HEAD send no body; other methods forward the in-flight body. The
response becomes the new in-flight message.
{ "call": { "method": "GET", "url": "/data/orders/${id}" } }
An absolute http(s):// URL leaves the node through the mount's httpOut
grants (8.5) — the same allowlist-grant vocabulary code mounts use, declared on
the pipeline (or wrapper) mount's config:
{ "path": "/orders", "service": "pipeline", "config": {
"access": { "invoke": "U", "write": "A" },
"grants": { "stripe": { "type": "httpOut",
"hosts": ["api.stripe.com"],
"inject": "infra:stripe-key" } } } }
{ "call": { "method": "POST", "url": "https://api.stripe.com/v1/charges",
"headers": { "x-account": "${account.id}" },
"effect": "keyed" } }
- Allowlist first: the target host must match a grant's
hosts(exact or*.suffix); no match iscapability_deniedbefore any I/O. A mount with nohttpOutgrants cannot call out at all. The check runs after${...}interpolation, so a variable-built URL is gated the same way. - Credentials are injected host-side: the matching grant's
inject(operatorinfra:ref or an inline strategy over granted secrets) is applied on the way out — the secret never appears in spec content. With overlapping grants, the first matching grant in grant-name order wins. - The external request is built fresh: it carries only the step's declared
headers(plus the auto idempotency key, 7.7) — never the inbound caller'sAuthorization/Cookie, and never the principal.elevateis inert on an external call. Header values interpolate with the same${...}rules as the URL. When absent, the host derivesContent-Typefrom a forwarded body and suppliesAccept: */*plus anrs2/<version>User-Agent. - Retry applies unchanged (7.7): retryable statuses, network errors,
backoff,
Retry-After, and effect-class gating work exactly as for internal calls. Transport failures surface as a502-class step failure with the usual per-step report. GET <spec>?$planwarns about literal external URLs no grant covers (7.8).
transform — reshape the body with JSONata
A JSONata expression (or a template object whose string leaves are expressions) computes a new body from the current one. Covered in detail in 7.6.
{ "transform": { "id": "$order.id", "ok": "$_ok" } }
pipeline — a nested subpipeline
A nested spec runs as a step. Its end/stop actions are local to it.
Nesting is how you build parallel branches and scoped sub-flows (7.4).
{ "pipeline": { "mode": "parallel", "steps": [ … ], "join": "jsonObject" } }
split — fan out over a collection (jsonSplit)
An array or object body is split into one message per element; the remaining
steps of the pipeline run per element, in parallel (fan-out cap 1000,
concurrency default 12), then the results join (jsonObject, keyed by element
name/index; errored elements become {"_errorStatus", …}).
{ "split": "jsonSplit" }
The modifiers
as: "$var"— capture the step's result into a variable instead of replacing the in-flight message. The message keeps its prior body; a failed captured call becomes{"_errorStatus", "_errorMessage"}in the variable and the pipeline continues. Use it to gather data without losing your working message.try: true— a failure becomes the body{"_errorStatus", "_errorMessage"}with status 200 and the pipeline continues, instead of failing the step. Use it for "attempt, but don't abort on error."if: "<condition>"— run the step only if the condition holds (7.5). A skipped step silently does nothing.name/retry— name a step (for parallel joins) and attach a retry policy (7.7).elevate: true(callsteps only) — add the mount's configured mediation role to the internal call while retaining the caller's identity. See below.
elevate — the gateway pattern
By default a call step forwards the caller's identity, so the internal call is
re-checked against the target mount's roles (5.4): a caller can only reach,
through a pipeline, what they could reach directly. elevate: true adds the
pipeline mount's configured elevate role to that principal for this call. An
anonymous caller gets a synthetic principal carrying only that mediation role.
{ "call": { "method": "GET", "url": "/data/secrets/${id}" }, "elevate": true }
This restores v1's gateway pattern: lock a service down, then front it with a caller-accessible pipeline that elevates — so non-admins get mediated access (validated, transformed, or restricted by the pipeline) without direct access to the locked mount.
// /data/payroll — readable only with the `payroll` role
{ "path": "/data/payroll", "service": "data",
"config": { "access": { "read": "payroll" } } }
// /my-pay — a pipeline mount configured (by an operator) with
// { "elevate": "payroll" }; anyone signed in may run it
{ "if": "...caller owns this record...",
"call": { "url": "/data/payroll/${principalId}" }, "elevate": true }
Safe by design: the elevate step adds the mount's operator-configured
elevate role to the caller's principal (keeping their identity) — it does not
drop identity or grant blanket trust. The authority lives in the mount config,
not the spec flag: without a configured role the flag is inert, and an elevate
role may not be an operator role (5.0). Omit it and behavior is unchanged
(principal forwarded and re-checked). In the string DSL it's a leading elevate
token on the step.
${...} interpolation
In a call URL, ${...} resolves against, in order: captured variables, then
the input body's JSON fields, then query params. Dot-paths work
(${order.customerId}). An unresolvable placeholder is a 400 — interpolation
never silently produces an empty string.
The same interpolation applies to a call's declared header values. With the step vocabulary in hand, the next section covers the modes that orchestrate them.
← Previous: 7.2 Typed spec and DSL · Manual home · Next: 7.4 Modes →