Streaming and SSE Guide
The streaming layer enables low-latency UI updates without aggressive polling. Use SSE where users need live progress, live logs, or cross-tab invalidation.
SSE Endpoints You Should Know
| API | Endpoint | Typical use | Notes |
|---|---|---|---|
| activity-feed-api | GET /realtime/stream | Unified realtime stream for org/project/session updates | In some deployments this route is provided by a shared module and may be absent from generated Swagger |
| activity-feed-api | POST /realtime/streams/:connectionId/subscriptions | Add project/session scoped subscriptions | Scope-specific opt-in after stream connect |
| activity-feed-api | DELETE /realtime/streams/:connectionId/subscriptions/:scopeKind/:scopeId | Remove scope subscriptions | Used when switching project/session context |
| activity-feed-api | GET /realtime/sessions/:sessionId/stream | Dedicated high-frequency session stream | Intended for very high event-rate session surfaces |
| agent-session-api | POST /sessions/:id/threads/:threadId/messages | Stream agent turn output back to client | SSE response on a POST request; every frame carries the threadId it belongs to |
| agent-session-api | GET /sessions/:id/preview/apps/:appId/logs | Stream preview app stdout/stderr | Useful for live app diagnostics in session UIs |
| workflow-engine-api | GET /workflows/:slug/runs/:id/stream | Stream workflow run status transitions | Includes snapshot/status/heartbeat/terminal frames |
Reconnect and Cursor Model
Last-Event-ID for unified realtime
For unified realtime connections, clients send a JSON cursor map in Last-Event-ID.
1Last-Event-ID: {"global":"81234","org":"302","project":{"proj_1":"44"},"session":{"sess_1":"9"}}Expected behavior:
- Server replays within the supported replay window.
- If the cursor is too old, server emits a
resynccontrol event and closes. - Client should clear stored cursors and reconnect from a fresh baseline.
Heartbeats and liveness
- Unified realtime sends heartbeat control frames periodically.
- Client watchdog should reconnect if no frame arrives within expected timeout.
- Session/post streams can also emit heartbeat comments to keep intermediaries from closing idle connections.
Control Frames vs Data Frames
Control frames
Control frames are sent with SSE event: names such as:
- connected
- heartbeat
- subscribed
- unsubscribed
- resync
- scope_denied
- goodbye
Data frames
Data frames carry event envelope payloads and are typically handled on the default message channel. Client behavior should be event-type driven and idempotent.
Payload Contract: CloudEvents Over SSE
Unified realtime SSE (GET /realtime/stream) delivers CloudEvents-style envelopes as the data: payload.
event:is usuallymessagefor data frames.id:is set from the global sequence value when available.data:is a JSON envelope containing CloudEvents core fields plus Xema event-hub extension attributes.
For the CloudEvents standard, see: https://cloudevents.io/
What to expect in each data frame
Common core fields:
idsourcespecversiontypesubject(optional)time
Common Xema extension fields used by realtime delivery and cursoring:
ehorgidehprojectid(optional)ehuserid(optional)ehvisibility(internal,public,project,user)ehglobalseqehorgseqehprojectseq(optional)ehsessionseq(optional)
Example envelope
1{
2 "id": "c9d59f6e-76c8-4d5d-a9a2-29d16467d34f",
3 "source": "xema/workflow-engine-api",
4 "specversion": "1.0",
5 "type": "workflow.run.completed",
6 "subject": "project/proj_123",
7 "time": "2026-04-27T15:41:02.128Z",
8 "ehorgid": "org_123",
9 "ehprojectid": "proj_123",
10 "ehvisibility": "project",
11 "ehglobalseq": "918273",
12 "ehorgseq": "2210",
13 "ehprojectseq": "809",
14 "data": {
15 "runId": "run_42",
16 "workflowSlug": "engineering-standard"
17 }
18}Why This Is Documented Here
Swagger usually describes the HTTP route but not the live event envelope matrix in enough detail for stream consumers. Treat this page as the authoritative consumer guide for control-frame behavior, CloudEvents payload shape, and reconnect/cursor semantics.
Realtime Events You May Receive
This guide keeps no list of event types. The event catalogue is the source of truth, filtered to your organisation: see the event catalogue (GET /events/catalogue, or GET /events/catalogue/asyncapi for an AsyncAPI document).
Client Integration Pattern
1const source = new EventSource('/realtime/stream', {
2 // Use a fetch-based polyfill when you need Authorization + Last-Event-ID headers.
3});
4
5source.addEventListener('connected', (e) => {
6 const frame = JSON.parse((e as MessageEvent).data);
7 console.log('connected', frame.connectionId);
8});
9
10source.addEventListener('resync', () => {
11 // Drop local cursor state and force a full refetch strategy.
12 resetLocalRealtimeCursors();
13 source.close();
14 reconnectFromZero();
15});
16
17source.onmessage = (e) => {
18 const envelope = JSON.parse(e.data);
19 applyInvalidation(envelope.type, envelope);
20};When to Use SSE vs Polling
Use SSE when:
- users watch long-running workflow/session operations,
- multiple browser tabs need near-realtime coherence,
- log streams or token streams must feel continuous.
Use polling when:
- data changes slowly,
- eventual consistency is acceptable,
- infrastructure constraints do not justify persistent connections.
Previous: ← API Selection Guide Next: Activity Feed Realtime Frames →