Status: Stable.
Normative home:
heartbeat,hostEvents,feedback,providerUsage,limits.
Why this exists
A run is its append-only event log. Every snapshot, stream, poll, fork and diff is a projection of it.
The envelope
schemas/v2/run-event.schema.json (RunEventDoc) is closed. eventId, runId, type, payload, timestamp, sequence and schemaVersion are REQUIRED; nodeId, engineVersion and causationId are OPTIONAL. Every id field $refs its grammar in schemas/v2/ids.schema.json (identity.md).
| Field | Meaning |
|---|---|
sequence | The one ordering field: integer ≥ 0, first event 0, strictly increasing per run |
schemaVersion | Per-event schema version, integer ≥ 1, first-class |
engineVersion | Integer ≥ 0 everywhere |
eventId | Host-minted, opaque |
causationId | The eventId that caused this event |
timestamp | ISO 8601 |
- Persisted logs are never renumbered.
- A consumer MUST treat
eventIdas a string. - A consumer MUST NOT throw on an event whose
typeit does not know; it folds what it understands and ignores the rest.
Types
type is oneOf a closed enum of registered protocol types and a vendor pattern. The enum is generated from spec/v2/event-codemap.json and MUST NOT be edited by hand. The vendor branch is exactly:
^(?!openwop\.)[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)?$
- Naming. A protocol type is
domain.verb-ed: kebab-case, exactly two segments, past tense for a transition (run.started,node.suspend-failed).domain.nounis permitted only for an emitted artifact (output.chunk,channel.presence); each exception is recorded in the codemap. - Reserved prefix.
openwop.is the only reserved prefix.core.,community.,vendor.,private.andlocal.are pack namespaces, not event namespaces; a type under them is invalid. - Vendor events. A vendor type's first segment MUST be an org registered in the
extensionsobject ofspec/v2/declaration.json(the org registry, not discovery'sextensionskey). An unregistered org fails validation. An org inreservedOrgsis forbidden and never registered.extensionsKeyPatternis a vendor type's shape, not a permission to use it.exampleis reserved for documentation and conformance, never a vendor. - Growth. The registry grows by the closed-enum rule in overview.md §0. A producer MUST NOT emit an unregistered protocol type. A consumer MUST accept an unknown registered member and MUST NOT act on it.
Payloads
schemas/v2/run-event-payloads.schema.json holds one $defs entry per payload, each additionalProperties: false, plus _typeIndex: the normative map from v2 type to $defs key, generated from spec/v2/event-codemap.json.
- A host MUST emit a payload that validates against the entry
_typeIndexnames for itstype. - Sub-typing is
$refcomposition, never duplication.approval.andclarification.resolve tointerruptRequested/interruptResolved(interrupt.md);lease.acquired,lease.renewedandlease.lostshareleaseLifecycle. - The CloudEvents mapping and the webhook delivery envelope are generated from the same definition. An event's
type,eventId,sequenceandpayloadare byte-identical across the run stream, a CloudEvents rendering and a webhook delivery.
Specific payloads:
run.startedcarries theownerblock (identity.md §1.1).run.cancelledcarriesreason,cancelledBy,durationMs, andparentRunId.run.completedMUST carryoutputsas an object. An empty object is valid; an absent key is not. This separates "no outputs" from "outputs not rendered".
The events channel
api/v2/asyncapi.yaml declares one channel, runEvents, at /runs/{runId}/events, and api/v2/openapi.yaml the same path (streamRunEvents); both MUST resolve to one absolute path.
Stream modes
The streamMode query parameter is one pattern:
^(values|(updates|messages|debug)(,(updates|messages|debug))*)$
| Mode | Emits | Combines |
|---|---|---|
updates (default) | Deltas for run transitions, terminal node transitions, suspensions, node.dispatched, interrupt events, artifact.created, eval., deployment., workspace.updated | yes |
values | One synthesized state.snapshot (schemas/v2/run-snapshot.schema.json) after each updates-tier transition | never |
messages | ai.message.chunk (outputChunk payload) from streaming AI nodes only | yes |
debug | Every event in the log, including log.appended, variable.changed, version.pinned, lease.*, node.retried and every vendor event | yes |
- A host MUST implement
updatesand SHOULD implement all four. - A value outside the pattern, or a mode the host does not implement, MUST return
400 unsupported_stream_modewithdetails.supportedlisting each individual mode the host serves; combinations are not listed. - Validation MUST run before any content negotiation.
- In
messages, a host MUST populate a Tier 1metaslot whenever it has the data. - Vendor events appear in
debugonly. - In a mixed mode the host emits the union of the filters in log order and MUST NOT reorder. Each frame SHOULD carry
event:naming the mode that admitted it. A consumer MUST tolerate an event admitted by more than one mode.
SSE frames
Each frame carries id: (the sequence), event: (the v2 type, in a single mode) and data: (the RunEventDoc). Three frame names are not types and are absent from the type enum:
state.snapshot—valuesmode;data:is aRunSnapshot.batch—bufferMs;data:is an array ofRunEventDoc.ai.message.chunk—messagesmode;data:is theoutputChunkpayload, persisted as typeoutput.chunk.
A host MUST set Content-Type: text/event-stream, MUST emit a keep-alive comment at least every 30 seconds, and MUST close the connection after the run's terminal event.
Resuming with Last-Event-ID
Last-Event-ID: N resumes every mode as an exclusive cursor, with the semantics of poll's afterSequence.
- The host MUST stream the events with
sequence > Nin log order and MUST NOT re-emitN. - When
Nis at or beyond the last persisted sequence there is no backlog. On a live run the host MUST hold the stream open for later events; on a terminal run it MUST close the stream without a frame. - A host MUST NOT refuse a well-formed non-negative integer
Last-Event-IDbecause no event carries that sequence. Any other value SHOULD be refused with400 validation_error. - The header is evaluated only after the caller is authorized to read the run. For a run the caller cannot read, the response MUST be the one the host gives without the header.
- In
valuesmode, resumption MUST emit astate.snapshotfirst.
Bounded close
With resumable=true, a host advertising limits.maxStreamAgeSec MAY end a live stream early, per that facet. That close is not terminal; the consumer MUST resume with Last-Event-ID.
Batching and subscribers
With bufferMs (0..5000) the host accumulates events into one event: batch frame whose data: is an array of RunEventDoc.
- It MUST flush on a terminal event, on
node.suspended, and on close. - The batch's
id:SHOULD be its highestsequence, andLast-Event-IDMUST honor that id. - A consumer MUST tolerate both a one-element batch and an unbatched frame.
- A host MUST NOT limit subscribers per run except for resource protection, and then MUST answer
429 rate_limitedwithRetry-Afterrather than drop silently.
Host events
hostEvents carries the heartbeat messages (schemas/v2/heartbeat-evaluated.schema.json, schemas/v2/heartbeat-state-changed.schema.json) and host events at the default address /host/events (streamHostEvents). A host MAY declare another address under heartbeat.deliveryChannel (capabilities.md); every channel has an address. The channel carries no run data. Every host event, and every heartbeat a tenant owns, MUST reach only that tenant's subscribers.
hostEvents
A host event's envelope (schemas/v2/host-event.schema.json) has no runId or sequence. hostEvents.types[] lists each emitted type with its delivery class. A host advertising it:
- MUST deliver each listed type here,
event:being the type anddata:the envelope, and MUST emit no other type or class; - MUST confine an event naming a
workspaceIdto that workspace, and an ephemeral event to subscribers it is visible to; - MUST emit each listed
host-test.*type, only throughemitTestHostEvent, to the caller's tenant.
negotiation.decided is reserved and durable (interop.md).
An ephemeral event MUST NOT be persisted, redelivered on reconnection, or sent through webhooks, A2A push or any sink; its frame has no id:. A durable event's id: is its eventId; a host SHOULD honour Last-Event-ID for it within retention, and MUST under hostEvents.replay. A vendor payload MUST NOT carry secrets.
heartbeat
A heartbeat evaluates a predicate on an interval (at least minIntervalSec) and acts only on a state change. A host advertising heartbeat SHOULD also advertise scheduling, and otherwise MUST document its own interval substrate. On each tick it MUST:
- skip, not queue, a tick while the prior evaluation still runs;
- bound the evaluation by
maxRuntimeMs, itself capped bylimits.maxRunDurationMs, terminating an overrun withstatus: timeout; - pass the predicate the prior tick's state, and perform no side effect itself. The predicate MUST be a pure function of observed and prior state;
- emit
heartbeat.evaluated; - on a transition only, emit
heartbeat.stateChangedand, if the predicate asks, callcreateRun; never on an unchanged tick.
The first tick of a heartbeatId MUST be treated as a transition from {}. A durable host SHOULD persist prior state so a restart does not re-notify.
Poll
GET /runs/{runId}/events/poll (pollRunEvents) is the long-poll fallback.
| Parameter | Meaning |
|---|---|
afterSequence | Integer ≥ 0; return events with sequence > afterSequence. Omitted means from sequence 0 |
timeout | Seconds to wait for new events, 1..60, default 30 |
lastSequence and since are not parameters.
The response is { runId, events, lastSequence, status, isTerminal } (closed):
lastSequenceis the highest sequence in the log at the time of the response,-1when the log is empty.statusis the snapshot status;isTerminalis whether the run is terminal.- A cursor past the end of the log MUST return
200with an emptyeventsarray.
The terminal event
A run's log MUST contain exactly one terminal run event: run.completed, run.failed or run.cancelled.
- After it, the log MUST NOT contain another terminal run event,
run.started,run.resumed,run.resume-started,run.paused,run.restored-from-snapshot, anynode.event or anyinterrupt.event. compensation.*events andrun.dead-letteredMAY follow it. Vendor-prefixed types are unconstrained.- A host that receives work for a run whose terminal event is recorded (a duplicate delivery, a late worker) MUST NOT append forward-execution events for it, SHOULD record the refusal in its operational log, and MUST NOT surface the refusal as a run event.
Era-2 logs
An eventLogSchemaVersion of 2 means v1-written. Every reader, diff included, translates it per persistence.md §"The reader rule".
- A projection MUST NOT silently drop a property: it carries it or fails with
500 payload_unprojectable(hatch^(openwop-|x-|vendor\.)). - Fork and replay over an era-2 parent are in replay.md.
Feedback and usage facets
feedback
feedback.signals lists the signal kinds a host accepts; absent means all four. feedback.targets names the resources an annotation may be attached to. A host advertising feedback MUST:
- accept an annotation on a terminal run;
- refuse an annotation whose target is outside the advertised
targets, and MUST NOT write it to the replayable run event log; - keep annotations visible only within the run's tenant (invariant
annotation-cross-tenant-isolation); - redact secret-shaped material in
signal.correctionandnotebefore persistence, listing and export (invariantannotation-content-redaction); - audit-log each recording with the acting principal.
providerUsage
A host advertising providerUsage MUST emit exactly one provider.usage per LLM provider invocation, before that node's node.completed. A host that does not advertise it omits the event.
inputTokensandoutputTokensMUST replay identically;costEstimateUsdMAY be omitted on replay.providerUsage.costEstimatesadvertises that the host stamps a derived cost on the event. It is an estimate from the host's rate table; a consumer MUST NOT treat it as a billed amount.- The payload MUST NOT carry credential refs, hashed credential identifiers, or prompt or response text (invariant
provider-usage-no-credential-leak). providerUsage.currencyis the ISO 4217 currency ofcostEstimateUsd; absent means USD.
Sources: RFCs 0026, 0030, 0032, 0033, 0056, 0060, 0171, 0172, 0176, 0185, 0194, 0213, 0236, 0247.