Skip to content
SurogateDocs

Event types

The Sessions UI’s EVENTS tab and the GET /v1/sessions/{id}/events API both return a stream of typed events. This page lists what you’ll see and what each event tells you. The authoritative list is the harness EventType enum.

TypeWhenPayload
user.messageInput was added to the sessioncontent. Channel messages carry a source object (platform, chat_id, chat_type, user_id, user_name, thread_id, ts) plus a principal_user_id / principal_service_account_id stamp. Server-generated messages carry synthetic instead (e.g. outcome_continuation)
llm.requestOne LLM call started — the agent may make many per turn—
llm.deltaA streamed token chunk. Very high volume; filter it out when reading a timeline—
llm.heartbeatThe upstream went quiet past the heartbeat interval but is still inside the stale-timeout window — the model is reasoning silently, the stream is not dead—
llm.thinkingAn extracted reasoning block (provider-dependent) — the complete snapshot for the iteration, not a deltareasoning
llm.responseThe model answered. The user-visible text is message.content — there is no separate final-answer eventmessage ({role, content, tool_calls?}), model, input_tokens, output_tokens, reasoning_tokens, cache_read_tokens, finish_reason, context_window; cost_usd when the serving model has a catalog rate, cost_unpriced_model when it does not
ask_user_question.responseThe user answered an ask_user_question prompttool_call_id, responses
channel.formThe agent sent a WhatsApp form with send_form. It is delivered to the channel, and it is the record a submitted form’s flow_token is checked againstform, flow_token, interactive
iteration.summaryOne LLM iteration finished — a one-line imperative recap—
turn.summaryThe final iteration of a turn finished — recap plus curated artifact list—
TypePayload
tool.calltool_call_id, name, arguments (path-sanitized — the workspace absolute path is replaced with __WORKSPACE__), plus checkpoint_hash on file-mutating tools
tool.resulttool_call_id, name, content (the result body, often JSON-encoded), elapsed_ms

Tool errors ride in tool.result — there is no tool.error event. A failure shows up as a content like {"error": "Tool execution failed: …"}. A rejection that never reached the tool at all — a governance denial or an allow-list block — carries elapsed_ms: 0.

For most debugging, the EVENTS tab’s TOOLS sub-view filters these for you. Programmatically there is no server-side filter — pull the log and select on event.type yourself (see Querying via the API).

TypeWhenPayload
skill.invokedThe harness expanded a /<skill> … user message: it loaded the skill server-side and inlined the body before the LLM saw the message. The original text stays in the preceding user.messageskill, raw_message, staged_at (workspace path where the skill’s supporting files were staged, or null); a SkillOpt rollout adds override_source, skillopt_run_id, candidate_id. Emitted at most once per user message — crash-recovery re-expansion does not re-emit
todo.updatedThe agent wrote its plan via the todo tool. The full list is written every time, so recovery reads the latest row rather than folding a delta logtodos
skill.stepThe agent marked a step of a procedure skill as started, completed or skipped, with skill_step. The skill canvas replays a session over the skill’s graph from theseskill, step, status, tool_call_id

A skill the SKILLS tab shows as loaded but with no skill.invoked was simply never slash-invoked — the model was free to reach for it and didn’t.

When a session starts sub-agents with agent, their outcomes flow back as events in the parent’s log.

TypeWhen
delegation.startA sub-agent’s child session was created and queued
delegation.complete / delegation.failedThe sub-agent’s run ended well, or failed or timed out
delegation.staleA foreground sub-agent went quiet past a threshold (once per child)
agent.notificationA background sub-agent, background command or monitor ended or reported. The session reads it as a notification at its next model request; it wakes a session at rest
worker.spawned / worker.complete / worker.failedA project’s thread started, reported a turn, or failed: the coordinator reads each as a notification, the thread’s files in a <files> block
coordinator.messageA project coordinator’s follow-up to one of its threads. The thread reads it as a message typed into it, but it is never the user’s
history.redoA project thread was told to redo its change to files its landing left out
history.redo_heldThe wake for a thread’s redo was held back, by its user’s limit or because ops was unreachable. The thread rests and its next turn redoes the change
board.noteA note passed admission on the writer’s session
board.updateThe rendered snapshot or delta that re-enters a reader session’s conversation on replay

See Coordination for how the board admits notes.

Emitted on the coordinator chat session; the mission dashboard polls them.

TypeWhen
mission.definedA mission was created
mission.evaluation.start / mission.evaluation.endOne mission evaluation pass
mission.continuationThe mission queued another round
mission.paused / mission.resumed / mission.cancelledOperator-driven mission state changes
mission.refinement_proposedThe evaluator proposed a new rubric instead of a verdict, and the mission paused to wait for a decision. This event is where /mission accept reads the new rubric from. Payload: mission_id, old_rubric, proposed_rubric, evidence, held_verdict, explanation
mission.amendedThe operator accepted the proposed rubric with /mission accept. Payload: mission_id, old_rubric, new_rubric, evidence, typed_at
TypeWhen
policy.deniedA tool call was blocked by the governance gate. Payload: tool, reason, timestamp
policy.allowedTwo shapes. Gate pass-through, emitted only when the deployment enables governance.log_allowed (off by default; doubles event volume): tool, check: "governance_gate", timestamp. Approved retry of a gated call: tool_name, reason: "human approval", tool_call_id
disclosure.presentedA messaging channel (Slack / Telegram / WhatsApp) posted the AI disclosure as the first message of a new conversation. Payload: level, channel, delivery: "channel_message"
disclosure.confirmedA web user accepted the disclosure banner. Payload: level, source: "web_banner"
security.injection_blockedThe prompt-injection detector refused a user message sent through the sessions API (web chat, API channel, and comment mentions in office documents, which go the same way); /code commands are not screened. The request gets a 422 and no user.message is recorded, so this event is what shows the block in the timeline. Payload: source (web_channel or api_channel), explanation, threat_level, confidence, injection_type, matched_patterns (up to 10), content_preview (first 2000 characters)

A denial of a tool named in the policy’s require_approval list is overridable: alongside policy.denied it emits inbox.governance_gate and hands the model policy_blocked_overridable rather than a hard error, so the call can be retried after a human decides.

The two disclosure.* events are the Art. 50 compliance evidence trail — see Governance & AI disclosure.

When the agent’s CONFIG has Saga enabled:

TypeWhen
saga.startSaga activated at session start. Payload: saga_id, session_id, timestamp
saga.step_begin / saga.step_committed / saga.step_failedPer-step tracking. Payload: saga_id, step_id, tool_name, state, timestamp, plus tool_call_id, arguments, compensation_tool, compensation_args, checkpoint_hash, result, error when set
saga.compensateA rollback fired. Payload: saga_id, steps_rolled_back, reason, timestamp, and failed_steps when present — a non-empty failed_steps is the escalation signal: a compensation step itself failed and an operator must intervene
saga.completeSaga finished. Payload: saga_id, status, steps_executed, timestamp

There is no saga.escalated event. Escalation is a saga state, surfaced through failed_steps on saga.compensate.

TypeWhenPayload
expert.delegationThe base LLM called consult_expertexpert, task (truncated to 500 chars), tools, max_iterations
expert.resultThe expert finished successfullyexpert, success (always true), iterations_used, content (the deliverable)
expert.failureThe expert errored or exhausted its iteration budgetexpert, success (always false), iterations_used, error
expert.endorse / expert.overrideA user or judge rated an expert.result thumbs-up / thumbs-down through the feedback endpoint — not a decision the base LLM makesexpert, target_event_id, rating (up / down), source (user / judge), rated_by_user_id or rated_by_service_account_id, plus reason, rationale, score, criteria when supplied
user.feedbackThe same thumbs, but on a regular llm.response. Consumed by training-data selection to filter rated trajectoriesSame payload, minus expert
TypeWhenPayload
user.define_outcomeA goal was set through the API — POST /v1/sessions/{id}/events. The /goal command does not emit thisdescription, rubric ({type, content}), max_iterations (clamped to 20), outcome_id, processed_at
outcome.definedThe goal was saved on the session by /goal. The API path emits user.define_outcome insteadoutcome_id, description, rubric, max_iterations
span.outcome_evaluation_start / span.outcome_evaluation_ongoingThe evaluator span opened around one turnoutcome_id, iteration; start also carries response_event_id
span.outcome_evaluation_endThe evaluator returned a verdictoutcome_id, outcome_evaluation_start_id, iteration, result (satisfied, needs_revision, blocked, failed, max_iterations_reached, or paused when the evaluator returned unparseable output too many times in a row), explanation, feedback, parse_failed
outcome.continuationA continuation was queued. The synthetic user.message that follows carries synthetic: "outcome_continuation"outcome_id, iteration, status_event_id
outcome.pausedThe goal was paused by /goal pause — the only thing that emits thisoutcome_id, reason (always user-paused)
outcome.clearedThe goal was removed by /goal clearoutcome_id (null when nothing was active)

The agent raised something to the user:

TypeWhat it surfaces
inbox.input_requiredAn ask_user_question prompt
inbox.action_requiredAn external action needed (browser login, MFA, etc.)
inbox.task_completeAn informational completion
inbox.governance_gateAn overridable policy denial awaiting a human decision. Payload: tool_name, tool_call_id, arguments_excerpt, deny_reason, policy_id. Answer with POST /v1/inbox/{item_id}/respond and {"decision": "approve"} or {"decision": "reject"}; approving mints an approval grant keyed to that exact call, which the retry consumes
inbox.progress_checkinA long-running-session status update
TypeWhen
browser.provisioned / browser.destroyedAn agent browser was allocated or torn down
browser.unavailableA browser tool ran on a local-folder session’s computer that has no supported browser
browser.control_granted / browser.control_returnedA user took over the live browser view, then handed control back
code.run_startedA /code run began. Carries the source user-event id for crash-recovery idempotency
code.run_progressCoalesced streamed output from the coding agent. Web UI only — never delivered to channels
code.run_channel_updateA throttled “still working” heartbeat, emitted only for channel sessions so a Slack or Telegram user isn’t left silent for the minutes a run takes
code.run_resultThe final message plus token usage

Credentials never appear in any code.* payload.

Emitted on sessions that work in a folder on a user’s computer, and on sessions with an open document editor.

TypeWhenPayload
device.waitingThe session’s work is waiting on its computer: it is offline, or it is making the thread’s first copy of the folderdevice_id, device_name, reason (offline or copying), plus folder for copying
device.resumedThe wait endeddevice_id
office.savedA save from the document editor that had been waiting for its computer landedpath
office.save_waitingA save waits for the computer its file is on. Said when the wait starts and again if its reason changespath, computer, reason (offline, or waiting for the computer’s user to allow it)
office.save_failedA save from the document editor failedpath, plus status or attempts; or reason: "declined" and computer
office.conflictThe file changed outside the editor, so the save landed as a sibling copy and the editors were told to reloadpath, conflict_path
TypeWhen
harness.wakeA worker acquired the session’s lease and began processing — the first event of every turn. Payload: worker_id, cursor. The background memory/skill review re-uses this type with review_memory / review_skills instead of cursor
session.pause / session.resume / session.complete / session.failSession-level transitions
session.stoppedA channel /stop aborted the running turn out-of-band. Unlike pause, the session stays active
session.model_escalatedThe model was swapped mid-turn to get past a provider behaviour the current model could not (today: repeated empty completions), so the tier change is visible in cost attribution. Payload: reason, model (the model escalated to)
session.title_updatedAuto-title landed on the session outside the chat-turn flow, so SSE subscribers can patch it in place
harness.crashThe runtime raised and the turn died
harness.recoveredThe orphan sweeper found a session abandoned by a dead worker (hard-killed — SIGKILL, OOM, pod eviction). No exception was raised; the event documents the gap and triggers the retry path on the next wake
context.compactThe session’s context window was compressed automatically
artifact.createdA create_artifact call produced an inline chat artifact. The event carries metadata only; fetch the body from the artifacts API
loop.resultA scheduled loop run’s final answer, delivered inline on the parent session — see Background jobs

These are declared in the EventType enum but have no producer in the runtime today (some are still carried in client-side type unions). Do not build on them.

session.start, session.reset, memory.update, artifact.updated, ambient.tick, ambient.post, sandbox.provision, sandbox.execute, sandbox.result, sandbox.destroy, research.dispatched, research.merged, research.pruned, research.report.

bash
# Stream live (SSE) — this is what /events is; it takes only `after`
GET /v1/sessions/{id}/events?after=<last_id>
# Read back as JSON (user JWT only — there is no /v1/api/ alias)
GET /v1/sessions/{id}/events/poll?after=<last_id>&limit=50

There is no server-side type filter — ?type= is silently ignored on both routes. Pull the page and filter client-side on event.type. Service-account tokens reach the SSE route under /v1/api/sessions/{id}/events but cannot reach /poll.

See the API reference for full schemas. For Slack and Telegram channel sessions, the same events stream is available — channel adapters just translate the user-visible portion into the channel’s native format.

Event type names and their documented payload keys are a stable contract. Adding new keys is safe; renaming or removing requires a deprecation cycle. If you’re querying programmatically, prefer reading specific keys (event.data.tool) over depending on the full payload shape.