Skip to main content
Walk pages, write in batches, wait on a job, or remember between runs. Each is one key on the call it belongs to, so it stays in the same ordered list as every other call. api/run.yaml on its own can say “start, then check once”. It cannot say “check until”. These keys are how you say it. The pattern is the same each time. You describe how that particular API works, and the platform does the looping. None of these changes the node’s kind, and all of them work on either. A PromiseNode can walk forty pages and still settle once. A CallbackNode that streams its final answer can page through a lookup first. What decides kind is the last call’s transport, because that is the node’s answer. Every earlier call settles by definition, so an earlier step can do anything at all. See Node types.

The capabilities

paginate

Walk the pages and accumulate the results.
api/run.yaml
cursor reads the next-page token out of a reply, and a falsy result ends the walk. into names the query parameter it goes back as, and the first request carries none. For a numbered API use strategy: page, which also needs size so the platform can tell a full page from the last one. strategy: offset counts records instead of pages. in: body sends the page number in the request body for an API that reads it there rather than from the query. The reply becomes { items, pages, truncated }, not the last page’s body. The accumulation is the answer, and handing back the final page would quietly lose the rest. truncated is true when a limit stopped the walk with more still to fetch. A consumer can tell “that is everything” from “that is the first hundred”. There is a ceiling of 100 requests per call whatever max says, because an API that keeps returning the same cursor would otherwise loop forever.

chunk

The mirror of paginate. That one loops because the API decides how much comes back. This one loops because the API limits how much you may send at once.
Each slice is in scope as batch, so you describe one request and the platform repeats it. size is the API’s limit, not a preference. Airtable rejects an eleventh record, HubSpot takes 100, Salesforce 200. The reply is { sent, batches, results, errors }, and partial success is normal. One rejected batch out of ten is neither a failed call nor a successful one. The batches before it have already landed and cannot be taken back. A failing batch is recorded and the walk continues, so check errors rather than assuming all or nothing. There is a ceiling of 200 per batch whatever size says.

poll

Start work, then ask until it is done. Every crawler, render farm, transcription and batch import works this way.
url is an expression over the start reply, which is where the job id is. The reply is the final status payload, not the start reply. The start reply is a receipt carrying a job id. Handing that back would give events a handle where it expected the answer. Always write failed. Without it, a job that fails terminally is polled until it runs out of attempts, and you get a timeout instead of the reason. until is asked of the start reply first, before any polling, because some endpoints finish inline and return the completed result with no job id at all. A manifest that assumed a handle would fail on exactly the fast case. intervalMs is capped at 60 seconds and maxAttempts at 300.

repeat

Built 2026-09-13. A package whose node goes round in turns declares requires.repeat: true in its package.yaml, so an executor without it refuses the package instead of settling on nothing.
Go round in turns until a reply says you are done. Use it when each turn is a few ordinary calls and each turn is built from the answer to the one before: walking a tree and choosing a branch at each fork, narrowing a search, letting a model ask for more before it answers. poll asks one question until the answer changes; repeat asks different questions, each one chosen by the last reply.
calls is a list in the grammar of api/run.yaml itself, and it is the turn: each call is made in order, and a later call reads an earlier one as calls.<name>, exactly as it does at the top level. previous is the turn before this one, the same names, and is null on the first turn, so previous ? … : … asks whether there is one. until is asked after every turn, over that turn’s calls. True ends the walk. Two bounds, both borrowed from the agent node’s own tool loop. maxTurns is the author’s limit on turns. stuckAfterRepeats stops a walk whose turn is the same as the one before it that many times running, the same call with the same arguments, because a loop that keeps choosing the same branch is not getting anywhere and maxTurns alone would let it spend every turn doing so. The platform adds a ceiling of 20 turns whatever maxTurns says. The reply is { last, turns, stopped }, not the last turn alone. last is the final turn’s calls, where the answer is. turns is every turn’s calls in order: the path taken, which is the lineage of the answer and must never be thrown away. stopped says why it ended: until, maxTurns or stuck. Every turn is also a trace, so the path shows in the Executions dashboard as it runs. A turn cannot itself repeat. Each walk is bounded, but nested they multiply, twenty turns of twenty. A walk that needs a walk inside it is two nodes. Lint refuses it, and so does the executor. repeat does not change the node’s kind. As everywhere else, the last call of the node decides it.

state

Sometimes an entry in the list is not a request at all.
Read a cache, call the API only when it was cold, write the answer back. That is one sequence, so it stays in one list. merge is read-modify-write rather than a replace, because more than one writer shares a key and replacing it wholesale would drop their fields. drain takes items permanently, so nothing is processed twice. max defaults to 25 and is bounded so one run cannot pull an unbounded queue. Write key as the logical key with no prefix, usually built from scope. The deployment namespace is added by the platform, so a manifest cannot forget it and write somewhere nothing reads. save is the exception: it takes no key, and lint refuses one. It belongs to this run, and a later node reads it as saved.<nodeId>.

loop

LoopStart and LoopEnd are a pair, and loop is how each half keeps its place. It makes no request. key is the id of the LoopStart the loop belongs to, so LoopStart passes {{ scope.nodeId }} and LoopEnd passes the paired id from its own settings. The run is supplied by the platform, so a loop can never reach another execution’s state. One capability rather than a handful of state operations, which is the point. Expressing this by hand took eight different store operations, and the ordering lived in whoever wrote it.

presign

Makes no request at all. It computes signed URLs from the credentials and the clock.
A presigned URL moves the signature out of the header and into the query string, with an expiry, so the link alone is enough to fetch the object. The link is shareable, and the expiry is why that is safe: anyone holding it has that access only until it lapses. The reply is always an array, positionally aligned with for, even for one file. A listing needs a link per object and a single file needs one, and making those different shapes would push the difference onto every reader. It earns its place in the ordered list for the same reason state does: listing a bucket and then minting a link for each object found is one sequence.

transport: ws

transport: ws is for a service you hold a conversation with rather than call: realtime voice, above all. Three lists cover the whole lifecycle, and each holds messages you send.
open is a list because services disagree about how much of a handshake there is. One sends a single configuration message; another needs an ordered sequence where each step is only valid after the one before. close matters more than it looks. A service that expects a teardown and never gets one is left holding a session open, and billing for it. send is deliberately general rather than tool-shaped. toolExchange counts turns, and a turn is one model call, which is not an idea that exists on a socket held open for a whole conversation. A reactive send covers a tool result going back, a response asked for, or a keepalive, without importing the wrong model of time. A conversation can also be told things while it runs. An input declared with signal: CONTINUE is delivered to the socket that is already open, and the node is not started again. A send rule with on: input reacts to it, with the value in scope as input.value and the connector’s name as input.handle. This is how another node on the canvas answers a voice call: its output is wired into the voice node’s CONTINUE input, and the rule hands the answer to the service.
Every send rule and every events row also sees two things the conversation has accumulated. seen holds the latest event of each type, so a reply can name the request it answers. said holds the text the service has streamed so far, in order, as { type, text }. Audio is separate. api/audio.yaml binds a voice node to the platform’s audio lane, because binary audio cannot travel the same path as everything else. Everything that is not audio belongs in events.yaml as usual.

Declare what you use

A package declares these in requires, alongside auth and transport.
package.yaml
paginate, state and loop name the strategies or operations used. chunk and poll are switches. Lint refuses a node that uses a capability its package does not list. Without that, a paginated call would fetch one page and stop, a cache would look permanently cold, and a queue would never drain, with nothing anywhere reporting it.

Real examples

Next steps

Testing

Run the node against the real service before you wire it up.

api/run.yaml

Every field a call takes, generated from the schema.