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

EventSource kindRecorded whenMeta fields
email.receivedemailEmail arrives at a Mobius agent address.agent_id
interaction.createdinteractionA Mobius interaction opens.interaction_id, kind, target_user_ids
interaction.resolvedinteractionAn interaction completes, is cancelled, or expires.interaction_id, kind, status, consumer_kind, responder_id, resolving_response_id
memory.entry.createdagent_memoryAn agent memory entry is created.agent_id, user_id, memory_entry_id, memory_key, version
memory.entry.updatedagent_memoryAn agent memory entry is updated.agent_id, user_id, memory_entry_id, memory_key, version
memory.entry.deletedagent_memoryAn agent memory entry is deleted.agent_id, user_id, memory_entry_id, memory_key, version
session.message.createdsessionA new message is posted in an agent session.session_id, agent_id, message_id, role
table.row.insertedtable_rowA table row is inserted.table_name, table_id, row_id
table.row.updatedtable_rowA table row is updated.table_name, table_id, row_id
table.row.deletedtable_rowA 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.updated

Internal source events

These event types exist in the source_events table for runtime processing, but they are internal. They derive no public envelope.

EventWhy it exists
http_trigger.receivedRecords an inbound request at the public receive endpoint.
interaction.http_subscriber.dispatchDispatches 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.

FrameFold into statePayload to expect
message.upsertReplace the message by id.The complete transcript message and its content blocks.
message.blockReplace one content block by message_id and content_index.A complete text, thinking, tool-use, or tool-result block.
message.block.patchMerge 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.deltaAppend live text or thinking to one block.text or thinking; keep the two buffers separate.
turn.upsertReplace 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.upsertReplace the interaction by id.The full interaction record. Pending, submitted, resolved, expired, and cancelled states are pushed on the same session stream.
stream.readyMark bootstrap/replay complete.session_id and the current resume_cursor. Derive live phase only after this frame.
stream.endApply 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

  1. Invoke the agent or start the session turn, then store the returned opaque resume_cursor before acknowledging upstream work.
  2. Fetch GET /sessions/{session_id}/transcript from that cursor, following next_page_token until has_more is false.
  3. Fold the snapshot, then open GET /sessions/{session_id}/transcript/stream?cursor=....
  4. Fold every known frame by its record or block key. Ignore unknown frame types so additive protocol changes remain compatible.
  5. Persist each delivered SSE id: as the new opaque cursor.
  6. On a disconnect or stream.end {reason:"rotate"}, reconnect with the same cursor. On stream.end {reason:"idle"}, stop for request/response use or reopen after a courteous delay for a long-lived follower.
  7. Prefer the TypeScript SDK's SessionChat or transcript watcher when you do not need to own cursor, paging, and reconnect behavior directly.

Next