SSE run streaming
One-way Server-Sent Events from the REST backend, pushing the live events of a workflow, composer, or assistant run to a single client. This is how you watch a run unfold and consume agent output token by token.
Socket.io collaboration
Bidirectional Socket.io from a separate realtime server, syncing the workflow canvas across everyone editing it — node moves, locks, cursors, and presence.
The two planes at a glance
How this differs from the product collaboration pages
This is the developer-facing realtime reference. If you are looking for the product experience rather than the wire protocol, you want a different page:- Canvas collaboration and Chat collaboration describe collaboration as a product feature — what multi-user editing looks like in the app.
- Realtime co-editing & external sync describes the builder UX — how edits sync while you work on the canvas.
- Realtime & collaboration model is the conceptual overview for a non-technical reader.
Plane 1 — SSE run streaming
Server-Sent Events stream the events of one run to one client over a long-lived HTTP response. The backend uses a raw streaming response withContent-Type: text/event-stream; it does not use any higher-level SSE framework. You start a run with a normal REST call, then open a listen stream for that run’s id.
SSE transport and frame format
Run-event streams use a data-only frame convention: each event is a singledata: line carrying JSON, terminated by a blank line, with no SSE event: line. The discriminator is the type field inside the JSON, not an SSE event name.
Run-event frame (data-only)
Cache-Control: no-cache, Connection: keep-alive, and X-Accel-Buffering: no so intermediaries do not buffer the stream.
One stream is the exception to the data-only rule. The chat-list feed at
GET /chats/stream uses named SSE events (a real event: line such as event: chat_list_updated) and carries chat-list invalidation — not run output or token chunks. It is documented for completeness on SSE run streaming; the run streams below are the ones you stream a run’s progress from.SSE authentication
SSE streams are REST endpoints, so they authenticate exactly like the rest of the API: theAuthorization: Bearer mx_live_… header plus the X-Organization-ID header. The run-listen endpoints require an owner or admin role (member is retired — see Roles & permissions), and each runs an ownership check before subscribing, so a run that is not in your organization returns 404, never 403. See Authentication for the full header reference.
Open a workflow-run stream
The SSE listen surfaces
There are three run-listen surfaces — one per run kind — plus the chat-list feed and a credentials helper. Eachlisten surface has an SDK method that wraps the raw stream into an async iterator.
Consume a workflow run the same operation three ways:
SSE event taxonomy overview
Each run kind emits its own set of eventtype values. The tables below are the overview; the per-field schema for every event lives on SSE run streaming, and the human-in-the-loop request/response contract lives on Human-in-the-loop (HITL) resume.
The published wire shapes differ from the backend’s typed event models in some cases (for example, a
node_update frame carries node and output on the wire, not node_id/node_type/status). Always build against the wire shapes documented on SSE run streaming, not against generated model types.- Workflow run
- Composer run
- Assistant run
Emitted by
GET /workflows/listen/{run_id}:A workflow
interrupt is resumed with POST /workflows/resume/{thread_id} and reuses the same run id. See the Interrupt node (HITL) and HITL resume.Terminal events, heartbeats, and the pause traps
Terminal events, heartbeats, and the pause traps
A few behaviors trip up consumers and are worth knowing before you read the per-event reference.
- Terminal set. The client-side terminal events are
done,error,cancelled, andinterrupted. Iteration stops on any of them.interruptanduser_input_requestare not terminal — they pause the run but keep the stream open. interruptedis history-only. A live composer or assistant interrupt does not publish aninterruptedframe and does not close the stream — the stream stays open with no terminal frame until a reconnect replays history. The live pause you observe is theuser_input_requestframe.user_input_requestnests its payload. Its real question payload sits one extra level deep (underdata.data), unlike every other event. The Python SDK shipsuser_input_request_from_event(event.data)to unwrap it; a naive read ofevent.data["kind"]returns nothing. See Human-in-the-loop (HITL) resume.- Two resume contracts. A workflow
interruptresumes with the same run id viaPOST /workflows/resume/{thread_id}. A composer/assistantuser_input_requestresumes viaPOST /composer/chat/{id}/resumeorPOST /assistant/chat/{id}/resume, which returns a new run id and a new stream url — you must re-listenon the new id. The resume body’sllmfield is optional in the schema but required at runtime (the endpoint returns400without it). - Heartbeats. A
{"type":"heartbeat"}frame is injected on each idle gap (a short interval, on the order of seconds) on the run streams. The Python SDK filters these out unless you passinclude_heartbeats=True; in JavaScript you receive them and should ignore any frame whosetypeisheartbeat.
Reconnects and delivery
Run events are fanned out through a shared server-side pub/sub layer, so any backend replica can serve a stream for any run, and each run’s recent history is buffered (with a short time-to-live) for replay. On (re)connect the listener replays the buffered history in order and then tails live — so a reconnect is replay-safe and a just-missed event is not lost. The SDKs do not auto-reconnect; if your connection drops, open the stream again and the history replay covers the gap. See SSE run streaming for the replay window and System status & health for liveness.SSE error envelopes
A non-2xx on connect surfaces as a typed SDK exception before the first frame (404 → not found, 401 → authentication, 403 → permission, 429 → rate limit). The error body for these REST failures is the standard FastAPI shape, {"detail": "<message>"}. Once the stream is open, a mid-stream failure arrives as a final data: frame whose type is error rather than as an HTTP error.
Because workflow runs, composer, and assistant are managed-usage surfaces, they also pass through the billing admission gate, which can deny a run before the stream opens with a 402, 403, or 429 flat DenialEnvelope ({code, layer, key, current, limit, reason}). That is a different shape from {"detail": …}. For all three error-envelope shapes and which surface emits each, see Errors & status codes; for the gate itself see Usage gating & limits.
Plane 2 — Socket.io canvas collaboration
The collaboration plane runs on a separate Socket.io server and keeps everyone editing the same workflow canvas in sync. It is bidirectional: clients emit commands (move a node, acquire a lock), the server validates and broadcasts the result to the other editors in the room. This is the plane behind Canvas collaboration and the builder’s Realtime co-editing.Socket.io transport and connection lifecycle
Clients connect over Socket.io (WebSocket, with a polling fallback). On a successful handshake the server joins you to a per-user room and a per-organization room, then emits, in order, aconnected event ({userId, userName, color, organizationId}) and a workflows_list event for your organization. You enter a specific canvas by emitting join-workflow, which replies with a room snapshot.
Socket.io authentication
Socket.io does not use HTTP headers for auth. The same two values you would send as REST headers go into the Socket.io handshakeauth payload instead: a token and an organization id.
Connect to the collaboration server
connect_error (with a message such as Authentication required, Organization ID required, Invalid token, or Not a member of this organization) — not as an in-band error event. See Auth model: JWT vs API key and Org context & X-Organization-ID for the underlying auth model.
The collaboration plane is consumed by the ModuleX web app, which authenticates the handshake with a Clerk session token rather than an
mx_live_ API key. The neutral placeholders above (COLLAB_SERVER_URL, CLERK_SESSION_TOKEN) stand in for your deployment’s values. The exact production collaboration-server hostname is not standardized in these docs — see Open questions.Socket.io authorization and rate limits
Most write commands pass two gates: a per-user rate limit and a write-permission check that requires an owner or admin role (member is read-only and retired as a first-class role — see Roles & permissions). The gates are not uniform: cursor events are ungated, unlock requires write permission but is not rate-limited, and workflow:external-sync has no gate at all because its handler is the trust boundary. A rate-limited command returns an error with code rate_limited; a permission failure returns error with code permission_denied.
Socket.io event taxonomy overview
The collaboration plane has a large, structured event set. The full client-to-server and server-to-client reference — every payload, ack, conflict, and error code — lives on Socket.io collaboration events, and presence/lock/version semantics on Presence, locks & versioning. The overview groups events by purpose:Acks, conflicts, and version planes
Acks, conflicts, and version planes
- No ack callbacks. Socket.io’s callback-style acks are not used anywhere. Every response is a separate emitted event —
ack(for typed writes),patch_ack(for the legacy patch path),joined,conflict, orerror— not a callback reply. - Two version planes. An in-memory room version increments per accepted operation and is reported in
ack/conflict/broadcasts. A separate databaseedit_versionincrements per flush (batched, every couple of seconds) and is reported insaved. Thesavedversion can be numerically behind the version your client tracks from acks — do not overwrite local state fromsaved. - Conflicts. A
conflictframe (the only user-visible conflict signal) fires when your version is too far behind the room version; its payload always carriesresolution: "rebase". Database-layer conflicts are silent — they re-queue and retry server-side.
Socket.io error envelope
Collaboration errors use a{code, message, retryAfterMs?} shape — a different envelope from the REST/SSE {"detail": …}. The retryAfterMs hint appears only on the rate_limited deny and is a transport hint, not the billing 429 envelope. There is no unified error schema across the two planes; branch on the shape per transport. See Errors & status codes.
External sync is not a background channel
When the AI Composer or a REST call changes a workflow, those changes reach the canvas over the Socket.ioworkflow:external-sync event (which the server rebroadcasts as workflow:external-updated) — not over a background pub/sub channel.
Live external sync runs over the Socket.io path. See Realtime co-editing & external sync, Socket.io collaboration events, and Realtime & collaboration model.
Which plane do you need?
Use SSE run streaming when…
You want to watch a run — a workflow execution, a composer edit session, or an assistant conversation — and consume its events or token output as it happens, from a server, a script, or an SDK.
Use Socket.io collaboration when…
You are building canvas editing — moving nodes, connecting edges, acquiring locks, or showing presence and cursors — and need changes to propagate to other editors of the same workflow.
Open questions
A couple of realtime behaviors are not pinned in source and are tracked here so you do not build on an assumption:Browser EventSource authentication for SSE
Browser EventSource authentication for SSE
A raw browser
EventSource cannot send Authorization / X-Organization-ID headers, yet the run-listen endpoints require them. The SDKs and the web app use fetch-based readers instead, which can set headers. How a bare-browser EventSource is expected to authenticate (cookie, query parameter, or proxy) is not confirmed in source. Use a fetch-based reader for browser clients.Production collaboration-server hostname
Production collaboration-server hostname
The collaboration server is a separate service from the REST API at
https://api.modulex.dev. The exact production hostname for the Socket.io endpoint is not standardized in these docs; the source material references .io-suffixed hosts that conflict with the .dev hosts used elsewhere here. Use your deployment’s configured collaboration-server URL.Next steps
SSE run streaming
Frame format, the full event taxonomy, and the run lifecycle.
Human-in-the-loop (HITL) resume
Pause and resume semantics: request kinds, response kinds, and the new-run-id contract.
Socket.io collaboration events
The complete client-to-server and server-to-client event reference.
Presence, locks & versioning
Presence, cursors, node locks, and the patch/version/conflict model.