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

APIEndpointTypical useNotes
activity-feed-apiGET /realtime/streamUnified realtime stream for org/project/session updatesIn some deployments this route is provided by a shared module and may be absent from generated Swagger
activity-feed-apiPOST /realtime/streams/:connectionId/subscriptionsAdd project/session scoped subscriptionsScope-specific opt-in after stream connect
activity-feed-apiDELETE /realtime/streams/:connectionId/subscriptions/:scopeKind/:scopeIdRemove scope subscriptionsUsed when switching project/session context
activity-feed-apiGET /realtime/sessions/:sessionId/streamDedicated high-frequency session streamIntended for very high event-rate session surfaces
agent-session-apiPOST /sessions/:id/threads/:threadId/messagesStream agent turn output back to clientSSE response on a POST request; every frame carries the threadId it belongs to
agent-session-apiGET /sessions/:id/preview/apps/:appId/logsStream preview app stdout/stderrUseful for live app diagnostics in session UIs
workflow-engine-apiGET /workflows/:slug/runs/:id/streamStream workflow run status transitionsIncludes 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.

http
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 resync control 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 usually message for 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:

  • id
  • source
  • specversion
  • type
  • subject (optional)
  • time

Common Xema extension fields used by realtime delivery and cursoring:

  • ehorgid
  • ehprojectid (optional)
  • ehuserid (optional)
  • ehvisibility (internal, public, project, user)
  • ehglobalseq
  • ehorgseq
  • ehprojectseq (optional)
  • ehsessionseq (optional)

Example envelope

json
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

ts
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 →