Skip to main content

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

FieldTypeDescription
captureCaptureSetting (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…
defaultsActionDefaults (nullable)Defaults handed to every action in this interface (see [ActionDefaults]). An action that sets the field itself always wins.
summarystring (nullable)
descriptionstring (nullable)
logCustomLog (nullable)
tagsArray<string> (nullable)
templatesMap<string, string> (nullable)
mqttstring (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…
wsstring (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…
respondRespond (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 `{…
streamStreamOut (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_authorizerstring (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…
publicboolean (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…
scheduleSchedule (nullable)
silenceboolean (nullable)
methodstring (nullable)
routestring (nullable)
show_error_detailboolean (nullable)
actionsArray<Action> (nullable)
urlstring (nullable)
conn_stringstring (nullable)
querystring (nullable)
paramsMap<string, Param> (nullable)
testsArray<Test> (nullable)
assertAssert (nullable)
outputInterfaceOutput (nullable)
responseInterfaceResponse (nullable)
disable_fastpathboolean (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 —…
acceptsInterfaceContract (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…
producesInterfaceContract (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_exampleanyExample request body shown in generated API documentation. DEPRECATED: use request: { example: … }. Still read so existing configs keep working; request.example wins when…
response_exampleanyExample successful response body shown in generated API documentation. DEPRECATED: use response: { example: … }. Still read so existing configs keep working;…
notesstring (nullable)Additional free-form documentation notes shown under the endpoint description.
networkNetworkPolicy (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…
mcpMcpTool (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

FieldTypeDescription
http_code_on_errornumber (nullable)
http_code_inherit_errorArray<string> (nullable)
headersobject (nullable)
custom_bodystring (nullable)
http_code_fallbacknumber (nullable)
http_code_fallback_strategyErrorFallbackStrategy (nullable)
logsArray<InterfaceLog> (nullable)

InterfaceLog

FieldTypeDescription
onInterfaceLogOnRequired.
databasestring (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.

FieldTypeDescription
enabledbooleanExpose this interface as an MCP tool. Default false. Default: false.
tool_namestring (nullable)Override the tool name shown to MCP clients. Defaults to the interface name.
descriptionstring (nullable)Override the tool description. Defaults to the interface summary/description.
list_authorizerstring (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…
serverstring (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.

FieldTypeDescription
titlestring (nullable)Display name shown to MCP clients and registries (serverInfo.title). Absent = clients fall back to the protocol-level server name.
instructionsstring (nullable)Server-level description, emitted as the initialize result's instructions. This is the field MCP registries surface as the listing description.
defaultbooleanServe 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"
FieldTypeDescription
valuestring (nullable)Source value expression (e.g., a|params::id|).
default_valuestring (nullable)Default value if the parameter is not provided.
error_messagestring (nullable)Custom error message when validation fails.
validateTest (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)
FieldTypeDescription
cronstringRequired. Cron expression in 5-field format (minute hour day month weekday)
enabledbooleanRequired. Whether the schedule is enabled
max_attemptsnumber (nullable)Max retry attempts on failure Default: 1.
retry_backoff_secondsnumber (nullable)Initial retry backoff in seconds Default: 60.
retry_backoff_multipliernumber (nullable)Exponential backoff multiplier Default: 2.
max_backoff_secondsnumber (nullable)Maximum retry backoff cap in seconds Default: 3600.
timezonestring (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

FieldTypeDescription
modeCaptureModeRequired.
for_minutesnumber (nullable)Switch off this many minutes after the window opens.
for_runsnumber (nullable)Switch off after this many runs.

StreamOut

FieldTypeDescription
fromstringRequired. 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.
toStreamTargetWhere the chunks go. Only channel exists today. Default: "channel".
channelanyChannel 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_msnumber (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_charsnumber (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].