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

EventSource kindTriggerableWaitableSent whenMeta fields
artifact.createdartifactYesYesAn artifact is produced during a run.artifact_id, loop_run_id, kind, content_type
email.receivedemailYesYesEmail arrives at a Mobius agent address.agent_id
http_trigger.receivedhttp_triggerYesYesAn inbound request reaches an HTTP trigger URL.trigger_id, http_handle
interaction.createdinteractionYesYesA Mobius interaction opens.interaction_id, kind, target_user_ids
interaction.resolvedinteractionYesYesAn interaction completes, is cancelled, or expires.interaction_id, kind, status, consumer_kind, responder_id, resolving_response_id
run.cancelledloop_runYesYesA loop run reaches cancelled.run_id, loop_id
run.budget_exceededloop_runYesYesA run halts at a checkpoint because spend reached its run budget.run_id, loop_id
run.completedloop_runYesYesA loop run reaches completed.run_id, loop_id
run.failedloop_runYesYesA loop run reaches failed.run_id, loop_id
run.progress_stalledloop_runYesYesThe duplicate-tool-call breaker trips during an agent turn.run_id, loop_id
loop.auto_pausedloop_runYesYesA loop auto-pauses after consecutive failed runs.run_id, loop_id
check.passedloop_runYesYesA check step records a passing verdict.run_id, loop_id
check.failedloop_runYesYesA check step records a failing verdict.run_id, loop_id
session.message.createdsessionYesYesA new message is posted in an agent session.session_id, agent_id, message_id, role
signal.<name>signalNoYesA signal is sent to a run-scoped wait.run_id, signal_name
table.row.deletedtable_rowYesYesA table row is deleted.table_name, table_id, row_id
table.row.insertedtable_rowYesYesA table row is inserted.table_name, table_id, row_id
table.row.updatedtable_rowYesYesA 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.updated

Use 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.

EventWhy it exists
interaction.http_subscriber.dispatchDispatches an interaction callback to an HTTP subscriber.
loop.http_trigger.receivedStarts the asynchronous handler for an HTTP trigger.
loop.run_allocation.completedRecords a worker-reported completed run allocation.
loop.run_allocation.failedRecords a worker-reported failed run allocation.
loop.schedule.tickStarts the asynchronous handler for a schedule trigger.
loop_run.continueRe-executes a run after a step lease expires.
loop_run.resumeResumes a run when a timer or signal is due.
run.progress_initialStarts server-side progression for a newly persisted run.
run.resumeResumes a suspended run from the older resume path.
schedule.tickRecords 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.

EventSent whenPayload to expect
run.startedThe run is created and execution begins.Run ID, loop/version identifiers, source fields.
run.suspendedThe run is waiting on a timer, event, interaction, or worker-owned work.Step key, wait kind, wait details.
run.resumedA suspended or recovered run starts moving again.Step key, recovery action, attempt, and resume reason.
run.completedThe run reaches completed.Final result context.
run.failedThe run reaches failed.Error text, step id when known, error type.
run.cancelledThe run is cancelled.Reason and cancellation metadata.
run.budget_exceededA budget checkpoint halts the run.credit_spent, credit_budget, percent_used, and halted step.
run.progress_stalledThe duplicate-tool-call breaker trips.Tool name, duplicate count, limit, step id, and attempt when retry continues.
loop.auto_pausedThe loop circuit breaker pauses the loop after consecutive failed runs.Consecutive failure count and threshold.
step.startedA step begins.Step key and step kind.
step.suspendedA step opens a wait.Step key, wait kind, subscription or interaction details.
step.resumedA suspended step resumes.Step key and kind.
step.completedA step finishes successfully.Step key and output.
step.failedA step fails.Step key, error, and error type.
step.retriedA 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.skippedA conditional loop step does not start its child run.Step key and kind.
wait.openedA sleep, wait_for_event, or interaction wait is registered.Wait kind, subject, deadline, and step id.
wait.resumedA wait receives the payload that resumes it.Resolved event, signal, or interaction payload.
wait.timed_outA wait reaches its timeout.Step key, wait kind, and timeout reason.
interaction.requestedAn interaction step creates an interaction.Interaction ID, protocol, targets, and prompt facts.
interaction.respondedA run-backed interaction resolves and resumes the step.Interaction ID, response value, and responder facts.
action.calledAn action step dispatches a server, worker, or environment action.Action name, step id, and parameters metadata.
action.completedAn action returns successfully.Action name, step id, and result.
action.failedAn action attempt fails.Action name, step id, error, and error type.
action.retriedA worker-executed action is requeued for another attempt.Action name, attempt, max attempts.
action.resultLegacy compatibility record for action result rendering.Same result shape as the action completion path.
artifact.createdA run-linked artifact is available.Artifact ID, name, content type, run and step lineage.
check.passedA check step records a green verdict.Step key, verdict, on_fail, assertion results, and evidence references.
check.failedA check step records a red verdict.Step key, verdict, on_fail, failed assertions, and evidence references.
generation.deltaA live generation emits answer text or summarized thinking.delta is {"text":"..."} or {"type":"thinking","thinking":"..."}.
limit.reachedA runtime or billing cap stops the run.Limit kind, configured cap, and observed usage.
usage.recordedUsage 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.

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.

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.

Follow a loop run

  1. Start or fetch a run.
  2. Open GET /runs/{run_id}/events?after_sequence=N with Accept: text/event-stream.
  3. Persist only durable SSE id: values as the run cursor.
  4. Treat generation.delta as a preview.
  5. Reconnect with the last durable cursor after an unexpected disconnect.
  6. On run.completed, run.failed, or run.cancelled, fetch GET /runs/{run_id} and GET /runs/{run_id}/steps if 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.completed

The 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