HTTP requests
Supply an HTTP URL for an action to use as an input, or to call an upstream API.
interfaces:
http/basic:
output: http
method: GET
actions:
- name: BasicHTTP
http:
url: https://jsonplaceholder.typicode.com/todos/1
HTTP actions also support auth (bearer / basic / digest), TLS options, multipart uploads, automatic pagination, and reverse-proxy passthrough. Every field is documented below.
Success and failure: expect_status
A response outside expect_status fails the action. The default is 2xx (plus 304),
so a 4xx or 5xx upstream fails the step that called it — which is what makes retry,
run_when_failed and http_code_inherit_error work without writing an assertion to
translate a status into an outcome.
actions:
- name: Push
retry: {attempts: 3, exponential_backoff: true}
http:
url: https://api.example.com/orders
method: POST
A failed action keeps its payload — status, headers and body — so an error branch can still read them:
- name: Recover
run_when_failed: [Push]
input: a|Push| # .status, .headers, .body are all here
When a non-2xx status is genuinely data rather than a failure, say so:
http:
url: https://api.example.com/users/42
expect_status: [2xx, 404] # 404 means "no such user", not an error
| Value | Meaning |
|---|---|
| unset | 2xx or 304 is success (the default) |
2xx / 4xx / 5xx | a whole class |
404 | one exact code |
[2xx, 404] | any of a list |
any | never fail on status — the status is only data |
304 Not Modified counts as success by default because RFC 9110 §15.4.5 defines it as the
successful outcome of a conditional request whose validator matched — a poller using
If-None-Match gets one on its happy path.
The outcome is declared, not inferred
expect_status decides whether the action failed. An assert validates the data and never
decides the outcome, so this does not do what it looks like:
# WRONG — the action fails on the 403 before the assert runs
http: {url: https://api.example.com/thing}
assert:
tests:
- value: status
is_equal_to: 403
# RIGHT — say what you expect
http: {url: https://api.example.com/thing, expect_status: 403}
One rule, in one place: read an action and you can see what counts as failure without
tracing which assertions happen to mention status. The engine says so when you get it
wrong — the failure message names the expect_status line to add.
One declaration for a whole interface
An interface whose actions share an expectation — a test runner hitting routes that answer 403 and 500 on purpose, a health-check fan-out that records whatever comes back — declares it once. An action that sets its own always wins.
interfaces:
tests/all:
defaults:
expect_status: any
actions:
- name: ChecksA
http: {url: http://localhost:44111/route-a} # inherits `any`
- name: ChecksB
http: {url: http://localhost:44111/route-b, expect_status: 2xx} # overrides
Polling for something that is not there yet
A GET that answers 404 until a record exists now fails on the first attempt, and 404 is
not in the default retry set, so the action gives up immediately. Say which one you mean:
# 404 is data; retry until the assert passes
http: {url: https://api.example.com/orders/42, expect_status: [2xx, 404]}
retry: {attempts: 10, delay: 500, exponential_backoff: true}
assert:
tests:
- value: body.id
is_not_null: true
Responses with no body
204 No Content and 304 Not Modified carry no body by definition, and a 200 with
content-length: 0 is legal. The body decodes to null rather than failing the action, so
a DELETE that answers 204 succeeds and a|Delete::status| still reads 204.
Retrying safely: idempotency
Retrying a POST can mean charging a customer twice. RFC 9110 §9.2.2 permits automatically retrying a non-idempotent request only when you have "some means to know that the request semantics are actually idempotent... or some means to detect that the original request was never applied". An idempotency key is that means.
http:
url: https://api.stripe.com/v1/charges
method: POST
idempotency: {} # engine generates the key
Every attempt of one action execution sends the same key — that is the whole point, and
it is the part a config cannot express itself: a|uuid| is re-evaluated per attempt, so
each retry would carry a fresh key and be processed as a new request.
Set the key yourself when the caller may retry the whole workflow and you want those attempts deduplicated too:
idempotency:
key: a|body::orderId| # derived from your data
header: PayPal-Request-Id # default: Idempotency-Key
HttpRequest
| Field | Type | Description |
|---|---|---|
url | string | Required. The URL to call. Interpolation applies, so it can be built from earlier steps or variables. Example ↓ |
expect_status | StatusExpectation (nullable) | Which response statuses count as a SUCCESSFUL action. Defaults to 2xx, so a 4xx/5xx fails the action — which is what makes retry, run_when_failed and… |
idempotency | Idempotency (nullable) | Send an idempotency key with this request, so a RETRY cannot be processed twice. The engine sends the same key on every attempt of one action execution — that is the whole… |
digest_auth_user | string (nullable) | Username for HTTP Digest authentication. |
digest_auth_pass | string (nullable) | Password for HTTP Digest authentication. Keep it in a secret or variable rather than in the config: a|ap_secret::UPSTREAM_PASS|. |
user | string (nullable) | Username for HTTP Basic authentication. |
pass | string (nullable) | Password for HTTP Basic authentication. As with digest, reference a secret. |
proxy | string (nullable) | Send this request through an HTTP proxy. |
headers | Map<string, string> (nullable) | Request headers. Values interpolate, which is how a token is passed without writing it into the config. Example ↓ |
body | any | Request body. A mapping is sent as JSON; a string is sent as-is, which is what you want for a pre-rendered payload. |
base64_bytes | string (nullable) | Send raw bytes decoded from base64, for binary uploads that are not multipart. |
method | string (nullable) | HTTP method. Defaults to GET. Deliberately a string rather than a closed set: the engine passes it through as a token, so an extension method a private API expects still… |
data_type | DataType (nullable) | How to encode the body and read the response. Inferred when unset. |
bearer_auth | string (nullable) | Token for Authorization: Bearer. Reference a secret or variable rather than writing the token into the config: a|ap_var::API_TOKEN|. |
multipart | Array<MultipartPart> (nullable) | A flat list of parts for a multipart/form-data request. Each entry is one form part. Whether it is a text or binary part is determined implicitly by which source field is… Example ↓ |
multipart_form | Map<string, string> (nullable) | Deprecated: use multipart instead. |
multipart_files | Array<MultipartData> (nullable) | Deprecated: use multipart instead. |
accept_invalid_certs | boolean (nullable) | |
ca | string (nullable) | |
root_certificates | Array<string> (nullable) | |
timeout | string (nullable) | |
connect_timeout | string (nullable) | |
pagination | Pagination (nullable) | |
proxy_body | boolean (nullable) | |
proxy_method | boolean (nullable) | |
proxy_headers | ProxyHeaders (nullable) | |
proxy_query | boolean (nullable) |
Field examples
url
The URL to call. Interpolation applies, so it can be built from earlier steps or variables.
url: https://api.example.com/users/a|params::id|
headers
Request headers. Values interpolate, which is how a token is passed without writing it into the config.
headers:
content-type: application/json
authorization: Bearer a|ap_var::API_TOKEN|
multipart
A flat list of parts for a multipart/form-data request.
Each entry is one form part. Whether it is a text or binary part is determined implicitly by which source field is provided:
value→ plain text partb64→ binary part decoded from base64file_path→ binary part read from disk (not allowed in managed/hosted mode)
filename and mime are always optional. When omitted on a binary part, mime
is guessed from the filename extension, falling back to application/octet-stream.
multipart:
- name: image
b64: a|previous_action::body.image.data|
filename: photo.png # optional — sent as Content-Disposition filename
mime: image/png # optional — guessed from filename if absent
- name: model
value: gpt-image-1
- name: prompt
value: Transform into Studio Ghibli style
Pagination
| Field | Type | Description |
|---|---|---|
original_url | string (nullable) | |
next_link | string (nullable) | |
no_pages | number | Default: 0. |
page_marker | number | Default: 0. |
cursor_marker | string (nullable) | |
increment | number | Default: 1. |
max_pages | number (nullable) | |
max_pages_key | string (nullable) | |
page_start | number | Default: 1. |
page_next_key | string (nullable) | |
page_limit | number (nullable) | |
page_limit_key | string (nullable) | |
payload_key | string (nullable) | |
next_cursor_key | string (nullable) | |
max_cursor_key | string (nullable) | |
next_link_key | string (nullable) | |
next_link_host | string (nullable) |
MultipartPart
A single part in a multipart/form-data request.
Exactly one of value, b64, or file_path must be provided per part.
All other fields are optional.
| Field | Type | Description |
|---|---|---|
name | string | Required. Form field name — the name attribute sent in Content-Disposition. |
value | string (nullable) | Plain text value. Use this for non-binary form fields. Mutually exclusive with b64 and file_path. |
b64 | string (nullable) | Base64-encoded binary data. Decoded before sending. Mutually exclusive with value and file_path. |
file_path | string (nullable) | Path to a file to read and send as binary data. Not permitted in managed/hosted mode — use b64 instead. Mutually exclusive with value and b64. |
filename | string (nullable) | Filename sent in the Content-Disposition header for this part (e.g. photo.png). Optional for binary parts; not meaningful for plain text parts. |
mime | string (nullable) | MIME type for this part (e.g. image/png, application/pdf). If omitted on a binary part, guessed from filename's extension. Falls back to application/octet-stream when… |
MultipartData
Deprecated: use [MultipartPart] via the multipart field instead.
One of:
MultipartFile
Deprecated: use [MultipartPart] via the multipart field instead.
| Field | Type | Description |
|---|---|---|
file_path | string (nullable) | |
file_name | string | Required. |
b64 | string (nullable) | |
mime_str | string (nullable) | |
part_name | string (nullable) |
MultipartText
Deprecated: use [MultipartPart] via the multipart field instead.
| Field | Type | Description |
|---|---|---|
key | string | Required. |
value | string | Required. |
mime_str | string (nullable) |
ProxyHeaders
Controls which headers are passed through from the incoming request.
One of:
- boolean — When true, pass through all incoming headers
- Array<string> — When a list, pass through only the specified headers; empty list means pass none