Skip to main content

Streaming

Items stream to clients over SSE as blocks execute. Every event has a sequence number, so clients can disconnect and resume without losing anything. This page covers the SSE protocol, event format, resume semantics, and client integration.

How it works

When a client invokes an action, the server starts executing blocks and streaming results immediately:

POST /api/flows/:kind/actions/:action  -->  202 { requestId }
GET /api/flows/:kind/requests/:requestId/stream --> SSE events

Events flow in real time:

event: item.added
data: { "item": { "type": "message", "role": "assistant", "status": "in_progress" } }

event: content.delta
data: { "itemId": "msg_1", "delta": { "text": "Hello" } }

event: content.delta
data: { "itemId": "msg_1", "delta": { "text": " there!" } }

event: item.done
data: { "item": { "type": "message", "role": "assistant", "status": "completed" } }

event: request.completed
data: { "status": "completed" }

The client assembles content progressively from deltas. Text appears token by token. When the request completes, the client refetches the state snapshot for the authoritative final state.

Stream events

EventMeaning
item.addedNew item in the stream. Contains the full item payload with status: "in_progress".
content.deltaText chunk appended to a streaming item (messages, reasoning).
content.addedNew content part added to an item (e.g., audio part on a message).
content.doneA content part finalized.
item.doneItem finalized with terminal status.
request.completedAll blocks finished. Request succeeded.
request.failedRequest failed with a terminal error.

Resume and replay

Every event has a sequence number. When a client disconnects — network blip, tab backgrounded, mobile app suspended — it can resume from exactly where it left off:

GET /api/flows/:kind/requests/:requestId/stream
Last-Event-ID: 42

The server replays all events after sequence 42, then switches to live streaming. No data loss. No duplicate events. No application-level retry logic needed.

You can also use the starting_after query parameter:

GET /api/flows/:kind/requests/:requestId/stream?starting_after=42

Both approaches produce the same result. Last-Event-ID is the standard SSE header. starting_after is a query parameter alternative for environments where setting headers isn't convenient.

React integration

On the React side, streaming is automatic. The useSession hook connects to the SSE stream, processes events, and updates items reactively:

const session = useSession(sessionId);

// Items update in real time as the stream delivers them
{session.items.map((item) => (
<ItemRenderer key={item.id} item={item} />
))}

// Filtered views
{session.messages.map(...)} // Only message items
{session.blockOutputs.map(...)} // Only block outputs

// Status
{session.isStreaming && <Spinner />}

No manual stream management. No event listeners. No reconnection logic. The hooks handle all of it.

Client SDK

If you're not using React, the client SDK provides direct SSE access:

import { createClient } from "@flow-state-dev/client";

const client = createClient({ flowKind: "my-app", userId: "user_1" });

// sendAction returns a requestId, then connect to the stream
const { requestId } = await client.sendAction("chat", { message: "Hello" });

See Client Overview for the full client API.