Skip to main content
Events are the durable truth of a Session. Every persisted event has a globally unique ID and a monotonically increasing per-Session sequence number. Transcripts, traces, usage, and UI state are all projections of this log.

Event envelope

Use seq for ordering within one Session. Use id to deduplicate at-least-once delivery across consumers. schema_version versions the envelope shape itself, independent of the dated wire version. Most current events, including the agent.message above, use v45; native session.admitted and native managed run.created events use v46. Version 46 is the highest supported envelope version, and Checkfu reads stored v1–v46 histories.

Provenance

provenance names who witnessed the fact. Every envelope returned by the list endpoint and the SSE stream carries exactly one of five values: The exec lane is realized by one event type today, exec.tool_invocation, in the event catalog below. Provenance is a read-side projection derived from the event type. It is never persisted, and it can never change sequence ordering, idempotency, settlement, retention, or replay. The stored log remains the truth. An event type the current classifier does not recognize arrives as narration, so new additive types stay readable by consumers that predate them. narration also remains the default display projection: a Harness’s account of a turn is what you render, not the raw wire evidence beneath it. Connected-runtime narration may additionally carry attested: true. That literal means the Session verified the batch’s detached Ed25519 signature against the runtime’s registered current key before appending it. It strengthens the harness’s self-report; it does not change its narration provenance into platform-witnessed wire or exec evidence. Unsigned connected batches remain valid and omit the field.

Presentation

presentation is a bounded product view derived from the canonical event and its provenance. It is never stored and never replaces type, payload, or provenance: Every closed current event has an explicit decision. Unknown future types still arrive with their canonical envelope and a conservative diagnostic presentation, so a product feed never drops evidence it does not yet understand. The SDK exposes the canonical iterator as sessions.events.stream(...). When you want the ergonomic view, streamPresented(...) yields both values together:

Client drive events

Post one of these bodies to /v1/sessions/{id}/events:
  • user.message
  • user.interrupt
  • user.custom_tool_result
  • user.tool_confirmation
  • user.question_answer
  • user.define_outcome
Every human-authored drive event requires attribution: authored_by (the PrincipalId on whose behalf the event is authored) and caused_by (the cause; { "kind": "api" } for a direct API call). An optional surface_scope_id may also be included.
Managed Harnesses run asynchronously through a leased Runner and sandbox. (The quickstart’s alpha echo harness happens to settle before the create call returns. Never depend on that; react to persisted events, not response timing.) user.interrupt succeeds while managed work is queued, actively running, or waiting on an outcome evaluation, Question, ActionApproval, or custom tool. Multiagent messages are asynchronous and never create a primary-thread wait. Interrupting an ActionApproval or custom-tool wait records the exact interrupted error result and returns the turn to idle without another model sample. A user.message posted while the Session’s first Run is still queued or provisioning is accepted rather than rejected: it appends to the log immediately and is ordered for the next Run, so an application never has to wait for running before it can send input. user.custom_tool_result and user.tool_confirmation succeed only for the exact current tool_use_id; when session_thread_id is present they route through the primary Session to that child wait. user.question_answer succeeds only while the primary is waiting on the named Question. Sending a command in the wrong state, answering the wrong tool call or Question, or naming a wrong or stale child origin returns a conflict without advancing either thread.

User message

If Checkfu finds deceptive Unicode, the first request returns 400 validation.prompt_integrity_review_required without appending an event. Its typed error.review contains the pinned analyzer version, normalized text, and exact findings, so API and SDK callers never need to reimplement Unicode analysis. Review that challenge, then resubmit the original content with one explicit choice:
Use accepted_normalization to let the server replace mixed-script confusables and remove bidirectional controls, or preserved_original to send the reviewed source exactly. Do not send the already-corrected string with a disposition: the server rejects a choice when its own analysis finds nothing, preventing consent from being replayed against changed text. The returned user.message contains the effective content and a server-authored prompt_integrity receipt. Requests with no findings omit both fields. Two more optional fields select delivery. run_delivery: "next_run" forces a message authored while a Run is running to begin the next Run instead of entering the current turn; omitting it takes native mid-Run delivery whenever that Run’s recorded capability set realizes mid_run_input: native, and the next Run otherwise. Either way the harness’s answer is durable: run.input_delivered records the acknowledgement, and the log — never response timing — decides which turn carried the message. connected_runtime belongs to connected Run admission; this route rejects a user.message carrying it with 400 validation.malformed.

Interrupt the current turn

Two optional fields narrow it. target_event names an API-authored user.message and fences cancellation to exactly that message: a still-queued next-Run control is retired without touching the current Run, and an already-promoted one interrupts only its own Run. An ID that names anything else is 400 validation.malformed. mode: "cooperative" asks the running turn to drain instead of force-canceling it — finish the current tool, emit final consistent state, end cleanly — and is honored only when the Run’s recorded capability set realizes interrupt: cooperative. An absent mode, a kill-only harness, a queued Run, and a targeted interrupt all keep the immediate user.interruptrun.canceledsession.status_idle settlement.

Return a custom tool result

For a child wait, copy session_thread_id from the primary stream’s agent.tool_use cross-post into the payload. Omit it only for a primary-thread call. Exact replay returns the same event; a conflicting result, wrong thread, or settled generation is refused.

Confirm a tool action

result is allow or deny; only deny may include a non-empty deny_message. This CMA-shaped event retains Checkfu’s ActionApproval policy and responder-authority checks. Allowing a governed Custom tool emits run.action_authorized on the child but keeps it parked until the matching user.custom_tool_result arrives.

Answer an agent question

A Question is ordinary input, not an ActionApproval: it grants no authority. question names the active Question, and answers must cover every item the matching agent.question requested, once each, honoring each item’s allow_multiple. Partial, duplicate, and unknown item IDs are rejected. Textual answers cross the same deceptive-Unicode boundary as user.message: a flagged value returns 400 validation.prompt_integrity_review_required, and the resubmission carries a payload-level prompt_integrity_disposition of accepted_normalization or preserved_original.

Define an outcome

user.define_outcome starts an outcome-driven Run from a pending or idle Session. Only one outcome may remain nonterminal at a time. max_iterations is optional and must be between 1 and 20. deliverables is optional and accepts 1–32 absolute paths; a zero-data-retention Workspace rejects outcome definitions that request deliverables.

Answering a parked run

A Run does not fail when it needs something from you. It parks. This is the loop most integrations have to implement, and it is driven entirely by events. When a Run needs an answer it emits run.requires_action and the Session moves to session.status_waiting. The payload carries run, attempt, cost, and an action that discriminates on kind:
A question wait is the one kind that also ends compute. The Runner releases custody at the yield and the Session moves through waiting to paused, so a Session can sit on an unanswered Question indefinitely without holding a sandbox. The parked Run stays in requires_action until an answer arrives; accepting one completes it and admits a fresh continuation Run that re-resolves mounts and receives the exact question and answers. Interrupting instead cancels the parked Run and returns the Session to idle with no continuation, so an unanswered Question cannot trap the Session. For an ActionApproval wait, action_approval.pending follows with the frozen decision context. A ToolInvocation ActionApproval carries the Connection, Tool identity, argument hash, and acting Principal:
The ActionApproval resource exposes these same frozen facts under different names: context_summary and arguments_hash (see ActionApprovals). Event consumers match on frozen_context/arg_hash; REST readers match on the resource names.
A Custom-tool ActionApproval instead carries kind: "custom_tool" plus the exact Session, Run, tool-use ID, admitted AgentDefinition, logical harness tool, argument hash, and acting Principal. Its exact argument values remain in the preceding agent.tool_use event:
Once a reviewer decides, action_approval.resolved records the decision (approve or deny), the tool_use_id it answers, who resolved it, and any reviewer instructions. Gateway decisions and Custom-tool denials emit run.resumed and create the normal continuation. A Custom-tool approval instead emits run.action_authorized; the same Run remains parked until the application submits its matching user.custom_tool_result.
Key the wait loop off run.requires_action, not off session.status_waiting. The status event tells you the Session is parked; only the action payload tells you what it is parked on and which tool_use_id to answer.
See ActionApprovals for the review side of this flow.

Event catalog

Every event type the current contract can append. Consumers must tolerate unknown types: the catalog grows with the platform, and a new event type is not a breaking change.

user.*

  • user.message: a human input; starts a turn (or steers a running one)
  • user.interrupt: ends work without making the Session terminal; optional session_thread_id targets one thread, while omission interrupts every non-archived thread
  • user.custom_tool_result: your app returning the result of a custom tool the Run parked on
  • user.tool_confirmation: allows or denies the exact current tool wait; optional session_thread_id routes a child confirmation through the primary Session
  • user.question_answer: a human answering one active agent.question, which admits a continuation Run
  • user.define_outcome: attaches a rubric-graded definition of done to the next turn

system.*

  • system.message: mid-conversation system-privileged context appended beside the Agent’s frozen system prompt, which it never replaces. Unlike a title or the metadata bag, the content is the point — an agent that cannot read it is not steered by it — so the text lives in the event and is retained content like a user message (up to 8,192 characters). No instruction changes and no release hash moves. It is admitted only from a control-plane role, never a runtime credential, and only where the admitted model route can carry a mid-conversation system turn

agent.*

  • agent.message: the agent’s reply text (payload.content is what you show users)
  • agent.thinking: reasoning content, when the harness surfaces it
  • agent.tool_use: the agent invoked a tool
  • agent.tool_result: what the tool returned
  • agent.question: a structured request for ordinary human input: one question ID and one to four uniquely identified items, each with bounded options and single- or multi-value semantics. Prose that reads like a question stays an ordinary agent.message
  • agent.subagent_started / agent.subagent_completed: a harness-internal subagent, observed but not governed
  • agent.thread_message_sent: one durable direction-relative send; carries bounded content, to_session_thread_id, and optional to_agent_name
  • agent.thread_message_received: the matching receiver-side fact; carries bounded content, from_session_thread_id, and optional from_agent_name

message.*

  • message.screened: a delivery screen passed or withheld an outbound agent message; screens never rewrite content

flow_review.*

  • flow_review.requested: one passive post-settlement review was frozen for an Automation-origin Turn
  • flow_review.started: the bounded evidence and one-attempt review lease were acquired
  • flow_review.completed: a cited typed report settled; it never changes the Outcome, delivery, or Session state
  • flow_review.unavailable: retained evidence or reviewer authority could not produce a report after the bounded policy

session.*

  • session.status_pendingsession.status_canceled: the ten status transitions, including compute-closed session.status_reconciling; see the state machine
  • session.admitted: admission settled, carrying the frozen release, requester, and (if installed) placement
  • session.forked: appended after the inherited source prefix as the forked Session’s first new event. Private alpha for committed-head, exact-at_event, and live-Checkpoint forks. Its payload names source_session, through_event, through_sequence, the nullable target checkpoint, and mode: "event_log_rebuild" | "checkpoint_clone". A Checkpoint fork preserves the inherited log prefix and independently clones the provider snapshot; it never copies a live Run, lease, wait, or ActionApproval.
  • session.updated: mount declarations changed at a turn boundary
  • session.title_updated: content-free { present } marker for a material title projection change
  • session.metadata_updated: the correlation bag settles like a title — a content-free { pairs } count of how many pairs the Session now carries. Keys and values are your text and never enter the log
  • session.mounts_resolved: the exact revisions and trees this Run received
  • session.compacted: context was compacted; the log itself is never rewritten
  • session.context_condensed: a context-window boundary was recorded; later conversation rebuilds serve an elision marker plus the tail after it, and each boundary also projects into the optional condensations array of GET /v1/sessions/{id}/ledger
  • session.thread_created: a governed child thread received its stable sthr_* identity
  • session.thread_status_running: a child thread began or resumed execution
  • session.thread_status_idle: a child thread yielded and remains addressable; its stop_reason is end_turn, requires_action, or retries_exhausted
  • session.thread_status_rescheduled: a bounded retry is pending for the same thread
  • session.thread_status_terminated: a child thread was archived or failed terminally
  • session.delivery_ambiguous: reserved by the built next-version Session delivery protocol for an outbound WebhookEndpoint POST whose acknowledgement may have been lost. The currently selected Session architecture does not emit it; the event names only structural delivery, endpoint, and event coordinates and is excluded from the webhook subscription catalog to prevent recursive delivery
  • session.archived: content-free marker that the Session left the working directory; its timestamp is the resource’s archived_at (see Archive a session)

watch.*

A Watch is a Session-owned subscription to external provider state.
  • watch.created / watch.updated / watch.removed: the Watch’s lifecycle, each naming the watch
  • watch.woken: a verified change in the watched state admitted an ordinary next Run; carries the source_state_hash that settled the wake
  • watch.suppressed: a hint that woke nothing, with reason: duplicate | self_authored | stale_hint | unsafe_content

memory.*

  • memory.mount_resolved: which store, revision, and access level a Run mounted
  • memory.accessed: emitted once per store that sealed a writeback at settlement (its content-addressed base was read); pure-read-mount coverage is a pending revisit
  • memory.change_checkpointed: a settlement seal naming the changed paths written into the store’s durable document surface, with content-addressed base/new tree digests (the versioned revision graph stays alpha behind MEMORY_VERSIONED_SURFACE, so these are digests, not graph revision IDs)
  • memory.proposal_created / memory.proposal_resolved: the shared-store review flow
  • memory.curated: a Dream landed curated content
  • memory.redacted: an erasure, recorded as history rather than hidden

project.*

  • project.writeback_captured: the Session accepted one exact sealed changed manifest and entered compute-closed reconciliation
  • project.writeback_started: the compute-free reconciler acquired the durable publication generation
  • project.writeback_no_changes: a complete tree enumeration proved the mounted Project was unchanged
  • project.writeback_succeeded: the owned branch, pull request, exact frozen base_revision, published immutable revision, capture retirement, and—when D127-eligible—reused Watch settled
  • project.writeback_conflicted: the target, owned branch, or pull-request identity diverged; Checkfu did not force-push or retry over human work
  • project.writeback_failed: strict capture, authority, provider, or publication failed rather than silently dropping edits

model.*

  • model.routed: which provider/model candidate answered this call
  • model.usage_recorded: the authoritative token receipt for one model call

wire.*

  • wire.call: one gateway-witnessed model request and response, retained as evidence
The event row carries no model bytes. It names the Run, attempt, request ID, binding, provider, model, and usage, plus typed references to the exact material: the admitted request body, each ordered request message, the response body, and the provider frames. Every reference is { source, content_digest, bytes }, where content_digest is a sha256:<hex> digest, so repeated history resolves to objects that already exist and only an operator can resolve one back to bytes. wire.call is evidence, not display: it sits in the ordinary Session sequence and appears in exports at its original position, but the Harness’s narration remains the conversational projection you render. A zero-data-retention Workspace constructs no wire evidence at all. The missing wire.call events are the visible capability difference. Retained wire bytes also carry a mandatory 30-day absolute retention ceiling, even where the Workspace’s standard policy has no TTL at all; a shorter Workspace or wire_frame TTL still wins. After expiry the references remain structurally in the event, and resolving one returns 404.

exec.*

  • exec.tool_invocation: one governed ToolInvocation, settled and witnessed by the platform itself
The payload is structural only: Run and attempt identities, tool_invocation_id, the associated resource identities and logical tool name, execution_backend (builtin, executor, or connector), disposition, an argument_fingerprint that hashes the canonical logical arguments rather than carrying them, and claim-to-settlement duration. connector means the pinned tool ran through the provider-neutral Integration Gateway; it does not identify the selected runtime adapter. No argument or result body ever appears, so unlike wire.call this event persists unchanged under zero data retention. disposition is completed, denied, or indeterminate. At most two exec.tool_invocation events exist for one tool_invocation_id: the settlement, plus at most one later corrective event carrying upgrade: true, which marks an already-settled outcome indeterminate.

run.*

  • run.created: admission accepted work; carries the trigger
  • run.started: an executor actually began
  • run.start_timed_out: a claimed attempt missed its ready deadline and was requeued before it could start
  • run.attempt_requeued: a started attempt released its native Checkpoint and was replaced, without changing the customer-visible logical Run. recovery is one-based and the current aggregate admits exactly one, so this appears at most once per Run
  • run.requires_action: parked on an approval, custom tool, Question, or outcome evaluation (see action.kind)
  • run.action_authorized: a Custom-tool ActionApproval authorized customer execution; the same Run now awaits its exact application result
  • run.resumed: the awaited action arrived; a continuation is executing
  • run.input_delivered: the harness answered a steering user.message delivered into the running turn, naming that event and a disposition of delivered, queued, or refused. This record, not the transport acknowledgement, is the delivery authority: delivered and queued fence the message to this Run, and refused leaves it to be promoted into the next one
  • run.lifecycle_hook: one reviewed sandbox lifecycle-hook command settled — phase, ordinal, exit class, duration, the declared on_failure, and a bounded tail of its output; the event, not the Runner’s memory, is the settlement authority, so a failed verification hook is recorded rather than passed silently
  • run.completed / run.failed / run.canceled: terminal settlement; the usage receipt keys off this event

action_approval.*

  • action_approval.pending: a frozen call awaits a human decision
  • action_approval.resolved: the decision, responder, and any instructions

outcome.*

  • outcome.evaluation_started: the grader began judging a settled revision
  • outcome.evaluation_completed: the verdict, one of five results, with rationale and cost

span.*

Telemetry adapters mint their own span names, so span.<name> is an open family rather than a fixed list. Spans are best-effort telemetry and never carry enforcement meaning; a consumer that does not recognize a span type should ignore it.

List persisted events

limit defaults to 100. Values greater than 1000 are capped at 1000; zero, negative, fractional, or nonnumeric values return 400 validation.malformed. The response contains readable envelopes in data, content positions that cannot be produced in unavailable, and next_page. An unavailable entry names the original seq, content class, and shredded or zdr reason. A reviewed ZDR prompt may additionally expose only its analyzer version and author disposition, never the prompt, source digest, or findings. This endpoint budgets a page by owned-log positions, not only readable events. A page can therefore have an empty data array and non-empty unavailable array. Its cursor is the last processed per-Session sequence number; continue until next_page is null.

Replay and follow over SSE

On connection, the current alpha replays every persisted event after the cursor in internal pages and then stays live. Send the last processed sequence in Last-Event-ID, or pass the same cursor as page:
CHECKFU_API_KEY and CHECKFU_WORKSPACE_ID come from Get access; CHECKFU_SESSION_ID is the Session you are tailing. Reconnect after the connection closes or a network failure and provide the last fully processed sequence. Pull-driven backpressure keeps at most one internal page in flight for a stalled consumer; reconnecting from Last-Event-ID resumes from the durable log. The SDK rejects replayed or reordered durable sequences instead of yielding duplicates. If retention or ZDR leaves unreadable positions before the next readable event, the server emits a cursor-neutral checkfu.cursor_gap control frame. The SDK stops with EventStreamCursorGapError, whose from, through, and next fields identify the omitted range. Record or resolve that omission through the paged event ledger’s unavailable entries before explicitly starting a new iterator from through; the SDK never skips evidence loss automatically.

Opt in to live previews

Repeat event_deltas to request best-effort previews for the currently running turn:
The stream first replays durable events through the requested cursor, then may emit these additional frames while it follows live work:
agent.message emits event_start followed by content deltas. agent.thinking emits event_start only; its content remains available in the final durable event when the harness provides it. If the corresponding observation commits, the agent.message or agent.thinking envelope uses the same event ID, so a client can replace its preview with the committed result. Preview frames carry no SSE id: line, no seq, and no provenance. They never advance Last-Event-ID, never appear in event lists or replay, and may be dropped for a disconnected or stalled consumer. A failed, interrupted, or lease-lost turn can leave a preview without a final event; discard unmatched previews when the turn settles. After a transparent SDK reconnect, the iterator yields { type: "preview_reset", reason: "reconnect" }; clear every unmatched preview before applying subsequent frames. A non-SDK client clears them at its observable HTTP reconnect. Always render the final durable event as authoritative. A DeliveryScreen agent does not expose agent.message previews because content has not yet passed its release-pinned screen.

Resolve retained wire content

Returns the exact retained bytes as application/octet-stream. This route is the only way wire material crosses HTTP, and it is deliberately narrow: the key must hold an admin or root role (anything else is 403 policy.denied), and the digest must be one the named wire.call event actually references. A digest that the event does not name, or whose bytes have passed their retention ceiling, is 404. There is no browsable index of retained content, and internal storage identifiers never appear on the wire.

Next steps

Resume a stream reliably

Handle disconnects, duplicates, and backpressure without losing or double-processing events.

Project a session

Fold the stream into named views: messages, tool calls, ActionApprovals, the subagent tree, and the context ledger.