Skip to main content

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, or jq::<expr> for jq).
  • ->filter optionally transforms the resolved value; filters chain left to right.

Roots

Request data (available on HTTP interfaces):

RootResolves to
bodythe request body — a|body::user.email|
paramsroute/query parameters — a|params::id|
headersrequest headers — a|headers::authorization|
ipthe resolved client IP
raw_bodythe exact request bytes
requestthe full request object

Config & environment:

RootResolves to
vara config-local variable from global.variablesa|var::api_base_url|
ap_vara platform managed variable, shared org-wide — a|ap_var::API_KEY|
secreta decrypted global.secrets value — a|secret::store::field|
envan OS environment variable (self-hosted only — unavailable in managed mode) — a|env::HOME|
templatea named response template
statedurable state — a|state::last_seen|, a|state::NAMESPACE.KEY|
uuida freshly generated UUID
timestamp:<spec>a formatted timestamp
includefile include (self-hosted)

Action outputs: any root that is not a built-in is treated as an action namea|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

FilterDoes
upper / lowerchange case wholesale
snake_case / kebab_case / camel_case / pascal_casere-case an identifier — Order Ref IDorder_ref_id
title_case / sentence_casere-case prose
lengthcharacters 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_pathpull 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_uripercent-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_markdownHTML 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

FilterDoes
to_numberparse 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.

FilterDoes
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_weekendSaturday 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_sinceepoch 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

FilterDoes
uniquedrop duplicates, keeping first-seen order
sort / sort('field') / sort('field','desc')order an array; sort('desc') descends a list of scalars
reversereverse the order
first / lastone 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
compactdrop 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_arraythe 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

FilterDoes
to_stringrender as text
single_quote / double_quotequote for SQL / JSON
json_esc / json_escapeescape for embedding inside a JSON string
json_stringify (alias tojson)serialise as a JSON document
raw / literalkeep any a|...| in the value literal instead of resolving it
sometrue 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|
note

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|
note

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(...).