Skip to main content

Air Pipe Fundamentals

Category: Backend & APIs

Get this pack →

This page is generated from the Air Pipe marketplace. Browse it live to install into your organization.

Reference config covering every core Air Pipe concept. Not a production pack — use it as a learning reference and to seed the RAG chatbot with authoritative Air Pipe syntax so it can answer "how do I do X?" questions.

All examples call public APIs (jsonplaceholder.typicode.com) — no database or managed variables required.


What's included

FilePurpose
config.ymlReference config — one endpoint per concept area

Endpoints

MethodPathConcept demonstrated
GET/fundamentals/input/paramsQuery param access with a|params
POST/fundamentals/input/bodyBody access, hide_data_on_success
GET/fundamentals/interpolation/demoAll interpolation forms
POST/fundamentals/assertions/demoAll assertion types
POST/fundamentals/run-conditions/demoAll run condition types
GET/fundamentals/transforms/demorename, remove, keep, nested_transforms
GET/fundamentals/transforms/filterfilter transform with query params
GET/fundamentals/metrics/democounter and gauge metrics

Input sources

Air Pipe actions receive input from five sources. Set the source on an action with the input: key.

SourceWhen to use
a|bodyParsed JSON request body (POST/PUT/PATCH)
a|paramsURL query parameters — ?key=value
a|headersHTTP request headers (all keys are lowercased)
a|requestFull envelope: { method, path, headers, params, body }
a|raw_bodyRaw unparsed request body string — required for HMAC verification
actions:
- name: ReadBody
input: a|body| # capture the parsed JSON body

- name: ReadParams
input: a|params| # capture ?key=value query string

- name: ReadHeaders
input: a|headers| # capture request headers

- name: ReadAll
input: a|request| # full request envelope

Access a specific field from a captured input:

# Simple field
a|ReadBody::title|

# Nested field (dot-path)
a|ReadBody::address.city|

Interpolation

Interpolation resolves values at runtime and injects them into strings, URLs, and SQL params.

# Managed variable (encrypted, never logged — set in the Air Pipe dashboard)
a|ap_var::OPENAI_API_KEY|

# Environment variable (self-hosted deployments only)
a|env::DATABASE_URL|

# Field from a previous action's result
a|ActionName::fieldName|

# Nested field via dot-path
a|ActionName::address.city|

# Wrap in JSON double-quotes (use inside JSON body strings)
a|double_quote(ActionName::title)|
a|double_quote(ap_var::MODEL_NAME)|

# Current Unix timestamp in seconds
a|timestamp|

# Current date as YYYY-MM-DD
a|timestamp:date|

# Raw unparsed request body (for HMAC verification)
a|raw_body|

IMPORTANT — missing fields do not resolve to null. When a field is absent from the source object, the interpolation token passes through as a literal string (e.g. "a|ReadBody::missingField|"). This causes:

  • Silent wrong results for text comparisons (the literal string never matches a real value)
  • Hard errors for typed casts (::int, ::timestamptz)

For optional SQL parameters, use the JSONB pattern instead of per-field interpolation:

# Pass the entire body as a single JSONB param
params:
- a|ReadFilters|

# In SQL, missing keys return genuine NULL
WHERE ($1::jsonb->>'field' IS NULL OR column = $1::jsonb->>'field')

Assertions

Assertions validate an action's result. Failing an assertion marks the action as failed — downstream actions using run_when_succeeded will not run.

assert:
http_code_on_error: 400 # HTTP status code on failure (default: 500)
error_message: "Bad input" # Response message on failure
tests:
# Presence
- value: email
is_not_null: true
description: "Shown in error context"

# Empty check
- value: name
is_not_empty: true

# Regex
- value: email
regex: "^[^@]+@[^@]+\\.[^@]+$"

# Equality
- value: status
is_equal_to: active
- value: status
is_not_equal_to: banned

# Numeric
- value: age
is_greater_than: 0
- value: score
is_less_than: 100

# Row/item count (database queries and arrays)
- value: count()
is_equal_to: 1
error_message: "Expected exactly one result"

# Null check (field must be absent or null)
- value: deletedAt
is_null: true

# HMAC signature verification
- value: signature_header
is_valid_hmac:
secret: a|ap_var::WEBHOOK_SECRET|
body: "a|ParseSignature::timestamp|.a|raw_body|"
algorithm: sha256

Run conditions

Run conditions control when an action executes relative to prior actions.

run_when_succeeded

Runs only if all listed actions succeeded.

- name: CreateRecord
run_when_succeeded: [ValidateBody]

# With a custom error code if the dependency failed
- name: CreateRecord
run_when_succeeded:
actions: [ValidateBody]
http_code_on_error: 400

run_when_failed

Runs as a fallback if the listed actions failed. Useful for alternative data sources or error recovery.

- name: FallbackUser
run_when_failed: [FetchPrimaryUser]

run_when_finished

Runs after the listed actions complete, regardless of success or failure. Use for logging, cleanup, or guaranteed side-effects.

- name: LogAttempt
run_when_finished: [FetchUser]

at_least

Runs if at least N of the listed actions succeeded.

- name: Proceed
run_when_succeeded:
at_least: 2
actions: [StepA, StepB, StepC, StepD]

run_on_assertion

A conditional guard — the action only runs if the named tests pass. Can reference data from any prior action by name. Does not replace the run condition; it layers on top of it.

- name: HandleAdminAction
run_when_succeeded: [FetchUser]
run_on_assertion:
tests:
- action: FetchUser
value: role
is_equal_to: admin

previous keyword

Shorthand for the immediately preceding action in the list.

- name: Step1
...
- name: Step2
run_when_succeeded: [previous] # = run_when_succeeded: [Step1]

Transforms

post_transforms apply sequentially to an action's result after it completes. They are applied in order — each transform receives the output of the previous one.

extract_value

Extract a field or array element from the result.

post_transforms:
- extract_value: body # extract the 'body' field
- extract_value: "[0]" # extract first array element
- extract_value: "choices[0].message.content" # nested path (if supported)

extract_with_regex

Extract named capture groups from a string field.

post_transforms:
- extract_with_regex:
value: stripe-signature # the field to parse
expr: 't=(\d+).*?v1=([a-f0-9]+)'
keys: [timestamp, v1_signature] # names for capture groups

filter

Filter an array by field conditions.

post_transforms:
- filter:
target: $
conditions:
$.title:
contains: "search term" # substring match
$.status: "active" # exact equality match

rename_attributes

Rename fields. Use $[*].old for all elements in an array, $.old for a single object.

post_transforms:
- rename_attributes:
$[*].name: fullName # array
$[*].email: contactEmail
$.username: user # object

remove_attributes

Delete specific fields.

post_transforms:
- remove_attributes:
- $[*].password
- $[*].internalId
- $.secretKey

keep_attributes

Remove everything except the listed fields.

post_transforms:
- keep_attributes:
- $[*].id
- $[*].name
- $[*].email

nested_transforms

Apply transforms inside a nested field.

post_transforms:
- nested_transforms:
$[*].address:
- remove_attributes:
- $.geo
- $.zipcode

bcrypt

Hash a field in-place using bcrypt (for password storage).

post_transforms:
- bcrypt:
value: password
cost: 12 # bcrypt cost factor (default: 10)

Metrics

emit_metric publishes a Prometheus metric, visible at /metrics.

# Counter — increments by 1 on each emit
- name: TrackRequest
hide_data_on_success: true
emit_metric:
name: app_requests_total
type: counter
labels:
endpoint: users
source: api

# Gauge — set to an explicit value on each emit
- name: TrackUserCount
hide_data_on_success: true
emit_metric:
name: app_active_users
type: gauge
value: a|CountUsers::total|
labels:
region: us-east

hide_data_on_success and hide_data_on_error

Use these flags to prevent intermediate action results from appearing in the final API response or in logs.

- name: HashPassword
hide_data_on_success: true # result not returned in response or shown in logs
hide_data_on_error: true # error details (including input) not logged — use for secrets

- name: StoreEvent
hide_data_on_success: true # side-effect only — suppress from response

hide_data_on_error: true is important for actions that handle secrets or PII — it prevents sensitive input from appearing in error logs.


Database patterns

Basic query with params

- name: GetUser
database: main
query: |
SELECT id, email, username FROM users WHERE id = $1;
params:
- a|ValidateBody::id|
post_transforms:
- extract_value: "[0]"

Assert row count

- name: GetUser
database: main
query: |
SELECT id, email FROM users WHERE id = $1;
params:
- a|ValidateBody::id|
assert:
http_code_on_error: 404
error_message: "User not found"
tests:
- value: count()
is_equal_to: 1
post_transforms:
- extract_value: "[0]"

Idempotent insert

- name: StoreEvent
database: main
query: |
INSERT INTO events (external_id, payload)
VALUES ($1, $2::jsonb)
ON CONFLICT (external_id) DO NOTHING
RETURNING id;
params:
- a|ParseBody::id|
- a|ParseBody|

Optional filter params (JSONB pattern)

# Pass the whole body as $1, use ->> to access fields
# Missing JSON keys return genuine SQL NULL — IS NULL checks work correctly
- name: SearchRecords
database: main
query: |
SELECT id, name, status FROM records
WHERE
($1::jsonb->>'status' IS NULL OR status = $1::jsonb->>'status')
AND ($1::jsonb->>'name' IS NULL OR name ILIKE '%' || $1::jsonb->>'name' || '%')
ORDER BY created_at DESC
LIMIT 100;
params:
- a|ReadFilters|

Notes

  • The [previous] keyword in run conditions is shorthand for the immediately preceding action in the action list.
  • run_on_assertion can reference any prior action by name — it is not limited to the immediately preceding action.
  • at_least: N in run_when_succeeded and run_when_failed sets the minimum number of listed actions that must satisfy the condition. Omitting it requires all listed actions to satisfy it.
  • Transforms are applied in the order they are listed. Each transform receives the output of the previous one.
  • count() in assertions counts rows from database queries or items in an array response.

Configuration

config.yml

name: AirPipeFundamentals
description: >
Reference config covering every core Air Pipe concept: input sources,
interpolation, assertions, run conditions, transforms, and metrics.
Not a production pack — use it as a learning reference and to seed
the RAG chatbot with authoritative Air Pipe syntax.

docs: true

# No managed variables required — all examples call public APIs or are self-contained.

interfaces:

# ── Input sources ─────────────────────────────────────────────────────────
#
# Air Pipe actions receive input from five sources:
#
# a|body| — parsed JSON request body (POST/PUT/PATCH)
# a|params| — URL query parameters (?key=value)
# a|headers| — HTTP request headers (keys are lowercased)
# a|request| — full request envelope: { method, path, headers, params, body }
# a|raw_body| — raw request body as a string (needed for HMAC verification)
#
# Set the input on an action with: input: a|body|
# Access a specific field with: a|ActionName::fieldName|
# Access a nested field with: a|ActionName::outer.inner|

# GET /fundamentals/input/params?userId=1&format=json
# Demonstrates a|params| for query string access.
fundamentals/input/params:
output: http
summary: Query param access
description: Access URL query parameters with a|params|. GET /fundamentals/input/params?userId=1
tags: [fundamentals, input]
response_example:
userId: "1"
format: "json"

actions:
- name: ReadParams
input: a|params|
assert:
http_code_on_error: 400
tests:
- value: userId
is_not_null: true
description: "userId query param is required"

- name: FetchUser
run_when_succeeded: [ReadParams]
http:
url: https://jsonplaceholder.typicode.com/users/a|ReadParams::userId|
headers:
content-type: application/json
post_transforms:
- extract_value: body

# POST /fundamentals/input/body
# Demonstrates a|body|, field access, and hide_data_on_success.
fundamentals/input/body:
output: http
method: POST
summary: Body + header access
description: Access parsed JSON body and request headers from a single action.
tags: [fundamentals, input]
request_example:
title: Hello
userId: 1

actions:
# input: a|body| captures the parsed JSON body.
# input: a|headers| would capture request headers.
# input: a|request| captures the full envelope (method, path, headers, params, body).
- name: ReadBody
input: a|body|
hide_data_on_success: true
assert:
http_code_on_error: 400
tests:
- value: title
is_not_null: true
- value: userId
is_not_null: true

- name: CreatePost
run_when_succeeded:
actions: [ReadBody]
http_code_on_error: 400
http:
url: https://jsonplaceholder.typicode.com/posts
method: POST
headers:
content-type: application/json
body: |
{
"title": a|double_quote(ReadBody::title)|,
"body": "Created via Air Pipe fundamentals",
"userId": a|ReadBody::userId|
}
post_transforms:
- extract_value: body

# ── Interpolation ──────────────────────────────────────────────────────────
#
# Interpolation syntax: a|<source>| or a|<source>::<field>|
#
# a|ap_var::NAME| — managed variable (encrypted, never logged)
# a|env::NAME| — environment variable (self-hosted only)
# a|ActionName::field| — field from a previous action's result
# a|ActionName::outer.inner| — nested field via dot-path
# a|double_quote(X)| — interpolate X and wrap in JSON double-quotes
# Use inside JSON body strings to avoid injection.
# a|timestamp:s| — current Unix timestamp (seconds)
# a|timestamp:date| — current date as YYYY-MM-DD
# a|raw_body| — raw unparsed request body string
#
# IMPORTANT: When a field is absent from the source, the interpolation token
# is passed as a literal string — it does NOT resolve to null.
# For optional parameters in SQL, use the JSONB pattern:
# $1::jsonb->>'field' IS NULL (missing JSON keys return genuine SQL NULL)

# GET /fundamentals/interpolation/demo
# Shows all common interpolation forms in one place.
fundamentals/interpolation/demo:
output: http
summary: Interpolation reference
description: Demonstrates every interpolation form available in Air Pipe configs.
tags: [fundamentals, interpolation]

actions:
- name: FetchPost
http:
url: https://jsonplaceholder.typicode.com/posts/1
post_transforms:
- extract_value: body

- name: FetchUser
run_when_succeeded: [FetchPost]
http:
# a|ActionName::field| — use a field from FetchPost's result
url: https://jsonplaceholder.typicode.com/users/a|FetchPost::userId|
post_transforms:
- extract_value: body

- name: BuildSummary
run_when_succeeded: [FetchUser]
http:
url: https://jsonplaceholder.typicode.com/posts
method: POST
headers:
content-type: application/json
body: |
{
"postTitle": a|double_quote(FetchPost::title)|,
"authorName": a|double_quote(FetchUser::name)|,
"authorEmail": a|double_quote(FetchUser::email)|,
"generatedAt": a|timestamp:s|
}
post_transforms:
- extract_value: body

# ── Assertions ────────────────────────────────────────────────────────────
#
# Assertions validate action results. If any test fails the action fails,
# and downstream actions using run_when_succeeded will not run.
#
# Available tests:
# is_not_null: true — field must be present and non-null
# is_null: true — field must be null or absent
# is_not_empty: true — field must not be empty string/array
# is_equal_to: <value> — strict equality
# is_not_equal_to: <value>
# is_greater_than: <number> — numeric comparison
# is_less_than: <number>
# regex: "^pattern$" — value must match the regex
# count() — number of rows/items in the result
# is_valid_hmac: — HMAC signature verification
# secret: <value>
# body: <signed-string>
# algorithm: sha256
#
# Error control:
# http_code_on_error: 400 — HTTP status code returned on assertion failure
# error_message: "..." — message returned on failure
# description: "..." — field-level description shown in error context

# POST /fundamentals/assertions/demo
# Shows every assertion type with realistic examples.
fundamentals/assertions/demo:
output: http
method: POST
summary: Assertions reference
description: Demonstrates every assertion type — field validation, regex, count, and HMAC.
tags: [fundamentals, assertions]
request_example:
email: [email protected]
age: 25
role: admin

actions:
- name: ValidateFields
input: a|body|
hide_data_on_success: true
assert:
http_code_on_error: 400
tests:
# Null / presence checks
- value: email
is_not_null: true
is_not_empty: true
description: "Email address is required"

# Regex — enforce email format
- value: email
regex: "^[^@]+@[^@]+\\.[^@]+$"
description: "Must be a valid email address"

# Numeric comparison
- value: age
is_not_null: true
is_greater_than: 0
description: "Age must be a positive number"

# Equality
- value: role
is_equal_to: admin
description: "Only admin role is accepted by this endpoint"

- name: FetchUsers
run_when_succeeded: [ValidateFields]
http:
url: https://jsonplaceholder.typicode.com/users
post_transforms:
- extract_value: body

- name: AssertCount
run_when_succeeded: [FetchUsers]
input: a|FetchUsers|
assert:
http_code_on_error: 502
error_message: "Expected exactly 10 users from upstream"
tests:
# count() checks the number of rows/items in the current result
- value: count()
is_equal_to: 10

# ── Run conditions ────────────────────────────────────────────────────────
#
# Run conditions control when an action executes relative to prior actions.
#
# run_when_succeeded: [ActionA, ActionB]
# — runs only if ALL listed actions succeeded.
# Use http_code_on_error to set the response code if this chain fails.
#
# run_when_failed: [ActionA]
# — runs only if ALL listed actions failed. Useful for fallbacks.
#
# run_when_finished: [ActionA]
# — runs after ActionA completes, regardless of success or failure.
#
# run_when_succeeded:
# at_least: 2
# actions: [A, B, C, D]
# — runs if AT LEAST N of the listed actions succeeded.
#
# run_on_assertion:
# — conditional guard: action only runs if the listed tests pass.
# Can reference data from any prior action by name.
#
# Special keyword: [previous]
# Shorthand for the immediately preceding action in the list.

# POST /fundamentals/run-conditions/demo
fundamentals/run-conditions/demo:
output: http
method: POST
summary: Run conditions reference
description: >
Demonstrates all run condition types: run_when_succeeded, run_when_failed,
run_when_finished, at_least, and run_on_assertion.
tags: [fundamentals, workflow]
request_example:
userId: 1

actions:
- name: ValidateBody
input: a|body|
hide_data_on_success: true
assert:
tests:
- value: userId
is_not_null: true

# run_when_succeeded — only runs if ValidateBody passed.
- name: FetchUser
run_when_succeeded:
actions: [ValidateBody]
http_code_on_error: 400
http:
url: https://jsonplaceholder.typicode.com/users/a|ValidateBody::userId|
post_transforms:
- extract_value: body

# run_when_failed — runs as a fallback if FetchUser failed.
- name: FallbackUser
run_when_failed: [FetchUser]
http:
url: https://jsonplaceholder.typicode.com/users/1
post_transforms:
- extract_value: body

# run_when_finished — runs after FetchUser regardless of success or failure.
# Good for cleanup, logging, or guaranteed side-effects.
- name: FetchUserPosts
run_when_finished: [FetchUser]
http:
url: https://jsonplaceholder.typicode.com/posts?userId=a|ValidateBody::userId|
post_transforms:
- extract_value: body

# run_on_assertion — conditional guard. Only runs if the named action's
# field passes the test. Does not affect the parent run condition.
- name: FetchAdminData
run_when_succeeded: [FetchUser]
run_on_assertion:
tests:
- action: FetchUser
value: id
is_equal_to: 1
http:
url: https://jsonplaceholder.typicode.com/users
post_transforms:
- extract_value: body

# ── Transforms ────────────────────────────────────────────────────────────
#
# post_transforms apply sequentially to the action's result after it completes.
#
# extract_value: "[0]" — extract first array element
# extract_value: fieldName — extract a field by name
# extract_value: "field.nested" — extract a nested field
#
# extract_with_regex: — extract capture groups into named keys
# value: fieldName
# expr: 't=(\d+).*?v1=([a-f0-9]+)'
# keys: [timestamp, signature]
#
# filter: — filter array by field conditions
# target: $
# conditions:
# $.fieldName:
# contains: "search term" — substring match
# $.otherField: "exact match" — equality match
#
# rename_attributes: — rename keys (array or object)
# $[*].oldName: newName — across all array elements
# $.oldName: newName — on a single object
#
# remove_attributes: — delete keys from the result
# - $[*].sensitiveField
# - $.internalId
#
# keep_attributes: — remove everything EXCEPT listed keys
# - $[*].id
# - $[*].name
#
# nested_transforms: — apply transforms inside a nested field
# $[*].address:
# - remove_attributes:
# - $.geo
#
# bcrypt: — hash a field in-place
# value: password
# cost: 12

# GET /fundamentals/transforms/demo
fundamentals/transforms/demo:
output: http
summary: Transforms reference
description: >
Demonstrates extract_value, filter, rename_attributes, remove_attributes,
keep_attributes, and nested_transforms — applied to a real API response.
tags: [fundamentals, transforms]
response_example:
- fullName: Leanne Graham
contactEmail: [email protected]
city: Gwenborough

actions:
- name: FetchUsers
http:
url: https://jsonplaceholder.typicode.com/users
post_transforms:
# 1. Extract the response body from the HTTP envelope
- extract_value: body

# 2. Rename: name → fullName, email → contactEmail across all elements
- rename_attributes:
$[*].name: fullName
$[*].email: contactEmail

# 3. Remove fields we don't want in the response
- remove_attributes:
- $[*].phone
- $[*].website
- $[*].company

# 4. Clean up the nested address object — remove geo + suite
- nested_transforms:
$[*].address:
- remove_attributes:
- $.geo
- $.suite
- $.zipcode

# 5. Keep only the fields we want in the final output
- keep_attributes:
- $[*].fullName
- $[*].contactEmail
- $[*].address.city

# GET /fundamentals/transforms/filter
# Demonstrates the filter transform with query params as the search input.
fundamentals/transforms/filter:
output: http
summary: Filter transform
description: Filter an array by field value. GET /fundamentals/transforms/filter?title=sunt
tags: [fundamentals, transforms]

actions:
- name: ReadParams
input: a|params|
assert:
http_code_on_error: 400
tests:
- value: title
is_not_null: true

- name: FetchAndFilter
run_when_succeeded: [ReadParams]
http:
url: https://jsonplaceholder.typicode.com/posts
post_transforms:
- extract_value: body
- filter:
target: $
conditions:
$.title:
contains: a|ReadParams::title|

# ── Metrics ───────────────────────────────────────────────────────────────
#
# emit_metric publishes a Prometheus metric. Metrics are available at /metrics.
#
# name: metric_name — metric name (snake_case)
# type: counter — counter (monotonically increasing)
# type: gauge — gauge (can go up or down)
# value: a|Action::field| — for gauges, the current value
# labels: — optional key/value dimensions
# key: value
#
# Counters are auto-incremented on each emit.
# Gauges require an explicit value.

# GET /fundamentals/metrics/demo
fundamentals/metrics/demo:
output: http
summary: Metrics reference
description: Demonstrates emit_metric for both counter and gauge types.
tags: [fundamentals, metrics]

actions:
- name: FetchUsers
http:
url: https://jsonplaceholder.typicode.com/users
post_transforms:
- extract_value: body

# Counter — increments by 1 on each request.
# Visible at /metrics as: app_users_listed_total{interface="fundamentals/metrics/demo"}
- name: TrackListRequest
run_when_succeeded: [FetchUsers]
hide_data_on_success: true
emit_metric:
name: app_users_listed_total
type: counter
labels:
source: api

# Gauge — set to the current value on each request.
# Useful for tracking sizes, counts, or any value that can decrease.
- name: TrackUserCount
run_when_succeeded: [FetchUsers]
hide_data_on_success: true
emit_metric:
name: app_users_gauge
type: gauge
value: a|FetchUsers::count()|