Skip to main content

Actions & workflow control

Actions are the steps an interface runs when called. Each action does one thing — fetch data or transform it — and actions can be ordered, retried, and run conditionally on each other's results.

An action's data comes from an input: selector (request data or a previous action's output), or from a fetch: http, database, command, email, or a lookup to fan out over an array. Then validate with assert, reshape with post_transforms, and order with run_when_succeeded / run_when_failed / depends_on.

actions:
- name: FetchUser
http:
url: https://api.example.com/users/a|params::id|
- name: Notify
run_when_succeeded: [FetchUser]
http:
url: https://hooks.example.com/notify
body: { email: a|FetchUser::email| }

Reference a previous action's output with a|ActionName::field| — see Interpolation. All action fields follow.

What retry repeats

retry only repeats failures that could plausibly succeed on another attempt. By default that is 408, 429, 500, 502, 503, 504 and transport failures (connection refused, DNS, timeout). A 400 or 401 is attempted once: repeating it cannot change the answer, and at scale it turns a partner's bad day into a retry storm.

- name: Push
retry:
attempts: 3
delay: 100
exponential_backoff: true # 100ms, 200ms, 400ms...
max_delay: 30000 # ...capped
http: {url: https://api.example.com/x, method: POST}

The default set is the intersection of what Polly, the AWS and Azure SDKs and urllib3 all treat as transient. It deliberately excludes 501 and 505, which are permanent — proxies blanket all of 5xx because they cannot know better, but on: [5xx] is available if you want that.

FieldEffect
onreplace the default set — codes (429), classes (5xx), timeout, connect, any
exceptsubtract from whatever on resolves to, including the default
respect_retry_afterhonour a Retry-After response header over the computed backoff (default true, still bounded by max_delay)
jitterfull (default) or none
max_delayupper bound on a single wait, in milliseconds

Jitter is on by default because without it every client that failed together retries together, and a recovering upstream is hit by the same synchronised wave it just fell over to.

Two narrowings worth knowing:

  • retry.on: [connect] retries only failures that provably never reached the server — the strictest safe setting for a non-idempotent endpoint whose provider has no idempotency-key support. A timeout after the request was sent leaves the outcome unknown.
  • Classification applies only when an unexpected status is why the action failed. An action that returned a fine status and failed its assert — polling until a record appears — is retried as it always was.

Retrying anything that is not an HTTP call

A database, command, email or state action has no status code to classify, so every failure of one is retryable — a query error, a failed assert, a non-zero expect_exit. That is unchanged, and it is what makes the common shapes work:

# retry until the row shows up
- name: WaitForRow
database: main
query: SELECT id FROM orders WHERE external_id = $1;
params: [a|body::externalId|]
retry: {attempts: 5, delay: 200, exponential_backoff: true}
assert:
tests:
- value: count()
is_greater_than: 0

Two things follow from that:

  • on / except match statuses and transport failures, so on a non-http action they match nothing and the filter is inert. The config-load lint warns if you set one there. To narrow what counts as a failure on these actions, use assert (or expect_exit for a command) — that is the knob with meaning here.
  • delay is an upper bound, not a fixed wait, because jitter applies to every action type: delay: 500 sleeps somewhere in [0, 500). Set jitter: none if you need the exact curve (a test asserting on elapsed time, say).

Action

FieldTypeDescription
replayableboolean (nullable)Whether a captured run containing this action may be replayed. Replay re-runs an action for real, so an action with a side effect the world can see — charging a card, sending… Example ↓
timeoutanyMaximum execution time for this action in milliseconds. Example ↓
inputstring (nullable)Where this action's data comes from. Common sources: - a|body| - HTTP request body - a|params| - URL parameters - a|action_name::field| - output from an earlier action Example ↓
input_fallbacksArray<string> (nullable)Alternative input sources to try if the primary input is not available. Example ↓
conditional_inputConditionalInput (nullable)Define conditional input based on runtime conditions. Uses Simple (list of input strings) or Complex (with tests). Example ↓
pre_logArray<LogMessage> (nullable)Log messages before action execution. Example ↓
post_logArray<LogMessage> (nullable)Log messages after successful action completion. Example ↓
error_logArray<LogMessage> (nullable)Log messages when action fails. Example ↓
success_logArray<LogMessage> (nullable)Log messages when action succeeds. Example ↓
retry_logArray<LogMessage> (nullable)Log messages when action retry is attempted. Example ↓
retryRetry (nullable)Configure automatic retry behavior. Example ↓
lookupstring (nullable)Name of a lookup configuration to use for pre-fetching data. Example ↓
item_timeoutanyPer-item execution timeout for lookup iterations. Controls how long each individual item in the lookup array is allowed to run. Use timeout to cap the total loop duration. Example ↓
lookup_concurrencynumber (nullable)Maximum number of lookup items to execute concurrently. Defaults to 10. Capped at 50 in managed run modes. Example ↓
lookup_partitionboolean (nullable)Separate per-item successes from failures in the lookup output. When true, the action returns `{ "succeeded": [...item data...], "failed": [{ item, http_code?, error?, data? }]… Example ↓
lookup_inherit_jqMap<string, string> (nullable)JQ transformations to apply to lookup results. Example ↓
lookup_inheritMap<string, string> (nullable)Fields to inherit from lookup results. Example ↓
actionsArray<Action> (nullable)Nested actions for lookup. Example ↓
json_outputanyCapture specific JSON values from output. Example ↓
commandCommandRun (nullable)Execute a system command. Example ↓
output_stdoutboolean (nullable)Include command stdout in output. Example ↓
httpHttpRequest (nullable)HTTP request configuration. Example ↓
databasestring (nullable)Database name for SQL operations. Example ↓
googleGoogle (nullable)Google API integration. Example ↓
awsAws (nullable)AWS service integration. Example ↓
stateStateAction (nullable)Persistent state operation (durable key/value): polling cursors, dedupe sets, idempotency keys, counters. Backed by a pluggable backend (in-memory for local single-node,… Example ↓
sftpSftp (nullable)Move files over SFTP: list a directory, fetch a file, write one, move or delete it. Example ↓
only_newOnlyNew (nullable)Keep only the records this config has not already handled. Polling is the shape behind most "trigger" integrations — fetch a page on a schedule, act on what is new, remember… Example ↓
discoverDiscover (nullable)Discover the live members of a service — container endpoints, pods, replicas — as an array, so a later action can fan out over them with lookup: instead of a hard-coded… Example ↓
delayanyPause before continuing. Accepts a duration string ("2s", "500ms", "1m", "1d") or a number of milliseconds. Short and long delays behave differently, and the difference… Example ↓
wait_for_callbackWaitForCallback (nullable)Suspend the run until something outside calls back — an approval, a signature, a third-party webhook saying the job is done. Unlike delay, which waits for a time you chose,… Example ↓
emailEmail (nullable)Email sending action. Example ↓
emit_metricEmitMetric (nullable)Emit a Prometheus metric as part of this action step. Requires expose_metrics: true on the parent config. Example ↓
ws_publishWsPublish (nullable)Publish a payload to realtime WebSocket channels (server push / fan-out).
mqtt_publishMqttPublish (nullable)Publish a payload to MQTT topics (pipeline → topic, cross-node fan-out).
agentAgentAction (nullable)Run a tool-calling AI agent loop: ask a model, run whatever tools it asks for, feed the results back, repeat until it answers or the iteration budget runs out. The tools are… Example ↓
actionstring (nullable)Action identifier for referencing outputs. Example ↓
depends_onRunCondition (nullable)Run this action when specified actions complete. Can be a list of action names or a RunCondition config. Example ↓
run_when_succeededRunCondition (nullable)Run this action when specified actions succeed. Example ↓
run_when_failedRunCondition (nullable)Run this action when specified actions fail. Example ↓
run_on_assertionAssert (nullable)Run this action based on assertion results. Example ↓
paramsArray<any> (nullable)Parameters for SQL queries. Example ↓
post_transformsArray<Transform> (nullable)Transformations to apply to action output. Example ↓
namestring (nullable)Action identifier/name. Example ↓
descriptionstring (nullable)Description of the action. Example ↓
querystring (nullable)SQL query for database actions. Example ↓
multiboolean (nullable)Run this action's query as a multi-statement batch (Postgres only). The native driver prepares every query, and a prepared statement can hold only one command — so a queryExample ↓
urlstring (nullable)URL for HTTP actions. Example ↓
conn_stringstring (nullable)Database connection string override. Example ↓
outputstring (nullable)Output destination handler. Example ↓
assertAssert (nullable)Validation and response configuration. Example ↓
document_operationDocumentOperation (nullable)Document database operations. Example ↓
hide_actionboolean (nullable)Hide entire action data. Example ↓
hide_data_on_successboolean (nullable)Hide action data on success. Example ↓
hide_data_on_errorboolean (nullable)Hide action data on error. Example ↓
hide_data_on_emptyboolean (nullable)Hide action data when empty. Example ↓
hide_errorsboolean (nullable)Hide error details. Example ↓
hide_metricsboolean (nullable)Disable metrics collection. Example ↓
response_on_successActionResponse (nullable)Custom response on success. Example ↓
response_on_errorActionResponse (nullable)Custom response on error. Example ↓

Field examples

replayable

Whether a captured run containing this action may be replayed.

Replay re-runs an action for real, so an action with a side effect the world can see — charging a card, sending an email, publishing to a topic — must say so. Any action marked replayable: false makes the whole run inspect-only, and the run records which action did it so the answer is "ChargeCard cannot be replayed" rather than a disabled button with no explanation.

Declared rather than inferred, in the same spirit as expect_status: guessing which actions are safe to repeat is exactly the judgement that should not be automatic. n8n simply re-runs, and it bites people.

Example

- name: ChargeCard
replayable: false

timeout

Maximum execution time for this action in milliseconds.

Example

timeout: 5000    # 5 seconds

input

Where this action's data comes from. Common sources:

  • a|body| - HTTP request body
  • a|params| - URL parameters
  • a|action_name::field| - output from an earlier action

Example

input: a|body|

Example

input: a|LoginBody::email|

Closing pipe

input accepts the bare form too — a|body|, with no trailing pipe — because this field names a source directly rather than interpolating one into a larger string. It is the only field where that is true.

Everywhere else the closing pipe is required and its absence is silent. url: https://x/a|body| does not resolve to anything; it sends the literal text a|body|, because the scanner that finds references only matches a closed marker. Nothing warns, and the request goes out wrong.

Write a|body| everywhere, including here. The bare form is kept for configs that already use it and is not offered in new examples.

input_fallbacks

Alternative input sources to try if the primary input is not available.

Example

input_fallbacks:
- a|header::Authorization|
- a|params::token|

conditional_input

Define conditional input based on runtime conditions. Uses Simple (list of input strings) or Complex (with tests).

Example - Simple

conditional_input:
- a|body::premium| # Try this first
- a|body::standard| # Fallback

Example - Complex

conditional_input:
- input: a|body::type|
tests:
- is_equal_to: "premium"

pre_log

Log messages before action execution.

Example

pre_log:
- msg: "Starting validation"
level: info

post_log

Log messages after successful action completion.

Example

post_log:
- msg: "Validation succeeded"
level: info

error_log

Log messages when action fails.

Example

error_log:
- msg: "Validation failed"
level: error

success_log

Log messages when action succeeds.

Example

success_log:
- msg: "Email sent"
level: info

retry_log

Log messages when action retry is attempted.

Example

retry_log:
- msg: "Retrying..."
level: warn

retry

Configure automatic retry behavior.

Example

retry:
attempts: 3 # Number of retry attempts
delay: 1000 # Delay between retries (ms)
exponential_backoff: true

lookup

Name of a lookup configuration to use for pre-fetching data.

Example

lookup: user_lookup

item_timeout

Per-item execution timeout for lookup iterations. Controls how long each individual item in the lookup array is allowed to run. Use timeout to cap the total loop duration.

Example

item_timeout: 30s

lookup_concurrency

Maximum number of lookup items to execute concurrently. Defaults to 10. Capped at 50 in managed run modes.

Example

lookup_concurrency: 20

lookup_partition

Separate per-item successes from failures in the lookup output. When true, the action returns { "succeeded": [...item data...], "failed": [{ item, http_code?, error?, data? }] } instead of a flat array, so a poller can advance its cursor / mark items seen only for successes and let failed items be retried on the next run. Failed items are those whose iteration returned an HTTP code >= 400 or timed out.

Example

lookup_partition: true

lookup_inherit_jq

JQ transformations to apply to lookup results.

Example

lookup_inherit_jq:
token: .result.token

lookup_inherit

Fields to inherit from lookup results.

Example

lookup_inherit:
api_key: lookup.api_key

actions

Nested actions for lookup.

Example

actions:
- name: GetToken
http:
url: https://auth.example.com

json_output

Capture specific JSON values from output.

Example

json_output:
user_id: .data.id

command

Execute a system command.

Example

command:
run: "echo hello"
shell: bash

output_stdout

Include command stdout in output.

Example

output_stdout: true

http

HTTP request configuration.

Example

http:
url: https://api.example.com
method: POST
headers:
Content-Type: application/json
body:
key: value

database

Database name for SQL operations.

Example

database: main
query: SELECT * FROM users

google

Google API integration.

Example

google:
credential: my_google_cred
get_signed_upload_url:
bucket: my-bucket
key: file.txt

aws

AWS service integration.

Example

aws:
credential: my_aws_cred
get_signed_upload_url:
bucket: my-bucket
key: file.txt

state

Persistent state operation (durable key/value): polling cursors, dedupe sets, idempotency keys, counters. Backed by a pluggable backend (in-memory for local single-node, Postgres for durable/shared, the AirPipe backend in managed mode). Read state inline with a|state::KEY|.

Example

state:
advance:
key: last_seen
value: a|Fetch::max_updated_at|

sftp

Move files over SFTP: list a directory, fetch a file, write one, move or delete it.

Example

sftp:
host: files.example.com
user: airpipe
private_key: a|secret::SFTP_KEY|
host_key: "SHA256:kW8xDm2YuGv9NsLzalFWnMJ3j28XTnQJzw7/5GkE9+w"
operation: list
path: /incoming

only_new

Keep only the records this config has not already handled.

Polling is the shape behind most "trigger" integrations — fetch a page on a schedule, act on what is new, remember what you acted on. Every piece was already here (schedule, http, state.seen), but assembling them meant iterating the page with lookup, asking about each record separately and gating on the answer: correct, verbose, and one state round trip per record.

Example

- name: NewOrders
input: a|Fetch->orders|
only_new:
id: .id
key: orders
ttl: 30d

discover

Discover the live members of a service — container endpoints, pods, replicas — as an array, so a later action can fan out over them with lookup: instead of a hard-coded address list.

Example

- name: Members
discover:
kubernetes:
label_selector: app=api
port_name: http
- name: PollAll
lookup: a|Members|
actions:
- name: Health
http:
url: a|body::url|/healthz

delay

Pause before continuing. Accepts a duration string ("2s", "500ms", "1m", "1d") or a number of milliseconds.

Short and long delays behave differently, and the difference matters. Up to AIRPIPE__MAX_DELAY_SECS (default 300s) the run simply sleeps — pacing between steps. Past that a sleep would be the wrong instrument, so the run is suspended instead: what has completed is written to a durable store, the caller is answered 202 with a run id, and the scheduler resumes the rest when it comes due. Nothing is held open and a restart does not lose the run.

Suspending needs somewhere to write. Managed has one; self-hosted needs AIRPIPE__DATABASE_URL. Without it a long delay is refused rather than quietly shortened into a sleep a restart would drop.

A delay wakes once, at the time you asked for. If you are waiting on work that might not be finished by then, poll for it instead — an assert that fails until the job reports done, plus a retry, which suspends between attempts in the same way.

Example

# sleeps in process
delay: "2s"

# suspends the run and resumes it tomorrow
delay: "1d"

wait_for_callback

Suspend the run until something outside calls back — an approval, a signature, a third-party webhook saying the job is done.

Unlike delay, which waits for a time you chose, this waits for an event you cannot predict. The run is parked in the durable store and the caller is answered 202; when POST /_ap/resume/<token> arrives the run continues from the next action, and whatever was posted becomes this action's output.

The token is yours to choose, and must be unguessable — anyone holding it can resume the run. Mint one with a|uuid| in an earlier action and build the link from it:

Example

- name: Ticket
json_output: '{"token": "a|uuid|"}'

- name: AskApprover
email:
to: approvals@example.com
subject: Approve this refund
body: 'Approve: https://api.example.com/_ap/resume/a|Ticket::token|'

- name: Approval
wait_for_callback:
token: a|Ticket::token|
timeout: "7d"

- name: Refund
run_when_succeeded: [Approval]
http:
url: https://api.example.com/refunds
body: '{"approved_by": "a|Approval::approver|"}'

email

Email sending action.

Example

email:
from: noreply@example.com
to: user@example.com
subject: Hello
text: Body text

emit_metric

Emit a Prometheus metric as part of this action step. Requires expose_metrics: true on the parent config.

Example

- name: TrackRevenue
emit_metric:
name: revenue_total
type: gauge
value: a|OrderAction::amount|
labels:
tier: a|OrderAction::tier|

agent

Run a tool-calling AI agent loop: ask a model, run whatever tools it asks for, feed the results back, repeat until it answers or the iteration budget runs out.

The tools are this config's own interfaces. Their JSON Schemas come from the asserts those interfaces already declare, so a tool is documented, validated and callable over HTTP and MCP by the same definition — there is no second place to describe it, and nothing to keep in step.

Example

- name: Assistant
agent:
url: https://api.openai.com/v1/chat/completions
model: gpt-4o-mini
headers:
authorization: "Bearer a|ap_var::OPENAI_API_KEY|"
system: "You help staff answer order questions."
input: a|body::messages|
tools: [orders/search, orders/refund]
max_iterations: 6

action

Action identifier for referencing outputs.

Example

action: ValidateUser
# Reference later: a|ValidateUser::result|

depends_on

Run this action when specified actions complete. Can be a list of action names or a RunCondition config.

Example - Simple list

depends_on:
- LoginBody
- InputValidation

Example - Config with at_least

depends_on:
at_least: 1
actions:
- OptionalStep1
- OptionalStep2

run_when_succeeded

Run this action when specified actions succeed.

Example

run_when_succeeded:
- PreviousAction

run_when_failed

Run this action when specified actions fail.

Example

run_when_failed:
- MainAction

run_on_assertion

Run this action based on assertion results.

Example

run_on_assertion:
tests:
- jq: .valid
is_equal_to: true

params

Parameters for SQL queries.

Example

params:
- a|body::user_id|
- "pending"

post_transforms

Transformations to apply to action output.

Example

post_transforms:
- extract_with_jq: ".[0]"

name

Action identifier/name.

Example

name: GetUserDetails

description

Description of the action.

Example

description: "Fetches user details"

query

SQL query for database actions.

Example

query: SELECT * FROM users WHERE id = $1

multi

Run this action's query as a multi-statement batch (Postgres only).

The native driver prepares every query, and a prepared statement can hold only one command — so a query with several ;-separated statements (e.g. a schema/seed block) normally fails with SQLSTATE 42601. Set multi: true to run it via the simple protocol instead, which executes all statements. Only valid for queries with no params (a batch cannot be parameterized); it returns an empty result set.

Example

multi: true
query: |
CREATE TABLE IF NOT EXISTS a (id int);
CREATE TABLE IF NOT EXISTS b (id int);

url

URL for HTTP actions.

Example

url: https://api.example.com/users

conn_string

Database connection string override.

Example

conn_string: postgresql://user:pass@host:5432/db

output

Output destination handler.

Example

output: http

assert

Validation and response configuration.

Example

assert:
tests:
- jq: .status
is_equal_to: "success"
success_message: "OK"
error_message: "Failed"

document_operation

Document database operations.

Example

document_operation:
database: users_db
collection: profiles
operation: insertOne
insert:
name: test

hide_action

Hide entire action data.

Example

hide_action: true

hide_data_on_success

Hide action data on success.

Example

hide_data_on_success: true

hide_data_on_error

Hide action data on error.

Example

hide_data_on_error: true

hide_data_on_empty

Hide action data when empty.

Example

hide_data_on_empty: true

hide_errors

Hide error details.

Example

hide_errors: true

hide_metrics

Disable metrics collection.

Example

hide_metrics: true

response_on_success

Custom response on success.

Example

response_on_success:
http_code: 200
body:
status: success

response_on_error

Custom response on error.

Example

response_on_error:
http_code: 500
body:
status: error

ActionResponse

FieldTypeDescription
http_codenumber (nullable)
headersobject (nullable)
bodystring (nullable)

Retry

Try an action again when it fails.

Retry is also how you poll. An upstream job that is still running answers 200 with a status that is not "done" — a success as far as HTTP is concerned — so pair retry with an assert that fails until the job reports finished, and the action repeats until the answer changes rather than only after an error.

A poll is a series of short waits whose total is long, so once they add up past AIRPIPE__MAX_DELAY_SECS the run is suspended between attempts instead of sleeping through them: parked in the durable store, the request closed, resumed by the scheduler for the next attempt. A job that finishes quickly never touches the store; one that does not is polled for as long as attempts × delay allows without holding anything open. The budget is spent across resumes, not restarted by them, so a job that never finishes still stops.

Suspending needs a durable store — managed has one, self-hosted needs AIRPIPE__DATABASE_URL. Without one the waits simply happen in process as before.

Example

# poll a render every minute for up to 40 minutes, suspending between attempts
assert:
tests:
- value: body.status
is_equal_to: completed
retry:
attempts: 40
delay: 60000
FieldTypeDescription
attemptsnumberRequired. How many times to attempt the action in total, including the first try. 1 (the default) means no retry.
delaynumber (nullable)Milliseconds to wait between attempts. With exponential_backoff this is the base the delay grows from rather than a fixed pause.
exponential_backoffboolean (nullable)Double the wait after each failed attempt instead of using delay unchanged. Worth setting against an upstream that is rate limiting or overloaded, where retrying at a fixed…
onArray<StatusMatcher> (nullable)Which failures are worth repeating. Accepts codes (429), classes (5xx), any, and the transport classes timeout / connect. Unset uses the default set — `408, 429, 500,…
exceptArray<StatusMatcher> (nullable)Subtracted from whatever on resolves to, including the default set. Use it to carve an exception out without restating the whole list.
respect_retry_afterboolean (nullable)Honour a Retry-After response header when the upstream sends one (RFC 9110 §10.2.3), in preference to the computed backoff. Defaults to true: a server that tells you when to…
jitterJitter (nullable)Randomisation applied to the computed backoff. Defaults to full, per AWS's "Exponential Backoff and Jitter": without it, every client that failed together retries together,…
max_delaynumber (nullable)Upper bound on a single wait, in milliseconds. Without it, exponential backoff on a late attempt can schedule a wait longer than any caller is willing to hold for.

LogMessage

FieldTypeDescription
msgstringRequired.
levelstringRequired.
jsonboolean (nullable)

RunCondition

One of:

RunOnConfig

FieldTypeDescription
at_leastnumber (nullable)
actionsArray<string>Required.
http_code_on_errornumber (nullable)

ConditionalInput

One of:

ConditionalInputConfig

FieldTypeDescription
inputstringRequired.
testsArray<Test>Required.

AgentAction

FieldTypeDescription
urlstringRequired. An OpenAI-compatible chat-completions endpoint. Anything speaking that shape works: OpenAI, Azure OpenAI, Groq, Together, vLLM, Ollama, or a gateway of your own.
modelstringRequired. Model id, passed through verbatim.
headersMap<string, string> (nullable)Headers for the model call — where the API key goes.
systemstring (nullable)System prompt, prepended as the first message.
inputanyThe conversation. Either a plain string (becomes one user message) or an array of {role, content} objects — the shape a chat UI already holds.
toolsArray<string> (nullable)Interfaces in THIS config to offer as tools, by name.
mcpArray<AgentMcpServer> (nullable)Remote MCP servers to draw tools from, in addition to tools. Their tools are discovered at run time via tools/list, so the model is offered whatever the server currently…
max_iterationsnumber (nullable)How many model round trips the loop may take. Default 4, hard ceiling 12. This is the bound that makes an agent safe to expose: each iteration is a model call plus every tool…
memoryAgentMemory (nullable)Remember this conversation across requests. Without it an agent starts every request with an empty history: it cannot answer "and what about tomorrow?" because it never saw… Example ↓
temperaturenumber (nullable)Sampling temperature, passed through when set.
timeoutnumber (nullable)Seconds to allow each model call. Default 60.

Field examples

memory

Remember this conversation across requests.

Without it an agent starts every request with an empty history: it cannot answer "and what about tomorrow?" because it never saw yesterday's question. With it, the last exchanges for a session are replayed into the next call.

Example

memory:
key: a|body::session_id| # what makes one conversation distinct from another
window: 10 # how many past messages to replay
ttl: 24h # how long a silent conversation survives

AgentMcpServer

FieldTypeDescription
urlstringRequired. The server's JSON-RPC endpoint.
headersMap<string, string> (nullable)Headers for every call to it — where its credential goes.
onlyArray<string> (nullable)Offer only these of the server's tools. Absent means all of them. Worth setting deliberately: a remote server decides what it publishes, so without a list the model's abilities…

AgentMemory

Air Pipe interface definition. An interface represents an endpoint that can be invoked (via HTTP, schedule, etc.) and executes a chain of actions.

Example

myApiEndpoint:              # Interface name (can be used as route path)
summary: Get users # Short description for documentation
description: | # Long description (optional)
Retrieves a list of all
users in the organization.
method: POST # HTTP method (for HTTP interfaces)
route: /users # URL path (optional, defaults to interface name)
tags: # OpenAPI tags for grouping
- Users
- API
output: http # Output type: http

templates: # Interface-specific response templates
success_template: |
{ "status": "ok" }

actions: # Ordered list of actions to execute
- name: ValidateJwt # Action name (used for references)
input: a|headers| # Input source (a|headers|, a|body|, etc.)
- name: FetchData
database: main # Database connection to use, assuming global is defined
query: SELECT * FROM users WHERE org_uuid = $1
params:
- a|CheckBody::organization_uuid|

assert: # Final assertion against action results
tests:
- value: count()
is_equal_to: 1

response: # Response configuration
http_code_on_error: 400 # HTTP status code on error
http_code_inherit_error: [FetchData] # Inherit error code from action

Field defaults an interface hands to every action it contains.

The outcome of an action is DECLARED (expect_status / expect_exit), never inferred from an assert. Declaring it per action is right for a pipeline where each call has its own contract, and tedious for an interface where they share one — a test runner asserting on the status of thirty routes under test, a health-check fan-out that records whatever it gets. So the interface can say it once and each action may still override it.

Resolved at config load (see IntegrationConfig::compile_expressions), so the interpreter and the compiled fast path both see a plain action-level value and the request path pays nothing for it.

Example

interfaces:
tests/all:
defaults:
expect_status: any # the statuses ARE what this interface tests
actions: [...]

Conversation memory for an agent: action.

FieldTypeDescription
keystringRequired. What makes one conversation distinct from another — a session id, a chat id, a user id. Usually a marker: a|body::session_id|. There is no default on purpose. A shared…
windownumber (nullable)How many stored messages to replay. Defaults to 10, capped at 50. Every replayed message is sent to the model on every call, so a larger window means a larger prompt. Whether…
ttlstring (nullable)How long a conversation survives without activity, e.g. 24h, 7d. Defaults to 24h.
namespacestring (nullable)State namespace to store under. Defaults to the config name.