Channels and events
A channel is the durable identity of one execution stream — an agent run, a LangGraph thread, a Claude session, a workflow instance, a business process. Each channel is an ordered, append-only event log.
An event is an immutable JSON blob with a type and optional payload. Events are never edited or deleted by normal operation; the log is the source of truth for what happened, in order.
Channels are cheap
Create a channel per logical execution. Forking, experiments, and lineage all operate on channels — naming them well is the foundation of every Actae workflow.
Cursors
Every event carries a cursor: its gapless, per-channel sequence number (1, 2, 3, …). Cursors are the currency of replay, subscribe-resume, and consumer-group acks.
- Replay and subscribe use exclusive semantics: pass
N - 1to read everything up to and including cursorN. - Cursors are per-channel — a cursor in one channel means nothing in another.
- State versions come from the server's global sequence; never assume per-channel gapless numbering for state.
Event records also carry a server-assigned global position (used for cross-channel ordering and lineage), but all cursor-relative APIs are per-channel.
State and transitions
Execution state — what a run knows so far — is persisted as versioned snapshots aligned with the event log. Three primitives cover most needs:
save_stateSaves a snapshot at a cursorManual, full controltransitionRecords an event and its resulting state in one server-side transactionThe state belongs to a specific eventAgentSessionSteps, auto-snapshots, fork/resumeInstrumenting a multi-step agent runAtomicity. A transition commits the event and the resulting state in one transaction — the two can never be observed apart. Expected-version and expected-cursor guards reject stale writers instead of silently overwriting progress: a write with a stale expected_version fails loudly with a version conflict.
Rule of thumb
Use transition when the state belongs to an event, AgentSession when you are instrumenting a run, and save_state (or StateManager) when you want full control.
Idempotency
Retries are the norm in distributed systems, so Actae makes them safe. Pass a stable operation_id (a UUID you own) with a record, transition, or publish: if the server already applied that operation, the original result is returned instead of a duplicate.
- Reusing an
operation_idwith different content fails loudly with409 idempotency_conflict. - Replay comparison is canonical — key-order-insensitive — so re-serialized payloads replay idempotently.
- For transitions, replay comparison also covers the snapshot
state, and replays are evaluated before the expected-* guards (a stale retry still resolves idempotently).
SDK sessions generate operation IDs deterministically from step content (UUIDv5 over scope/type/channel/step number/canonical content), so a crash-recovery re-drive of the same step replays, while changed content records anew.
Replay to live
The WebSocket subscription model removes the handoff bug: a subscriber catches up from a cursor (past events delivered in order), synchronizes, and continues receiving live events on the same subscription. The SDK exposes this as subscribe() with a cursor, or stream() for async iteration.
- Delivery is batched and ordered per channel.
- Auto-reconnect resubscribes all topics from their last cursors — no gaps, no duplicates, no application-built catch-up.
- The server does not echo a publish back to the connection that sent it; use
echo_self=Truefor single-client demos.
Retention and security
Channels can be deleted; retention and GC policies are configurable per deployment. The API is API-key-gated (dashboard sessions via JWT), with token-bucket rate limiting, and optional TLS/mTLS in production. Execution payloads from self-hosted instances are never uploaded to the control plane — the SaaS processes account, entitlement, and billing metadata only.