OpenAPI plugin
Use OpenAPI for HTTP operations your API declares in an OpenAPI document. Reflow validates the request before sending it, checks the response status and can assert JSON fields or capture one public value for a later step. Prefer the MCP plugin when your server exposes the operation as a tool.
Requirements and selection
Section titled “Requirements and selection”- A signed-in Reflow CLI and an approved team. See Get started.
- A selected OpenAPI plugin. The bundled set contains seven plugins: Browser,
LocalBash, PostgreSQL, DockerUbuntu, Mailbox, MCP and OpenAPI. Use
list_pluginsor the application’s Plugins page to inspect the selection actually installed. - A local OpenAPI 3.0 or 3.1 YAML/JSON file and an HTTP(S) test server implementing its operations. Reflow does not download the spec or start that server.
- A test identity, if required. Supported schemes are HTTP bearer and API keys sent in headers. The plugin has no OAuth sign-in flow.
On first initialization:
reflow init --collection <collection-id>Existing applications retain their selection. To add OpenAPI, stop active
executions and supply the complete desired set with --upgrade, for example:
reflow init Browser LocalBash PostgreSQL DockerUbuntu Mailbox MCP OpenAPI --collection <collection-id> --upgradeThis also upgrades the other listed plugins to the bundled release. Check the plan afterward. See one selection per application. The native plugin requires no Node or Go runtime for ordinary use.
Pin the API contract
Section titled “Pin the API contract”Save the spec at an absolute path. It must be a regular file, at most 16 MiB, owned by the runner’s user, with no symlink component and no group/other write permission. Compute SHA-256 over its exact bytes:
chmod 600 /absolute/private/api.yamlshasum -a 256 /absolute/private/api.yamlThe private binding holds specFile and its 64 lowercase hex specSha256.
Every command repeats spec="sha256:…" with that same full digest. Changing the
spec means updating both pins; the step pin makes the contract part of the plan.
The first command pins the spec for that provider instance. A mismatch refuses
before contact. Spec servers entries do not select a destination; baseUrl does.
Only local $ref pointers are followed. External references are unsupported.
Copyable example contract
Section titled “Copyable example contract”This is a synthetic item API. Save the following code block as api.yaml,
including its final newline. Its digest is
9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444. Implement these routes on an owned test server
or use your own spec and replace every example pin and operation accordingly.
The example assumes the test item already exists and the server returns
{"code":"missing"} after it is archived.
openapi: 3.1.0info: title: Synthetic item management API version: "1"security: - project: [] session: [] - apiKey: [] project: []paths: /items.archive: post: operationId: archiveItem requestBody: required: true content: application/json: schema: type: object required: [id] additionalProperties: false properties: id: type: string format: uuid responses: "200": description: Archived, including when already absent /items.get: get: operationId: getItem parameters: - name: id in: query required: true schema: type: string format: uuid responses: "200": description: Item content: application/json: schema: type: object properties: id: type: string "404": description: Missing content: application/json: schema: type: object properties: code: type: string /exports.routes.delete: delete: operationId: deleteExportRoute parameters: - name: id in: query required: true schema: type: string format: uuid responses: "204": description: Deleted "400": description: Invalid request content: application/problem+json: schema: type: objectcomponents: securitySchemes: session: type: apiKey in: header name: X-Session project: type: apiKey in: header name: X-Project apiKey: type: apiKey in: header name: X-Api-KeyPrivate binding and origin access
Section titled “Private binding and origin access”Save this as a private bindings.json, replacing the paths, server and test
session value. The names session and project are security scheme names
from the spec; X-Session and X-Project are their declared wire headers.
{ "http": { "provider": "OpenAPI", "configuration": { "specFile": "/absolute/private/api.yaml", "specSha256": "9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444", "baseUrl": "https://api.example.test", "credentials": { "session": {"value": "replace-with-a-test-session-token"}, "project": {"public": true} }, "timeoutMs": 30000, "maxResponseBytes": 8388608 } }}chmod 600 /absolute/private/bindings.jsonreflow target https://api.example.test --group item-tests --collection <collection-id> --bindings /absolute/private/bindings.jsonThe target admits its origin. If another target is selected, grant this API’s
origin with --allow-origin https://api.example.test. The binding cannot widen
host access. See origin grants.
baseUrl must be absolute HTTP(S), without user information, query or fragment;
its escaped path prefix is preserved. Use caFile for an absolute private PEM
CA bundle when system trust does not cover your test server.
For each scheme choose exactly one of value, tokenFile:true or public:true.
Static secrets need at least eight printable ASCII characters. public:true
is available only for API-key header schemes: each step supplies that public
value in header, such as a project slug. Secret credentials belong in the
binding or a token file. A secret scheme header in step arguments refuses.
The first satisfiable security alternative in spec order is used, including an
anonymous {} alternative; inspect securitySchemes when checking identities.
Per-run tokens and cookie jars
Section titled “Per-run tokens and cookie jars”For a token minted during a run, replace the session credential with
{"tokenFile":true} and add "tokenDirectory":"/absolute/private/tokens"
to the configuration. A visible earlier authentication step writes the file;
OpenAPI reads it and does not perform login. Pass its filename as
tokenFile="session" on each request that needs it.
The directory must be absolute, owned by the runner’s user, mode 0700 or stricter, without symlinks. A token file must be a single-link regular file, owned by that user, mode 0600 or stricter, at most 16 KiB. A plain token needs at least eight printable characters. A successful read pins the filename and contents for the instance; use separate bindings for separate identities. Keep token bytes out of captures and flow text.
To read a Netscape cookie jar instead, the credential is
{"tokenFile":true,"cookie":"session"}. The selected value fills the spec’s
credential header; no Cookie header is sent. Exactly one unexpired named cookie
must match the base URL. Include-subdomains FALSE matches the exact host;
TRUE also matches subdomains on a dot boundary. The cookie path must be a
literal prefix of the base path. Secure cookies require HTTPS; expiry 0 is a
session cookie. There is no public-suffix validation. Malformed jars, zero
matches or multiple matches refuse before contact as token_file_unavailable.
Every recognizable nonempty jar value is private, including short, expired or
other-host rows. A well-formed jar pins even when no row is eligible; expiry is
rechecked on selection. Correcting a malformed jar is allowed before it pins.
Write a flow
Section titled “Write a flow”With the example server, binding and pre-existing test item above, save this flow
as /tmp/item-check.md. Calls really reach the server, including archive.
---name: Check and archive a test itemproviders: http: OpenAPI---
### Inspect the declared operation without contacting the server
```rflhttp.describe_operation operationId="getItem" spec="sha256:9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444"```
### Check the item and capture its public id
```rflhttp.call ( operationId="getItem" spec="sha256:9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444" query=json( {"id":"3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b"} ) header=json( {"X-Project":"default"} ) expect=json( {"status":200,"json":[{"pointer":"/id","equals":"3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b"}]} ))http.call_value ( operationId="getItem" spec="sha256:9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444" query=json( {"id":"3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b"} ) header=json( {"X-Project":"default"} ) expect=json( {"status":200} ) select="/id") -> item_id```
### Archive the captured item and prove it is absent
```rflhttp.call ( operationId="archiveItem" spec="sha256:9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444" header=json( {"X-Project":"default"} ) body=json( {"id":"${item_id}"} ) expect=json( {"status":200} ))http.call ( operationId="getItem" spec="sha256:9c491f4050369bc8572eaf86ab1333bad35dabb38f11a890f94b4bd0d2a37444" query=json( {"id":"${item_id}"} ) header=json( {"X-Project":"default"} ) expect=json( {"status":404,"json":[{"pointer":"/code","equals":"missing"}]} ))```reflow flows create --name item-check --file /tmp/item-check.mdreflow plan item-checkreflow apply item-checkreflow dashboardA captured scalar is public evidence. In JSON arguments, put ${item_id} inside
a JSON string value as shown; Reflow escapes its content, including quotes
and backslashes. It cannot introduce a new member. Do not put the reference in
a member name or an unquoted numeric slot. call_value returns a string even
when the source is a number or boolean. See JSON references
for declared-input forwarding, capture scope and refused placements. Grouped
calls and JSON blocks require a runtime supporting that syntax.
Commands and request validation
Section titled “Commands and request validation”| Command | Effect | Contract |
|---|---|---|
http.describe_operation | observe | Read the pinned spec, supported parameters/body/security/responses and bounded public pointer hints. No HTTP request. |
http.call | mutate | Send one typed request and assert status plus optional JSON checks. No scalar return. |
http.call_value | mutate | Send one typed request and return one selected public scalar as text. expect accepts status only. |
Every command requires the exact operationId and spec pin. Request commands
accept JSON text in path, query, header and body, plus expect and optional
tokenFile. Only declared parameters are allowed. There is no raw method, URL,
path or header escape hatch. An absent operation is operation_not_found.
Path values are encoded as single segments. Query arrays use repeated keys;
objects and path/header arrays are unsupported. readOnly body fields refuse.
Integer path/query/header values use exact canonical integer text; body numbers
keep their authored lexemes after validation. Native admission adds limits below.
Status must be declared exactly or by an NXX response range; default alone
does not admit it. Omit expect.status only when there is one declared 2xx.
All JSON assertions must also pass. Expected 4xx responses can be asserted.
The plugin sets transport headers itself; reserved headers, including Cookie,
Content-Type, Authorization and Idempotency-Key, cannot be supplied by a step.
A declared security scheme can fill Authorization privately.
Schema and media limits
Section titled “Schema and media limits”The supported subset includes primitive/object/array types, nullable values,
properties/required/additionalProperties/items, enum, length/item/numeric bounds,
RE2 patterns, local refs and the documented OpenAPI formats: int32, int64, double,
float, uuid, date-time, date, email, uri and password. Defaults are never applied.
RE2 excludes lookaround/backreferences and differs from JavaScript for \s and
.; use common literal/ASCII patterns. Unknown keywords, formats and extensions
fail closed; the supported schema annotations include description/title/examples
and x-speakeasy-name-override/x-speakeasy-deprecation-message. x-sensitive
is a privacy marker. Composition such as oneOf/allOf, cookie/query credentials,
parameter content, form/multipart bodies and OAuth flows are unsupported.
Request-schema gaps make an operation unsupported before contact. A response schema gap can leave status-only calls usable while withholding its hash, assertions and capture. Unknown structural extensions or unclassifiable Header privacy declarations can refuse the whole binding, including unused components. Document admission also caps each map/list at 2048 members and total container entries at 160000; a spec can fit 16 MiB and still exceed these limits.
Response JSON media (application/json or application/*+json) are assessed
individually for the expected status. describe_operation lists jsonMedia with
classification, root comparison eligibility and bounded public-pointer hints.
A status-level hashable means at least one hashable media, not every pointer.
Hints are per media and include containers useful for type/count; they do not
prove scalar capture or acceptance by cross-media privacy checks. Declared
container selections refuse before contact; untyped targets need runtime checks.
At least one eligible media permits dispatch; the actual media must be declared,
classified and support the requested pointers. Unknown/ineligible media, encoded
bodies, runtime schema mismatch, malformed JSON or duplicate members anywhere
fail assertions/capture after the one response. Truncated hints are not an
exhaustive pointer inventory; explicit additionalProperties paths are checked
when a command uses them.
JSON assertions and selection
Section titled “JSON assertions and selection”Each expect.json entry contains pointer and exactly one operator. Between
1 and 32 entries are allowed; all must pass.
| Operator | Operand | Meaning |
|---|---|---|
exists | boolean | Require presence/absence; null is present. |
equals | string, number, boolean or null | Exact scalar equality; no object/array equality. |
type | string/number/integer/boolean/null/object/array as a string | Check JSON kind; integer means exact integral value. |
count | integer 0–10000 | Array length or object member count. |
includes | nonempty string | Substring of a public string. |
Pointers use RFC 6901: empty means root, ~1 means /, ~0 means ~.
An object key 01 is valid; array index 01 is not. * has no wildcard meaning
in assertions or selection. Pointers are limited to 1024 UTF-16 code units;
string operands to 4096. Invalid entries and private/control operands refuse
before contact. Assertion evidence contains only index, public pointer,
operator, pass/fail and a fixed reason, never target or operand values.
select returns a string/number/boolean as text, at most 4096 UTF-8 bytes.
Null, containers, missing/private values and C0/C1 controls cannot be captured.
No failed or unknown call returns a value. Pure control-bearing response strings
may pass exists/type; equals/includes reject terminal controls.
Privacy and minted secrets
Section titled “Privacy and minted secrets”Before contact, every assertion and selection refuses a sensitive target or
ancestor in any JSON media for the expected status. Sensitivity includes
writeOnly, format password, x-sensitive, secret-looking names such as token/key,
and binding sensitiveTargets. A bounded privacy walk follows refs and union
branches along the pointer without making unions eligible response schemas.
Unclassifiable alternate branches refuse before contact even when another media
is eligible. Register minted credentials the spec does not
mark using operation-specific pointers, for example this configuration fragment
only when your spec declares createAPIKey and a response /key:
{"sensitiveTargets":{"createAPIKey":["/key"]}}Binding pointers must reach a declared JSON response schema. Only these binding
pointers allow * to match an array index. Public siblings and parent counts
remain usable after sensitive descendants are replaced. A replaced target or
ancestor cannot satisfy an assertion by matching a placeholder.
Known binding values, paths, tokens and jar values are screened in pointers,
operands and responses, including conservative numeric/escaped forms. Detection
is bounded and cannot recognize every transformation a server could apply.
Capture is for public identifiers and data; never use it for credentials.
Native JSON limits and exact numbers
Section titled “Native JSON limits and exact numbers”The native host validates JSON-slot arguments before the provider. Body accepts
any JSON root (its content schema is any): scalar, array or object. Parsed
numbers must be finite with absolute value at most 9007199254740991. That parsed
check can round or underflow; an admitted assertion still compares its original
exact decimal spelling. Exact comparison allows up to 1000 mantissa digits,
1024 bytes and six significant exponent digits (up to ±999999).
The pinned host’s measured simple nested-array body boundary is 13 levels around scalar 0 admitted, 14 refused before HTTP contact. Other shapes also obey bounded host traversal. The provider’s direct parser separately admits request depth 32 and larger numeric lexemes; that does not establish native argument admission. Captured response numbers retain their original text; a later JSON numeric argument has its own admission check. Installed flow interpolation and application behavior require validation in your environment.
Evidence, uncertainty and recovery
Section titled “Evidence, uncertainty and recovery”Raw response bodies are never retained. Evidence includes status, completion, size, admitted media, fixed diagnostics and a canonical JSON hash when safe. Sensitive fields/private echoes are replaced before hashing. The unsalted hash can confirm a guess about a small low-entropy body. Hash unavailability does not change a status-only call’s outcome; assertions/capture need their own complete safe projection. A finished projection may be comparable even when legacy numeric hash bounds make the hash unavailable.
Calls always count as mutations, including GET. Each sends at most one request,
without retries, redirects, connection reuse, HTTP/2 or compression. A refusal
before dispatch has completion:not_dispatched. A complete response is
confirmed, including a failed status/assertion or selection. A lost/incomplete
response after connection is unknown: it may have changed the server. The
instance then closes and sends nothing more (instance_closed).
Inspect server state using an independent known-safe check before deciding whether to reconcile or allow a new write. Unknown work is blocked from automatic rerun; explicitly taint only after that decision, then plan again. Taint and reset do not undo server effects. Keep cleanup and identity preparation visible in your flows. See execution recovery.
timeoutMs is one absolute request/projection/assertion deadline: default 30000 ms,
range 1–600000. maxResponseBytes defaults to 8 MiB, configurable from 1 KiB to
16 MiB. Host/flow deadlines also apply. Incomplete projection withholds a capture
without replaying the HTTP effect. Expiry withholds all assertion results and
capture; an HTTP status mismatch retains priority over other source failures.
Compare checks stable call fields and
selection type/failure/pointer; the selected text itself is volatile and excluded.
Return/retained-value mismatches are rejected.