Interpolation reference
Air Pipe interpolates values into your config at runtime using the a|…| marker.
This page is the full grammar reference — for a gentle introduction see
Variable substitutions.
Every marker has the form:
a|<root>::<path>->filter1->filter2|
<root>selects a data source (see below).::<path>optionally drills into it (dotted path, orjq::<expr>for jq).->filteroptionally transforms the resolved value; filters chain left to right.
Roots
Request data (available on HTTP interfaces):
| Root | Resolves to |
|---|---|
body | the request body — a|body::user.email| |
params | route/query parameters — a|params::id| |
headers | request headers — a|headers::authorization| |
ip | the resolved client IP |
raw_body | the exact request bytes |
request | the full request object |
Config & environment:
| Root | Resolves to |
|---|---|
var | a config-local variable from global.variables — a|var::api_base_url| |
ap_var | a platform managed variable, shared org-wide — a|ap_var::API_KEY| |
secret | a decrypted global.secrets value — a|secret::store::field| |
env | an OS environment variable (self-hosted only — unavailable in managed mode) — a|env::HOME| |
template | a named response template |
state | durable state — a|state::last_seen|, a|state::NAMESPACE.KEY| |
uuid | a freshly generated UUID |
timestamp:<spec> | a formatted timestamp |
include | file include (self-hosted) |
Action outputs: any root that is not a built-in is treated as an action
name — a|FetchUser::email| reads the email field from the FetchUser
action's output. Use jq:: for richer extraction: a|FetchUser::jq::.items[0].id|.
Filters
Chain with ->. A filter takes the value on its left and hands the next one its
result, so they read left to right.
# Uppercase a param, defaulting when absent
url: "https://api.example.com/a|params::region->upper->default(US)|"
Text
| Filter | Does |
|---|---|
upper / lower | change case wholesale |
snake_case / kebab_case / camel_case / pascal_case | re-case an identifier — Order Ref ID → order_ref_id |
title_case / sentence_case | re-case prose |
length | characters in a string, elements in an array, keys in an object |
prefix('x') / suffix('x') / wrap('x') | add text around the value |
hash / hash('sha256') / hash('sha256','base64') | unkeyed digest — sha1, sha256 (default), sha384, sha512; encoded hex (default), base64 or base64url. For a signature, use the hmac transform instead: a digest proves nothing about origin |
extract_email / extract_domain / extract_url / extract_url_path | pull the first one out of free text; null when there is none, so pair with default(...) |
encode_uri_component (alias url_encode) | percent-encode a value going into a URL as ONE query value or path segment — : / and spaces escaped, the unreserved - _ . ~ left alone so a provider comparing a redirect_uri literally still matches |
encode_uri | percent-encode a WHOLE URL, leaving : / ? # & = intact so it stays a URL |
decode_uri / decode_uri_component (alias url_decode) | the inverse of each |
strip_tags / strip_markdown | HTML or Markdown reduced to the prose inside it. strip_tags parses the page the way a browser does, so <script> bodies and attribute text never leak into the result and block elements keep their words apart. For pulling out a specific element instead, use the extract_with_css transform |
Numbers
| Filter | Does |
|---|---|
to_number | parse the value as a number |
number_format(2) | fixed decimals, grouped thousands — 1,234,567.89 |
number_format(2,'.',',') | the same with the separators swapped — 1.234.567,89 |
Dates
Every date filter accepts RFC 3339, RFC 2822, YYYY-MM-DD[ HH:MM:SS], or an epoch
number (10 digits seconds, 13 milliseconds), and emits RFC 3339 UTC — so they chain.
| Filter | Does |
|---|---|
date_format('%Y-%m-%d') | strftime rendering; a second argument names an IANA timezone |
date_tz('Australia/Sydney') | the same instant, written in another zone |
date_add('7d') / date_sub('12h') | shift by 90s, 15m, 12h, 7d, 2w |
start_of('day') / end_of('month') | truncate to second, minute, hour, day, week (Monday), month, quarter, year. A second argument reads the calendar in a timezone — the start of the Sydney day is not the start of the UTC one |
date_part('month') | one field as a number: year, quarter, month, day, hour, minute, second, millisecond, weekday (Monday = 1), week (ISO), day_of_year |
is_weekend | Saturday or Sunday, in the given timezone |
since('hours') / until('days') | whole units elapsed since, or remaining until, the value — milliseconds, seconds, minutes, hours, days, weeks |
epoch / days_since | epoch seconds; whole days old |
# Everything since the start of the month, in the customer's timezone
query: |
SELECT * FROM orders
WHERE created_at >= 'a|timestamp:s->start_of('month','Australia/Sydney')|'
Arrays and objects
| Filter | Does |
|---|---|
unique | drop duplicates, keeping first-seen order |
sort / sort('field') / sort('field','desc') | order an array; sort('desc') descends a list of scalars |
reverse | reverse the order |
first / last | one element, or null when the array is empty |
chunk(100) | split into runs of N — batching an API that takes 100 ids at a time |
compact | drop null and "" entries from an array or object. 0 and false are kept: they are values somebody sent |
join(', ') | join into a string |
array_values / json_array | the elements without brackets / the array as JSON |
map(...) | template each element ($item, $index) |
access('key') / path(...) | read into an object |
An array filter pointed at something that is not an array fails the action rather than passing the value through, so the mistake surfaces at the marker instead of in the output.
Rendering
| Filter | Does |
|---|---|
to_string | render as text |
single_quote / double_quote | quote for SQL / JSON |
json_esc / json_escape | escape for embedding inside a JSON string |
json_stringify (alias tojson) | serialise as a JSON document |
raw / literal | keep any a|...| in the value literal instead of resolving it |
some | true when the value is present and non-empty |
default(...) | substitute when the value is null or empty |
Function wrappers
Wrap a marker to coerce its rendering: single_quote(...), double_quote(...),
json_esc(...), json_stringify(...). These are useful when injecting values
into SQL or JSON bodies. A wrapper applies wherever the marker appears — on its
own, or inside a longer string such as an HTTP body field.
json_stringify follows JSON.stringify semantics: an object or array is
emitted as its compact JSON document, anything else as a JSON string literal.
The typical use is handing a structured result to an API that wants it as a
string — an LLM tool message, say:
- role: tool
tool_call_id: a|Plan::callId|
content: json_stringify(a|Tool|) # or: a|Tool->json_stringify|
A marker that is a field's whole value normally keeps its type — content: a|Tool|
sends an object. The text filters (json_stringify/tojson, json_escape,
to_string) are the exception: they exist to produce text, so their result stays
a string rather than being re-read as JSON.
Pipes inside a marker
A marker ends at its first |, so a jq expression written inside one needs to say which
pipes belong to it. Two rules do that:
- a
|inside(...),[...]or{...}is part of the expression; \|escapes a top-level pipe.
params:
# bracketed — the pipes are inside [ ... ]
- 'a|Process::jq::[.failed[] | {id: .item.id, reason: .error}]|'
# top-level pipe, escaped
- 'a|Rows::jq::.[0] \| .id|'
Quotes are deliberately not tracked, so an apostrophe (->default(can't)) never swallows
the closing pipe. A marker also still ends at a newline: keep the expression on one line, or
put it in a post_transforms step where pipes need no escaping at all.
Multi-line values
For templates, HTML or JSON, use a YAML literal block so the marker expands inside the string:
http:
url: https://example.com/webhook
body: |
a|var::some_big_json|
Always close the marker with a trailing | — input: a|body|, not input: a|body.
The open form without the trailing pipe is deprecated; older examples may still
show it. The legacy <OR> fallback syntax is likewise deprecated — use ->default(...).