kind is where you say which.
A PromiseNode is stateless. It is called, it answers, and it is gone. The docs call
that settling: the node answers once and is done. Nothing lives
between the call and the answer, so there is nothing to hold: an API call, a transform, a
read or a write.
A CallbackNode is stateful. It stays alive while it works and holds what it has done
so far: the text streamed to this point, which turn of a tool loop it is on, what it is
waiting for. It emits as it goes, meaning it puts a value on an output more than once, and settles when
it is finished.
Settle it first, because everything else about the node follows from it. You declare it at
the top of
node.yaml:
It is declared, and it is checked
kind could be inferred from the rest of the file. You write it anyway, because stateful
or stateless is the first thing anyone asks about a node.
Lint checks the declaration rather than trusting it. A node is a CallbackNode when
any of these is true:
- its last callβs transport streams (
sseorws) - it declares a
toolExchange, which is a multi-turn loop by definition - an input declares a
SPAWNsignal, which only a node that spawns a long-lived actor does
PromiseNode while doing any of them and lint names the one that contradicts you.
Only the last call counts. It is the nodeβs answer, so it alone decides whether the node
streams. Every earlier call settles by definition, which is why a node can look up a record,
page through a list and then stream its reply.
Many requests is not state. Paging, batching, waiting on a job and remembering between
runs all work on either kind and change neither. The platform does that looping inside one
call, and the node still answers once. A node that walks forty pages still
settles once if its last call settles. Beyond one request covers
them.
A node that answers once
transport: json says the reply arrives as one body. The events table maps that body onto
the nodeβs outputs.
api/run.yaml
api/events.yaml
match. There is one body, and the rows shape it.
A node that keeps answering
transport: sse says the reply arrives as a stream of events. Now each row names the event
type it fires on.
api/run.yaml
api/events.yaml
accumulate: true emits the running total, not the fragment. A stream of single words is
almost never what a downstream node wants. It wants the answer so far.
throttleMs bounds how often it emits. A long answer would otherwise produce hundreds of
events. Nothing held back is lost, because whatever is pending is flushed when the run ends.
The last row uses from: complete, which fires once at the end over everything emitted. That
is how a streaming node also produces a settled final value.
How the reply arrives
xml is for the services that never moved, and it parses to the same shape as JSON so an
events row reads it identically.
encoding is a second axis. transport says how the reply is framed, encoding says how
the values inside it are spelled. dynamodbJson is the one to know: DynamoDB carries
{ name: { S: "Ada" } } where you want { name: "Ada" }, and the platform translates both
ways so a node never writes type tags.
Where each part lives
Next steps
Anatomy of a node
The folder, the files in it, and how a call is described.
node.yaml
Where
kind sits, with every other envelope field.
