Skip to main content

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.

2 min read