Skip to content

Streaming & WebSockets ​

Most of the API is plain request/response JSON. Three kinds of endpoint are not, because the work behind them takes seconds to minutes and users want to watch it happen: streamed text replies, Server-Sent Events, and three WebSocket channels that push live progress.

Streamed text replies ​

Endpoints that generate an answer with a language model return it as it is produced, in chunks, with Content-Type: text/event-stream and Transfer-Encoding: chunked.

Not SSE-framed

Despite the content type, the body of these endpoints is the raw answer text — there are no event:/data: lines. Read the body incrementally and concatenate the chunks. Some clients (browsers' EventSource) will refuse this; use fetch with a ReadableStream, curl -N, requests with stream=True, or similar.

EndpointBody
POST /secure-gpt/chat/Answer text
POST /semantic-search/single-file-chat/, multi-file-chat/A JSON array of sources, immediately followed by the answer text
POST /semantic-search/all-files-chat/A JSON array of source paths, then ||, then the answer text
POST /semantic-search/advanced-search/, chat/, /spreadsheet-search/chat/Grouped sources, then the answer text
POST /microservices/dashboard-chat/Answer text (as produced by the dashboard service)
POST /workflow/run/stream/Answer text

Splitting sources from the answer in the document chats: scan the leading [ … ] with bracket matching (strings may contain brackets), parse it as JSON, and take everything after it (dropping a leading ||) as the answer. Until the closing bracket has arrived, nothing is displayable yet.

python
import json, requests

r = requests.post(
    "https://api.example.com/semantic-search/single-file-chat/",
    headers={"Authorization": f"Token {TOKEN}"},
    data={"query": "What are the key risks?", "document_name": "Q3-report.pdf",
          "file_uid": "1b6c1c1e-…"},
    stream=True,
)
buf = ""
for chunk in r.iter_content(chunk_size=None, decode_unicode=True):
    buf += chunk
    # display buf (minus the leading sources array) as it grows
print(buf)

Server-Sent Events ​

Two endpoints emit real SSE (event: + data: frames separated by blank lines). Standard SSE clients work.

EndpointEvents
POST /microservices/prompt-2-workflow/agent/ with stream: trueprogress, skeleton, workflow, message, error, done
GET /microservices/dedicated-agent/runs/{run_id}/events/The agent gateway's run events (tool calls, approvals, output)

The copilot stream is a POST, so browsers must use fetch and parse frames themselves; curl -N shows them directly.

WebSockets ​

Live progress is pushed over WebSockets served by the same host as the API, under /ws/…. Use wss:// on HTTPS deployments.

Connecting ​

There is no Authorization header on a WebSocket handshake, so the access token is passed as a query parameter:

text
wss://api.example.com/ws/chat/<conversation_id>?token=<access_token>

The server checks the token and that the user may see the resource, then accepts; otherwise it closes the socket immediately. Every channel answers {"type": "ping"} with {"type": "pong"} — send one every 30 seconds to keep proxies from dropping an idle connection, and reconnect with backoff if the socket closes (progress you missed is also available by polling the REST endpoints).

ChannelPathWho may connectPushes
Workflow run/ws/workflow/{workflow_id}/nodes/Owner, collaboratorsNode status changes, agent-node messages, definition updates
Chat AI turn/ws/chat/{conversation_id}The conversation's ownerStage telemetry, streamed partial answer, the final answer
Dedicated agent/ws/dedicated-agent/{agent_id}/The agent's ownerSame telemetry envelope, plus produced files and workflow specs
Workflow collaboration/ws/workflow/{workflow_id}/collab/Owner, collaboratorsPresence and the shared canvas document while people edit together

Workflow run channel ​

On connect the server sends the current node list; afterwards it pushes a frame whenever anything changes. Frames:

json
{"type": "workflow_nodes", "data": [ { …WorkflowNode… }, … ]}

The full node list (same objects as GET /microservices/workflow/{id}/nodes/), sent on connect, after every status change, and in reply to ping (as "type": "pong").

json
{"type": "agent_response", "node_id": "<node execution id>", "data": { … }}

A conversational node (an agent tool) sent something to the user — a question it needs answered (reply with POST /microservices/workflow/agent-message/) or a progress message. data.status is "telemetry" for progress frames (see the envelope below) and the payload otherwise carries the node's message.

json
{"type": "workflow_definition_updated", "data": { …json_spec… }, "actor": {"id": 57, "username": "omar"}}

Someone saved the workflow (or a restore/share update changed it). actor is absent for automated updates.

Chat AI channel ​

While a POST /microservices/agent/chat/ turn runs, the agent's progress is relayed here. Every frame shares one envelope:

json
{"type": "agent_response", "node_id": "chat", "data": { … }}

Progress frames have data.status == "telemetry" and a data.event_type:

event_typedata.eventMeaning
stage:thinking, stage:planning, stage:writing, …—A status line ("Thinking…")
stage:tool_started{tool, step_id, step_description}A tool call began
stage:tool_finished{tool, step_id, success, duration_ms, error}It finished
stage:partial_answer{text}A chunk of the answer as it is written — chunks are sometimes cumulative snapshots rather than pure deltas; if a chunk starts with what you already have, replace instead of append
stage:memory_recall{hit_count, top_kinds, top_texts}Memories injected at the start of the turn
stage:done—The turn finished
stage:memory_persisted, stage:memory_skipped{text, kind, memory_id, confidence, superseded_id}Post-turn bookkeeping: the agent stored (or decided not to store) a long-term memory
stage:error{message}The turn failed

The final answer arrives as a non-telemetry frame with data.message (and data.mgid / data.dashboard_id when the turn produced a chart or a Studio page). The HTTP response of the POST remains authoritative when it completes; the socket is what delivers the answer when the HTTP call timed out (504) or was queued.

json
{"type": "agent_response", "node_id": "chat",
 "data": {"status": "telemetry", "event_type": "stage:tool_started",
          "event": {"tool": "web_search", "step_id": "s1", "step_description": "Search for competitor news"}}}

{"type": "agent_response", "node_id": "chat",
 "data": {"status": "telemetry", "event_type": "stage:partial_answer", "event": {"text": "Q3 revenue grew "}}}

{"type": "agent_response", "node_id": "chat",
 "data": {"status": "completed", "message": "Q3 revenue grew **12.4%** year over year…", "turn_id": "7c1f…"}}

Ownership of a conversation_id is claimed by the first authenticated user who posts to it; the socket refuses anyone else.

Dedicated agent channel ​

The dedicated agent console is driven entirely by this channel: after POST /microservices/dedicated-agent/chat/stream returns 202, every stage event, partial answer and the final answer arrive here in the same envelope as the Chat AI channel, with two additional frames:

event_typedata.eventMeaning
stage:files{urls: […]}Media URLs of files the agent produced this turn
stage:workflow{json_spec, workflow_id, content_revision, summary, questions, suggestions, warnings, route, phase}A workflow the agent built or modified (phase: "skeleton" first, then the final spec) — used when the workflow copilot runs on a dedicated agent

Frames carry the conversation they belong to (chat_session_id), so one socket serves every conversation with the agent. Conversations run in parallel, so route frames by that id rather than assuming the latest message belongs to the conversation on screen.

Workflow collaboration channel ​

When several people have the same workflow open, the web app keeps their canvases in step through this channel: who is there and what they are pointing at, and every change to the canvas as it is made. It is the editor's own protocol — you do not need it to read or change a workflow from code. Save with PUT /microservices/workflow/{id}/ as usual; the live room picks the change up and merges it into everyone's canvas.

What it carries, for anyone building tooling around it:

  • Text frames (JSON), each with a t field. Presence (presence, peer_joined, peer_info, peer_left: cursors, selections, who is editing which node) is never stored, and the server stamps it with the sender's identity, so a client cannot pose as someone else. definition says a save landed (rev, the actor, and origin: collab for the room's own save, oob for anything else, such as the assistant, a restore or an API call). role says your access changed (for example to view only); losing access altogether closes the socket.
  • Binary frames — updates to the shared canvas document (Yjs updates), kept in order and relayed to everyone in the room.

One browser at a time — the saver — writes the shared document back to the workflow, through the ordinary save endpoint with its collab_session. Editors and owners may change the canvas; view-only collaborators receive it.

Close codeMeaning
4401Missing or expired access token — reconnect with a fresh one
4403No access to this workflow (or it was just removed)
4410The workflow is gone (moved to the trash)
4429Too many messages — slow down and reconnect

Choosing polling instead ​

Every live channel has a polling equivalent, which is simpler for batch integrations:

Instead ofPoll
Workflow run channelGET /microservices/workflow/{id}/nodes/ every few seconds until every node is COMPLETED/FAILED/SKIPPED, or GET …/webhook/status/ with a webhook credential
Chat AI channelJust wait for the POST to return (pass a generous timeout)
Dedicated agent channelGET /microservices/dedicated-agent/messages/ for the persisted answer; GET …/runs/{run_id}/ for runs

Finblade documentation