API
Event catalog
Mobius has three event planes. Source events are durable project events.
event triggers and wait_for_event steps can match them.
Run-stream events are per-run timeline records. Operators, server-sent events (SSE), and the run detail page all read that stream.
Session-stream events are live and durable frames for agent messaging and agent sessions. Chat UIs and embedded clients read that stream.
Do not use run-stream events as triggers directly. When a run reaches a
terminal state, Mobius emits a separate source event such as run.completed
for loop-to-loop reactions.
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..."
}
}Event triggers start runs with the normalized event exposed at event.*.
Routing fields live at meta.*. Templates in step config reference both
namespaces directly: ${{ event.table_id }}, ${{ meta.run_id }}.
wait_for_event conditions evaluate against { event, meta } of the matched
event only.
Public source events
| Event | Source kind | Triggerable | Waitable | Sent when | Meta fields |
|---|---|---|---|---|---|
artifact.created | artifact | Yes | Yes | An artifact is produced during a run. | artifact_id, loop_run_id, kind, content_type |
email.received | email | Yes | Yes | Email arrives at a Mobius agent address. | agent_id |
http_trigger.received | http_trigger | Yes | Yes | An inbound request reaches an HTTP trigger URL. | trigger_id, http_handle |
interaction.created | interaction | Yes | Yes | A Mobius interaction opens. | interaction_id, kind, target_user_ids |
interaction.resolved | interaction | Yes | Yes | An interaction completes, is cancelled, or expires. | interaction_id, kind, status, consumer_kind, responder_id, resolving_response_id |
run.cancelled | loop_run | Yes | Yes | A loop run reaches cancelled. | run_id, loop_id |
run.budget_exceeded | loop_run | Yes | Yes | A run halts at a checkpoint because spend reached its run budget. | run_id, loop_id |
run.completed | loop_run | Yes | Yes | A loop run reaches completed. | run_id, loop_id |
run.failed | loop_run | Yes | Yes | A loop run reaches failed. | run_id, loop_id |
run.progress_stalled | loop_run | Yes | Yes | The duplicate-tool-call breaker trips during an agent turn. | run_id, loop_id |
loop.auto_paused | loop_run | Yes | Yes | A loop auto-pauses after consecutive failed runs. | run_id, loop_id |
check.passed | loop_run | Yes | Yes | A check step records a passing verdict. | run_id, loop_id |
check.failed | loop_run | Yes | Yes | A check step records a failing verdict. | run_id, loop_id |
session.message.created | session | Yes | Yes | A new message is posted in an agent session. | session_id, agent_id, message_id, role |
signal.<name> | signal | No | Yes | A signal is sent to a run-scoped wait. | run_id, signal_name |
table.row.deleted | table_row | Yes | Yes | A table row is deleted. | table_name, table_id, row_id |
table.row.inserted | table_row | Yes | Yes | A table row is inserted. | table_name, table_id, row_id |
table.row.updated | table_row | Yes | Yes | A table row is updated. | table_name, table_id, row_id |
signal.<name> is waitable but not triggerable. Use signals to resume an
existing run. interaction.resolved can either resume a waiting consumer or
start a new loop for channel-independent follow-up automation. Every terminal
interaction is triggerable, including ones already bound to a run, agent tool,
or HTTP subscriber. Filter event.consumer_kind == "none" when a trigger
should handle only standalone interactions.
Integration source events
Integration events are provider-scoped and project-aware. The event catalog in the app and API is the source of truth for which provider events are active in your project.
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.updatedUse exact names for narrow triggers. Use a trailing wildcard only when every event below a prefix should match:
triggers:
- kind: event
config:
event_type: github.pull_request.*Internal source events
These event types exist in the source_events table for runtime processing,
but they are internal. Do not use them in authored triggers or waits.
| Event | Why it exists |
|---|---|
interaction.http_subscriber.dispatch | Dispatches an interaction callback to an HTTP subscriber. |
loop.http_trigger.received | Starts the asynchronous handler for an HTTP trigger. |
loop.run_allocation.completed | Records a worker-reported completed run allocation. |
loop.run_allocation.failed | Records a worker-reported failed run allocation. |
loop.schedule.tick | Starts the asynchronous handler for a schedule trigger. |
loop_run.continue | Re-executes a run after a step lease expires. |
loop_run.resume | Resumes a run when a timer or signal is due. |
run.progress_initial | Starts server-side progression for a newly persisted run. |
run.resume | Resumes a suspended run from the older resume path. |
schedule.tick | Records a schedule tick before the public loop handler runs. |
If you need to react to a schedule, add a schedule trigger. If you need to
react to an HTTP request, add an http trigger.
Run-stream events
Run-stream events are stored on a single run and replayed by the run events API and SSE stream. They are what the Timeline tab renders.
| Event | Sent when | Payload to expect |
|---|---|---|
run.started | The run is created and execution begins. | Run ID, loop/version identifiers, source fields. |
run.suspended | The run is waiting on a timer, event, interaction, or worker-owned work. | Step key, wait kind, wait details. |
run.resumed | A suspended or recovered run starts moving again. | Step key, recovery action, attempt, and resume reason. |
run.completed | The run reaches completed. | Final result context. |
run.failed | The run reaches failed. | Error text, step id when known, error type. |
run.cancelled | The run is cancelled. | Reason and cancellation metadata. |
run.budget_exceeded | A budget checkpoint halts the run. | credit_spent, credit_budget, percent_used, and halted step. |
run.progress_stalled | The duplicate-tool-call breaker trips. | Tool name, duplicate count, limit, step id, and attempt when retry continues. |
loop.auto_paused | The loop circuit breaker pauses the loop after consecutive failed runs. | Consecutive failure count and threshold. |
step.started | A step begins. | Step key and step kind. |
step.suspended | A step opens a wait. | Step key, wait kind, subscription or interaction details. |
step.resumed | A suspended step resumes. | Step key and kind. |
step.completed | A step finishes successfully. | Step key and output. |
step.failed | A step fails. | Step key, error, and error type. |
step.retried | A step is retried: a transient absorption, an authored step retry, or an operator recovery. | Step key, attempt, retry_scope, error type, and retry metadata. |
step.skipped | A conditional loop step does not start its child run. | Step key and kind. |
wait.opened | A sleep, wait_for_event, or interaction wait is registered. | Wait kind, subject, deadline, and step id. |
wait.resumed | A wait receives the payload that resumes it. | Resolved event, signal, or interaction payload. |
wait.timed_out | A wait reaches its timeout. | Step key, wait kind, and timeout reason. |
interaction.requested | An interaction step creates an interaction. | Interaction ID, protocol, targets, and prompt facts. |
interaction.responded | A run-backed interaction resolves and resumes the step. | Interaction ID, response value, and responder facts. |
action.called | An action step dispatches a server, worker, or environment action. | Action name, step id, and parameters metadata. |
action.completed | An action returns successfully. | Action name, step id, and result. |
action.failed | An action attempt fails. | Action name, step id, error, and error type. |
action.retried | A worker-executed action is requeued for another attempt. | Action name, attempt, max attempts. |
action.result | Legacy compatibility record for action result rendering. | Same result shape as the action completion path. |
artifact.created | A run-linked artifact is available. | Artifact ID, name, content type, run and step lineage. |
check.passed | A check step records a green verdict. | Step key, verdict, on_fail, assertion results, and evidence references. |
check.failed | A check step records a red verdict. | Step key, verdict, on_fail, failed assertions, and evidence references. |
generation.delta | A live generation emits answer text or summarized thinking. | delta is {"text":"..."} or {"type":"thinking","thinking":"..."}. |
limit.reached | A runtime or billing cap stops the run. | Limit kind, configured cap, and observed usage. |
usage.recorded | Usage attribution is recorded for the run. | Category, quantity, step id, credit_cost, budget_cost, and cumulative run spend. |
Prefer action.completed for new consumers. Keep action.result handling only
for compatibility with older run records.
Durable run events expose sequence, and SSE frames for those events carry
id: <sequence>. Persist only that durable id as your after_sequence
cursor. generation.delta frames are live previews: they may include
delta_sequence and the deprecated preview alias sequence, but those values
are publisher-local ordering hints and are not replay cursors.
That split applies to the run stream, where generation.delta is the only live
preview. The session stream has a wider ephemeral set:
session.message.preview, tool.call, tool.result, and the turn.* pulses
are all live-only there. Read each plane's own table rather than carrying one
plane's durability rule to the other.
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.
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.
Follow a loop run
- Start or fetch a run.
- Open
GET /runs/{run_id}/events?after_sequence=NwithAccept: text/event-stream. - Persist only durable SSE
id:values as the run cursor. - Treat
generation.deltaas a preview. - Reconnect with the last durable cursor after an unexpected disconnect.
- On
run.completed,run.failed, orrun.cancelled, fetchGET /runs/{run_id}andGET /runs/{run_id}/stepsif final structured state is needed.
Example run stream
event: run.started
event: step.started
event: action.called
event: action.completed
event: step.completed
event: usage.recorded
event: run.completedThe stream is replayable by sequence number. If a client reconnects, ask for events after the last sequence it saw so the timeline stays complete.
Next
- Match events from triggers.
- Resume failed runs with recover a failed run.
- Inspect timelines from runs.