Interfaces
An interface is an endpoint. Add any number of them to a config under
interfaces: — each key is the interface name (and default route). When an
interface is triggered it runs its ordered list of actions.
interfaces:
prod/user-login:
summary: Log a user in
description: Verify credentials and return a token
tags: [auth]
output: http
method: POST
actions:
- name: CheckInput
- name: VerifyUser
- name: VerifyPassword
An interface can also run on a schedule, be exposed as an MCP tool, and carry a network policy.
Answering before the work finishes
By default an interface answers once every action has finished. Two fields change that, and they are independent of each other.
stream: forwards a streaming action's output onward as it arrives, instead of only
returning it once the action completes. Point from: at an action already reading a
stream (stream: true on its http:) and give it a channel; subscribers receive the
pieces live while the action is still running.
respond: early returns { accepted, request_id } with a 202 straight away and
finishes the run in the background. Use it when the work outlives what a caller — or a
proxy in front of you — is willing to wait for.
interfaces:
chat:
output: http
method: POST
respond: early # 202 now, keep working
stream:
from: AskModel # the action reading the stream
to: channel
channel: a|body::session_id| # subscribers watch this
actions:
- name: AskModel
http:
url: https://api.example.com/v1/chat/completions
method: POST
stream: true
idle_timeout: 30s
Together these are how a chat UI is built on Air Pipe: the browser posts, gets its
202 immediately, and renders tokens as they arrive on the channel.
Two things to know before using respond: early. The status code is committed before
the work runs, so http_code_on_error and any assert after that point can no longer
change what the caller sees — a later failure has to reach them another way, which is
what the channel is for. And detached runs are capped per organisation: over the limit
the request is refused with 429 rather than queued.
Deltas are batched (about every 50ms, or 200 characters) rather than published one per token. See WebSocket channels for subscribing.
All fields follow.
Interface
| Field | Type | Description |
|---|---|---|
capture | CaptureSetting (nullable) | How much of this interface's runs to record for execution history. Absent means off, which is also the fleet-wide default: capture is opt-in per interface, then bounded again… |
defaults | ActionDefaults (nullable) | Defaults handed to every action in this interface (see [ActionDefaults]). An action that sets the field itself always wins. |
summary | string (nullable) | |
description | string (nullable) | |
log | CustomLog (nullable) | |
tags | Array<string> (nullable) | |
templates | Map<string, string> (nullable) | |
mqtt | string (nullable) | MQTT trigger name. When set, this interface is triggered by an MQTT PUBLISH to the topic <environment>/<interface_name>/<this value> (carried over MQTT-over-WebSocket at… |
ws | string (nullable) | WebSocket marker. When set (to any value), this interface is reachable over WebSocket at /ws/<org>/<environment>/<interface_name>; each inbound JSON text frame runs the… |
respond | Respond (nullable) | Return the HTTP response WITHOUT waiting for the actions to finish. The run continues on the runtime exactly as it would have; only the response is early. The caller gets `{… |
stream | StreamOut (nullable) | Forward a streaming action's output onward as it arrives, instead of only returning it once the action finishes. The action named by from must already be reading a stream… Example ↓ |
subscribe_authorizer | string (nullable) | Realtime-channel access control. The name of an interface run to authorize a socket's ap_subscribe to a channel: it runs with { "channel": "<name>" } as a|body and the… |
public | boolean (nullable) | Public realtime opt-in. When true, this WebSocket interface accepts END-USER connections with NO credential at all — no org API key and no JWT — for non-sensitive public… |
schedule | Schedule (nullable) | |
silence | boolean (nullable) | |
method | string (nullable) | |
route | string (nullable) | |
show_error_detail | boolean (nullable) | |
actions | Array<Action> (nullable) | |
url | string (nullable) | |
conn_string | string (nullable) | |
query | string (nullable) | |
params | Map<string, Param> (nullable) | |
tests | Array<Test> (nullable) | |
assert | Assert (nullable) | |
output | InterfaceOutput (nullable) | |
response | InterfaceResponse (nullable) | |
disable_fastpath | boolean (nullable) | Opt this interface out of the compiled fast path (AP_FASTPATH), forcing every request through the interpreter. The fast path is byte-identical, so this never changes output —… |
accepts | InterfaceContract (nullable) | What this interface ACCEPTS: the shape of the payload its trigger delivers, and an example of it. Transport-neutral on purpose. An interface is not always an HTTP endpoint — it… |
produces | InterfaceContract (nullable) | What this interface PRODUCES: the shape of its result, and an example of it. For an HTTP interface that is the response body; for a schedule it is what the run yielded; for… |
request_example | any | Example request body shown in generated API documentation. DEPRECATED: use request: { example: … }. Still read so existing configs keep working; request.example wins when… |
response_example | any | Example successful response body shown in generated API documentation. DEPRECATED: use response: { example: … }. Still read so existing configs keep working;… |
notes | string (nullable) | Additional free-form documentation notes shown under the endpoint description. |
network | NetworkPolicy (nullable) | Network access-control policy for this interface (IP/geo/ASN/rate-limit). Inherits and tightens the config-wide network policy unless inherit: false. Evaluated before any… |
mcp | McpTool (nullable) | Expose this interface as an MCP tool (opt-in). When set with enabled: true, the interface is listed by the MCP server (tools/list) and callable via tools/call, reusing… |
Field examples
stream
Forward a streaming action's output onward as it arrives, instead of only returning it once the action finishes.
The action named by from must already be reading a stream (stream: true on its
http:); this says where its pieces GO. Without it the deltas are still reassembled
into the action's body exactly as before — they are simply not forwarded anywhere.
Example
stream:
from: AskModel
to: channel
channel: a|body::session_id|
InterfaceResponse
| Field | Type | Description |
|---|---|---|
http_code_on_error | number (nullable) | |
http_code_inherit_error | Array<string> (nullable) | |
headers | object (nullable) | |
custom_body | string (nullable) | |
http_code_fallback | number (nullable) | |
http_code_fallback_strategy | ErrorFallbackStrategy (nullable) | |
logs | Array<InterfaceLog> (nullable) |
InterfaceLog
| Field | Type | Description |
|---|---|---|
on | InterfaceLogOn | Required. |
database | string (nullable) |
InterfaceLogOn
Type: string — one of: error, success, any
InterfaceOutput
Type: string — one of: http, cli, none
ErrorFallbackStrategy
Type: string — one of: last_action, last_action_with_error, any_action
McpTool
Opt-in MCP tool exposure for an interface. See [Interface::mcp].
The MCP server auto-generates the tool's inputSchema from the interface's
assert tests (the same schema that powers the OpenAPI docs), so no separate
schema needs to be authored here.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Expose this interface as an MCP tool. Default false. Default: false. |
tool_name | string (nullable) | Override the tool name shown to MCP clients. Defaults to the interface name. |
description | string (nullable) | Override the tool description. Defaults to the interface summary/description. |
list_authorizer | string (nullable) | Optional per-tool tools/list authorizer: the name of an interface in this config that is RUN (with the forwarded request headers) when a client lists tools. The tool appears… |
server | string (nullable) | Which declared MCP server this tool belongs to (see [IntegrationConfig::mcp_servers]). Omitted = the default server, which is what every pre-existing config gets. The id may… |
McpServer
A named MCP server: an identity, plus the tools that join it by id.
Declared under [IntegrationConfig::mcp_servers] and joined by [McpTool::server]. Declaring
and contributing are deliberately separate. Air Pipe's own management MCP is assembled from many
configs but is one server with one identity, so repeating the identity in each contributing
config would leave no single source of truth; conversely one org may publish several distinct
MCP offerings from different subsets of its configs.
title and instructions exist because MCP registries (mcp.so, Glama, Smithery, PulseMCP) read
a remote server's listing metadata straight off the initialize response — there is no separate
place to write a description. Without them a listing renders as a bare name and a wall of tool
descriptions.
| Field | Type | Description |
|---|---|---|
title | string (nullable) | Display name shown to MCP clients and registries (serverInfo.title). Absent = clients fall back to the protocol-level server name. |
instructions | string (nullable) | Server-level description, emitted as the initialize result's instructions. This is the field MCP registries surface as the listing description. |
default | boolean | Serve this identity on the bare /<org>/<env>/mcp route, i.e. adopt the tools that name no server. At most one per org+environment; ties resolve deterministically by config name. Default: false. |
Param
Parameter definition for interface-level URL parameters with optional validation and default values.
Example
interfaces:
getUser:
method: GET
params:
id:
value: a|params::id|
error_message: "User ID is required"
validate:
is_not_empty: true
regex: "^[0-9]+$"
page:
value: a|params::page|
default_value: "1"
| Field | Type | Description |
|---|---|---|
value | string (nullable) | Source value expression (e.g., a|params::id|). |
default_value | string (nullable) | Default value if the parameter is not provided. |
error_message | string (nullable) | Custom error message when validation fails. |
validate | Test (nullable) | Validation test to run against the parameter value. Example ↓ |
Field examples
validate
Validation test to run against the parameter value.
Example
validate:
is_not_empty: true
is_less_than: 100
regex: "^[a-zA-Z0-9]+$"
Schedule
AirPipe schedule configuration. Defines when and how an interface should be executed automatically via the scheduler.
Example
myScheduledEndpoint: # Interface name
method: POST
actions:
- name: MyAction
http:
url: https://api.example.com/trigger
schedule: # Schedule configuration (enables auto-execution)
cron: "0 9 * * *" # Required: Cron expression (5 fields: minute-hour-day-month-weekday)
enabled: true # Optional: Enable/disable the schedule (default: false)
# Retry configuration
max_attempts: 3 # Optional: Max retry attempts on failure (default: 1)
retry_backoff_seconds: 60 # Optional: Initial backoff in seconds (default: 60)
retry_backoff_multiplier: 2.0 # Optional: Backoff multiplier for exponential backoff (default: 2.0)
max_backoff_seconds: 3600 # Optional: Maximum backoff cap in seconds (default: 3600)
| Field | Type | Description |
|---|---|---|
cron | string | Required. Cron expression in 5-field format (minute hour day month weekday) |
enabled | boolean | Required. Whether the schedule is enabled |
max_attempts | number (nullable) | Max retry attempts on failure Default: 1. |
retry_backoff_seconds | number (nullable) | Initial retry backoff in seconds Default: 60. |
retry_backoff_multiplier | number (nullable) | Exponential backoff multiplier Default: 2. |
max_backoff_seconds | number (nullable) | Maximum retry backoff cap in seconds Default: 3600. |
timezone | string (nullable) | IANA timezone for cron interpretation (e.g. 'America/New_York'); defaults to UTC Default: null. |
CaptureSetting
capture: on an interface — either a mode, or a mode that switches itself off.
The window form is what keeps the fleet's resting state at "capture off" no matter how many people click the button. Permanent capture stays a deliberate, tier-gated choice rather than the easy path.
capture: errors # plain mode
capture: # self-expiring window
mode: all
for_minutes: 30
for_runs: 500 # whichever comes first
One of:
CaptureMode
How much of a run to record.
One of:
- string — Record nothing. The fleet-wide default.
- string — Record failed runs only. The mode worth defaulting to when capture is wanted at all: volume then tracks the ERROR RATE rather than throughput, so a healthy system at a million…
- string — Record successful runs too. The expensive mode, and the one tiers actually buy.
CaptureWindow
| Field | Type | Description |
|---|---|---|
mode | CaptureMode | Required. |
for_minutes | number (nullable) | Switch off this many minutes after the window opens. |
for_runs | number (nullable) | Switch off after this many runs. |
StreamOut
| Field | Type | Description |
|---|---|---|
from | string | Required. Name of the action whose stream is forwarded. It must be an action in this interface that reads an event stream; anything else forwards nothing. |
to | StreamTarget | Where the chunks go. Only channel exists today. Default: "channel". |
channel | any | Channel key(s) to publish to — a string, an array, or an a|... marker resolving to either. Same shape ws_publish takes. Required when to: channel. |
flush_ms | number (nullable) | Flush the buffer after this many milliseconds. Default 50. Publishing per token is not viable: a publish fans out to one HTTP call per peer holding a subscriber, so a 500-token… |
flush_chars | number (nullable) | Flush early once this many characters are buffered, so a fast stream does not sit waiting on the timer. Default 200. |
StreamTarget
Where a streaming action's chunks are forwarded.
Kept separate from respond: on purpose. Streaming to a channel and returning the
response early are independent choices: an SSE response would stream while HOLDING the
request open, so folding "detach" into "stream" would make that combination unexpressible.
One of:
- string — Publish each batch to a realtime channel. Subscribers receive them live; the HTTP response is unchanged.
Respond
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.
When the HTTP response is sent relative to the work.
One of:
- string — Answer once every action has finished. The default, and what every config does today.
- string — Answer immediately and keep running. See [
Interface::respond].