Stream a live response
For a chat or any long-running flow, waiting for the full response before showing anything feels slow. Streaming sends events as the run progresses — including token-by-token output from the model — so you can render a live, typing-as-it-goes experience.
Turn it on
Add "stream": true to the run body (or send Accept: text/event-stream). The response becomes a text/event-stream of events instead of one JSON blob — see the streaming reference for the full event list.
curl -N -X POST https://api.zerowidth.ai/1.0/flows/$ZW_FLOW/runs \
-H "Authorization: Bearer $ZW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":{"kind":"chat","messages":[{"role":"user","content":"Explain flows in one line."}]},"stream":true}'
Consume it
Read the stream and act on two events: node_update (token deltas as a model node runs) and the terminal run_complete.
const res = await fetch(`https://api.zerowidth.ai/1.0/flows/${flowId}/runs`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ input, stream: true }),
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buffer = ""
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// Events are separated by a blank line.
const events = buffer.split("\n\n")
buffer = events.pop() ?? ""
for (const raw of events) {
const type = raw.match(/^event: (.*)$/m)?.[1]
const data = JSON.parse(raw.match(/^data: (.*)$/m)?.[1] ?? "{}")
if (type === "node_update") appendToUi(data.data?.content ?? "") // token delta
if (type === "run_complete") finish(data.outputs) // the final result
if (type === "run_error") fail(data.error)
}
}
appendToUi grows the visible message as tokens arrive; run_complete carries the same payload the non-streaming call would have returned, so you can reconcile the final state. The stream closes with a done sentinel.
Which events you'll see, and when
node_start / node_complete bracket each node; node_update fires repeatedly inside a running node (token deltas from a model); node_error marks a node that failed (the run may continue). The run ends with exactly one of run_complete (success or partial) or run_error, then done. You only need node_update + the terminal event for a typing UI; the others are there for a live trace view.