api_call. It belongs to the same family of server-side action
nodes as Slack messages and webhooks,
but unlike those it produces an output you consume downstream rather than a
fire-and-forget side effect.
How it works
A flow run carries a step bag a map keyed by node id that accumulates each node’s output as the run advances. When the engine reaches anapi_call node, it
first interpolates the node’s config against everything the run knows so far,
then performs the fetch, then writes the result under the node’s id. Downstream
nodes interpolate their own config against that same bag, which is how a later
node “sees” what the API returned.
The sequence inside one node execution:
Everything here happens in ApiCallExecutor on the backend. The browser SDK is
never involved it only ever receives the finished widget that downstream
composition produces.
Config is interpolated before the fetch
Config values may contain{{...}} tokens, and the engine resolves them against
the run before the request is built. Two roots are available:
{{trigger.<path>}}the payload that started the run. For a message-driven guide that includes{{trigger.content}}(also aliased as{{content}}) and{{userId}}.{{steps.<nodeId>.<path>}}the output of any node that already ran, including earlier API Call nodes.
"{{steps.lookup.body.plan}}" yields the
string "pro", while "id={{userId}}" yields "id=usr_123".
The response shape
On a successful (2xx) call the node writes this object to the step bag:body falls back
to { text: "<raw response>" } so downstream interpolation never breaks. Read
any of it by path from a later node:
{{steps.account_lookup.body.plan}} equals trial and another when it equals
pro. Branch evaluation runs server-side, so the data the branch reads is the
data the backend fetched.
Configure a node
The flow editor writes two things: the request config travels with the flow graph (apiCall* keys on the node), and the secret API key is stored
separately and encrypted, never in the graph. Saving the key uses
PUT /agents/:id/api-calls/:nodeId.
The endpoint to call. Must be
https:// (plain http://localhost is allowed
only in development). The URL may contain {{...}} tokens. URL-embedded
credentials (https://user:pass@host) are rejected.HTTP method, upper-cased.
GET is typical for reads.Query-string parameters appended to the URL. Keys and values are
URL-encoded; rows with a blank key are dropped. Values may use
{{...}}.Extra request headers. FirstFlow always sends
Accept: application/json and
User-Agent: FirstflowBot/1.0; your headers are merged on top. Values may use
{{...}}.The header the stored secret key is injected into, as
Bearer <key>. Set this
to a custom name (e.g. X-Api-Key) if the target expects something other than
Authorization.Request timeout in milliseconds, clamped to
1000–10000. The request is
aborted when it elapses.Maximum response size, clamped to
1024–102400 (50 KB by default). A larger
body fails the node rather than truncating, so downstream paths never read a
half-parsed object.Saving the secret key
The key never lives in the flow graph. It is encrypted at rest (the same credential vault used for other integrations) and only decrypted in memory at call time, server-side, to build theBearer header.
url (https only, no embedded credentials) and
clamps timeoutMs (1000–10000) and maxResponseBytes (1024–102400). The
matching GET returns config metadata plus a boolean hasApiKey never the key
itself:
"apiKey": null to clear a stored key, or omit apiKey on a later PUT to
leave the existing key in place while changing other fields.
Most teams configure all of this in the flow editor and never call these
endpoints directly. The contract is documented here because the key handling —
encrypted at rest, decrypted only server-side, surfaced to clients only as
hasApiKey is the reason an API Call node is safe to point at an
authenticated endpoint.Test before you ship
The editor can run a node’s request once, on demand, viaPOST /flow-node-test/api-call, so you capture the real { status, headers, body }
as the step’s sample shape while authoring. The test applies the same SSRF
guard as a live run and attaches the stored key only for an agent in your own
organization, so a key can never be exfiltrated by testing someone else’s node.
{{steps.<id>.body.plan}} in downstream
nodes with confidence that the path exists.
Failures, retries, and limits
The node distinguishes definitive failures from transient ones, because only transient failures are worth retrying.
A thrown error is handed to the engine’s retry policy. The default policy is
maxAttempts: 1, which means no retry the run fails unless the node is
configured with a higher attempt count, in which case the delay grows
exponentially from a base. A definitive failed is never retried; the flow takes
its failure path immediately.
The SSRF guard is not optional. At runtime the URL is re-validated to be
https:// pointing at a public host private, loopback, link-local,
unique-local, and carrier-grade-NAT IP ranges are rejected, as are localhost
and .local names (except localhost in development). Redirects are re-validated
on every hop, so an endpoint cannot 302 you onto an internal address. This is why
you cannot point an API Call node at internal infrastructure; expose a public
HTTPS endpoint instead.
A few smaller constraints worth knowing: the response must fit in
apiCallMaxResponseBytes (50 KB default, 100 KB max), the body is interpreted as
JSON with a { text } fallback, and the timeout caps at 10 seconds. If you need
to push data outward rather than pull it in, reach for a
webhook instead it is the outbound counterpart and
runs through a retrying outbox.