Develop
Event catalog
Mobius has two event planes.
Source events are durable organization events. Mobius records one whenever something notable happens in the organization or in a connected provider.
Session-stream frames are the live and durable records of a conversation. Chat UIs and embedded clients read that stream. If you are building an interface, this is the plane you want; skip to session-stream frames.
Source event envelope
Source events use one normalized envelope:
{
"event_type": "table.row.inserted",
"source_kind": "table_row",
"source_id": "tbl_01...",
"event": {
"table_id": "tbl_01...",
"row_id": "row_01..."
},
"meta": {
"event_type": "table.row.inserted",
"table_id": "tbl_01...",
"row_id": "row_01..."
}
}The normalized payload is at event.*. Routing fields are at meta.*.
The Meta fields column below lists fields copied to meta.* for routing. It
is not the complete event.* payload. Read the live catalog schema when you
need fields specific to one event.
Public source events
| Event | Source kind | Recorded when | Meta fields |
|---|---|---|---|
email.received | email | Email arrives at a Mobius agent address. | agent_id |
interaction.created | interaction | A Mobius interaction opens. | interaction_id, kind, target_user_ids |
interaction.resolved | interaction | An interaction completes, is cancelled, or expires. | interaction_id, kind, status, consumer_kind, responder_id, resolving_response_id |
memory.entry.created | agent_memory | An agent memory entry is created. | agent_id, user_id, memory_entry_id, memory_key, version |
memory.entry.updated | agent_memory | An agent memory entry is updated. | agent_id, user_id, memory_entry_id, memory_key, version |
memory.entry.deleted | agent_memory | An agent memory entry is deleted. | agent_id, user_id, memory_entry_id, memory_key, version |
session.message.created | session | A new message is posted in an agent session. | session_id, agent_id, message_id, role |
table.row.inserted | table_row | A table row is inserted. | table_name, table_id, row_id |
table.row.updated | table_row | A table row is updated. | table_name, table_id, row_id |
table.row.deleted | table_row | A table row is deleted. | table_name, table_id, row_id |
interaction.resolved fires for every terminal interaction, including ones
bound to an agent tool or an HTTP subscriber. Check event.status before acting
on one: cancellation and expiry resolve an interaction too, and neither is
approval. Filter event.consumer_kind == "none" to handle only standalone
requests.
Integration source events
Integration events are provider-scoped and organization-aware. The event catalog in the app and API is the source of truth for which provider events are active in your organization.
Provider events follow this pattern:
<provider>.<resource>.<verb>Examples:
github.pull_request.opened
github.pull_request.closed
linear.issue.created
slack.event
gmail.message.received
jira.issue.updatedInternal source events
These event types exist in the source_events table for runtime processing,
but they are internal. They derive no public envelope.
| Event | Why it exists |
|---|---|
http_trigger.received | Records an inbound request at the public receive endpoint. |
interaction.http_subscriber.dispatch | Dispatches an interaction callback to an HTTP subscriber. |
Session-stream frames
The v2 transcript stream
(GET /sessions/{session_id}/transcript/stream) is the canonical protocol for
embedded chat. It bootstraps authoritative messages, turns, and pending human
interactions, then tails state changes as idempotent upserts. Treat the SSE
id: and resume_cursor as opaque watermarks. Reconnect with ?cursor=... or
Last-Event-ID; never parse or increment a cursor yourself.
| Frame | Fold into state | Payload to expect |
|---|---|---|
message.upsert | Replace the message by id. | The complete transcript message and its content blocks. |
message.block | Replace one content block by message_id and content_index. | A complete text, thinking, tool-use, or tool-result block. |
message.block.patch | Merge fields into one content block. | Tool status, free-form progress, or resolved_action. An open_interaction wait uses status: "waiting" and progress.interaction_id. |
message.delta | Append live text or thinking to one block. | text or thinking; keep the two buffers separate. |
turn.upsert | Replace the turn by id. | Turn phase, errors, usage, and optional wait. An interaction wait includes interaction_id, tool_call_id, and optional expires_at. |
interaction.upsert | Replace the interaction by id. | The full interaction record. Pending, submitted, resolved, expired, and cancelled states are pushed on the same session stream. |
stream.ready | Mark bootstrap/replay complete. | session_id and the current resume_cursor. Derive live phase only after this frame. |
stream.end | Apply the close policy. | idle means the session settled; rotate means reconnect immediately with the same cursor. |
The JSON snapshot at GET /sessions/{session_id}/transcript returns the same
state model: messages, turns, pending interactions, and a resume cursor. Fold
all snapshot pages before attaching the stream. A final snapshot removes stale
pending interactions that are no longer present; terminal interaction upserts
may remain in local history.
message.delta is a live preview. message.upsert, message.block,
turn.upsert, and interaction.upsert are durable state you can rely on. The
turn.* pulses and tool telemetry on the older v1 stream are live-only.
When mobius.open_interaction suspends a turn, three related projections
arrive: the turn's wait.interaction_id identifies what blocked, the
interaction.upsert contains the renderable prompt and response contract, and
the waiting tool block's progress.interaction_id identifies where the prompt
belongs. Respond with POST /interactions/{interaction_id}/respond. Continue
folding the stream: a terminal interaction upsert clears the prompt and the
turn resumes without polling.
Tool calls and results remain ordinary transcript content blocks, paired by
tool call ID. There is no separate presentation contract for tools or custom
actions: render the tool-use input and tool-result output, shaped by the
action's output_schema.
The older GET /sessions/{session_id}/stream endpoint remains available for
existing v1 consumers. It uses numeric after_sequence cursors and mixes
durable message rows with best-effort turn.*, preview, generation, and tool
telemetry. New embedded-chat integrations should use the v2 transcript stream;
v1 does not carry interaction.upsert frames.
Follow a session turn
- Invoke the agent or start the session turn, then store the returned opaque
resume_cursorbefore acknowledging upstream work. - Fetch
GET /sessions/{session_id}/transcriptfrom that cursor, followingnext_page_tokenuntilhas_moreis false. - Fold the snapshot, then open
GET /sessions/{session_id}/transcript/stream?cursor=.... - Fold every known frame by its record or block key. Ignore unknown frame types so additive protocol changes remain compatible.
- Persist each delivered SSE
id:as the new opaque cursor. - On a disconnect or
stream.end {reason:"rotate"}, reconnect with the same cursor. Onstream.end {reason:"idle"}, stop for request/response use or reopen after a courteous delay for a long-lived follower. - Prefer the TypeScript SDK's
SessionChator transcript watcher when you do not need to own cursor, paging, and reconnect behavior directly.
Next
- Read what a provider emits from source events.
- Put a chat window in your own software with agent invocation.
- Follow the lower-level protocol in Sessions and turns.