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 it 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
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 many emissions. Paging, batching, waiting on a job and remembering
between runs all work on either kind and change neither. 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.
Where each part lives
Anatomy of a Node walks through all of them.

