The three rules
Order by seq
seq is the per-Session sequence. It is the cursor you persist and the order you process in.Skip what you have
Delivery is at-least-once. In one Session stream the cursor filters replays; across unordered consumers, deduplicate on the globally unique
id.Settle on events
A closed connection is not a finished workflow. Read both the status event and its
stop_reason before deciding to stop following.Connecting and resuming
CHECKFU_API_KEY and CHECKFU_WORKSPACE_ID come from Get access; CHECKFU_SESSION_ID is the sess_… you are following. The TypeScript tab uses the TypeScript SDK with this client:
Last-Event-ID. A disconnect is not a reason to resend the input or its wait
response: reconnecting observes the original work; it does not restart it.
seq in id:, the event type in event:, and the full envelope in data:.
Process each event exactly once
Delivery is at-least-once, so an event can reach your code again after a reconnect or crash. Commit the event’s local changes and cursor in the same database transaction. This prevents duplicate local application of that event; it does not guarantee exactly-once effects in another service. The sample follows one sequential turn through its waits. For overlapping Runs, follow each Run’s recorded outcome instead of using Session idle as a completion boundary for all work. Use the TypeScript SDK’s stream rather than writing your own SSE parser. It resumes fromLast-Event-ID, honors Retry-After, bounds its reconnects, and cleans up the response body. The follower below keeps no cursor in memory: it reads the durable cursor, and each event is applied inside the same database transaction that advances it.
path=follow-session.ts
followSession with your SDK client, your database, the Session ID, and an apply function that writes the event’s local effect through tx.
Keep the cursor and applied state together across restarts. Before following
again, check that stored state for an already-recorded settlement. An absent
cursor starts this sample at zero and is appropriate for a new local
projection. If an existing projection loses its cursor, recover the matching
state and cursor or rebuild a fresh projection; do not replay external effects
from the beginning.
How the pieces behave under failure:
Your database adapter must serialize cursor updates for each Session, for example by creating the cursor row and locking it with SELECT ... FOR UPDATE in loadCursor. A transaction alone does not stop two followers from reading the same cursor and both applying the effect. Every follower must use the same locking rule.
- Crash after the effect, before the cursor. Both are in one transaction, so neither committed. The event replays and is applied once.
- The cursor write fails. The transaction rolls back the effect with it, and the error reaches your caller. Following again replays the same event.
- Duplicate delivery. The transaction re-reads the durable cursor, so an event at or below it is skipped without calling
apply. - Your own handler or database fails. The original error propagates without advancing the cursor, and
followSessionstops. Even a retryableCheckfuErrorraised by your application is not classified as a stream reconnect failure. Handle that failure at its application boundary. - The stream fails or ends early. When
isRetryableStreamFailure(error)is true, the SDK has already used its reconnect budget or the stream closed without a settlement event; wait, then callfollowSessionagain.followSessionresolves only after a settlement event, never because a connection closed. It resumes from the durable cursor. A non-retryableCheckfuError(an invalid key, a missing Session) needs a fix, not a retry. - Leaving early. Returning from the loop, or an exception inside it, closes the SDK iterator, which cancels the underlying request. A cleanup failure propagates when it is the only failure; if application or stream work already failed, that original failure takes precedence.
requires_action. An idle boundary whosestop_reasonisrequires_actionis intermediate, so the follower keeps going through the continuation.
Effects outside your database
A database transaction cannot include a call to another service, so a local receipt written after a remote call is not atomic with it. For a remote effect, pick one:- The provider supports idempotency keys. Send a key derived from the event, such as its
id. A replay repeats the call, and the provider returns the first result instead of acting twice. - It does not. In
apply, record the intent (eventid, target, request) in the same transaction as the cursor, and dispatch it from a separate worker after commit. That worker marks the intent done on success, and after a crash reconciles against the provider’s own state before it sends anything again.
Without the SDK
If you consume the stream directly, keep the same transaction rule and also handle what the SDK does for you: check the HTTP status before reading the body, retry only on429, 503, and other 5xx responses, honor and bound Retry-After, and resume with the last committed seq in Last-Event-ID. Use an SSE parser that follows the event-stream format: lines can end in CRLF, LF, or a lone CR, and a frame can be split across network reads, so splitting on "\n\n" is not enough. Cancel the response body when you stop reading.
Live previews do not move the cursor
The stream is durable-only unless you repeat theevent_deltas query parameter. An opted-in stream may also receive event_start and event_delta frames for live agent.message output, plus event_start for agent.thinking. These frames have no SSE id: line and no seq, are never replayed, and may be dropped. Do not save a cursor for them.
If the corresponding observation commits, the final persisted agent.message or agent.thinking event uses the preview’s event ID. Reconcile the temporary rendering to that event, perform durable side effects from the final envelope, and advance Last-Event-ID only from its numeric seq. A failed, interrupted, or lease-lost turn can leave no matching final event, so discard unmatched previews when the turn settles. A non-SDK client may also clear them at an observable HTTP reconnect; the SDK reconnects transparently, so recreating its iterator is the observable reconnect boundary. The TypeScript SDK enforces cursor neutrality automatically when you pass eventDeltas to events.stream.
Settlement
Use both the event type andstop_reason to decide whether your workflow follower can return:
These stop rules apply to the sequential follower above. An idle boundary does
not end the Session or prove that every overlapping Run has finished. Match
run.completed, run.failed, or run.canceled to the Run you are tracking.
requires_action and session.status_waiting are intermediate waits. A matching
approval decision, question answer, or tool result continues the same original
Run; it does not admit a replacement Run. Approval still requires current
authority, and an application-executed tool still needs its result. See
Handle an approval.
Backpressure
A stalled consumer does not lose events. Checkfu keeps at most one internal page in flight and lets the rest wait in the durable log, so a slow reader applies backpressure rather than dropping data. Reconnecting fromLast-Event-ID resumes from that log.
This means a consumer that falls far behind is fine. It also means the stream is not a substitute for a queue: if your processing is genuinely slow, read with the list endpoint on your own schedule instead.
Catching up without streaming
GET /v1/sessions/{id}/events pages the same log. limit defaults to 100 and caps at 1000.
The list supports two mutually exclusive entry points. Pass a decimal event seq as after to begin immediately after that durable position—the same coordinate carried by SSE Last-Event-ID. Or start without after and continue an ordinary paginated walk by echoing each opaque next_page value as page. Never send after and page together, and never try to construct an opaque page token from a sequence.
Use it for backfill, reconciliation after downtime, or rebuilding a projection from scratch. The stream and the list return the same persisted envelopes.
Next steps
Events
The envelope, the full event catalog, and the drive events you can post.
Fold a session into named views
Fold the stream into named views: messages, tool calls, ActionApprovals, the subagent tree, and the context ledger.