Status: Stable.
Normative home:
runList,limits,conversationPrimitive,dataResidency,deadLetter,budget.
Why this exists
Creating, reading, streaming, cancelling, pausing, forking and diffing a run. Every host projects the same snapshot from the same event log (events.md).
Identity
Every id $refs schemas/v2/ids.schema.json (identity.md). A runId is host-minted and tenant-bound: <tenantId>/<opaque>.
- A caller MUST treat every id as opaque.
- A host MUST reject a
runIdwhose tenant segment is not the caller's with403 id_tenant_mismatch, and MUST NOT disclose whether the run exists.
Surface
Every operation accepts OpenWOP-Version (overview.md) and every response carries it. Every mutating operation accepts Idempotency-Key (idempotency.md). Scopes are the vocabulary listed in api/v2/openapi.yaml's security schemes, matched as identity.md §2.1 describes.
| Operation | Method and path | Scope | Gate |
|---|---|---|---|
createRun | POST /runs | runs:create | — |
getRun | GET /runs/{runId} | runs:read | — |
listRuns | GET /runs | runs:read | runList |
streamRunEvents | GET /runs/{runId}/events | runs:read | events.md |
pollRunEvents | GET /runs/{runId}/events/poll | runs:read | events.md |
cancelRun | POST /runs/{runId}/cancel | runs:cancel | — |
bulkCancelRuns | POST /runs:bulk-cancel | runs:cancel | — |
pauseRun | POST /runs/{runId}:pause | runs:cancel | — |
resumeRun | POST /runs/{runId}:resume | runs:cancel | — |
forkRun | POST /runs/{runId}:fork | runs:create + runs:read | replay |
diffRun | GET /runs/{runId}:diff?against= | runs:read on both | OPTIONAL |
getRunAncestry | GET /runs/{runId}/ancestry | runs:read | multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported |
createAnnotation / listAnnotations | POST / GET /runs/{runId}/annotations | runs:annotate / runs:read | feedback |
getArtifact | GET /runs/{runId}/artifacts/{artifactId} | artifacts:read | — |
getEvalSummary | GET /runs/{runId}/eval-summary | runs:read | agents.evalSuite |
getRunCompensation | GET /runs/{runId}/compensation | runs:read | compensation |
getRunEffects | GET /runs/{runId}/effects | runs:read | idempotency |
A gated operation the host does not advertise (or an absent diffRun) answers 404 not_found (errors.md).
Create
The createRun body is closed (unevaluatedProperties: false). Its fields:
workflowId— REQUIRED unlessmode: eval.inputs,residency,tenantId,scopeId.callbackUrl— see interrupt.md §Callback delivery. A refused value is400 validation_errorwithdetails.field: "callbackUrl".mode,evalSuiteRef,agentId.runSecrets— run-supplied secrets, accepted only by a host advertisingsecrets.runSecrets(host-services.md §Run-supplied secrets). It is never part ofRunOptions.- The
RunOptionsfieldsconfigurable,tags,metadata. A body withoutRunOptionsMUST be accepted as if it were{}.
Request headers
| Header | Rule |
|---|---|
Idempotency-Key | RECOMMENDED. A replayed create MUST NOT create a second run, and carries OpenWOP-Idempotent-Replay: true. |
OpenWOP-Dedup: enforce | The host MUST reject a duplicate (tenantId, scopeId) with 409 run_already_active and Retry-After. |
OpenWOP-Force-Engine-Version | Test keys only (the seams profile). A host MUST reject it on a production credential with 403. |
Response
The 201 response is { runId, status, eventsUrl, statusUrl? }. status is one of pending, running, waiting-approval, waiting-input, waiting-external.
eventsUrlandstatusUrlMUST resolve under the origin the request was made to — a relative path, or an absolute URL on the same origin — and MUST NOT downgrade the scheme. A link naming a different host, orhttp://on anhttps://origin, is non-conformant.- The base that minted
runIdMUST resolve it:GET /runs/{runId}andGET /runs/{runId}/events/pollat that base MUST answer200for the returned id, percent-encoded per identity.md §5.
Refusals
mode: evalmakesevalSuiteRefandagentIdREQUIRED. It starts an eval-suite projection that emits the content-freeeval.*family and terminates with anEvalSummary. A host that does not advertiseagents.evalSuiteMUST reject it422 capability_not_provided.- A host advertising
dataResidencyMUST reject aresidency.regionoutsidedataResidency.regionswith422 residency_unavailableand create no run (see §"Conversation and residency capabilities"). - A workflow that references a capability-gated reserved node type on a host that does not advertise the capability MUST be rejected with
422 capability_required. - A host that lists
budget.onExhaustionserves exactly those values. It MUST reject any otherbudget.onExhaustionwith422 capability_not_providedand create no run. It MUST NOT apply a different behaviour or ignore the field.
The start event
run.started (events.md) MUST echo the run's owner block exactly as RunSnapshot.owner carries it. Its transport records rest, mcp, a2a or ui.
Run options
schemas/v2/run-options.schema.json is { configurable?, tags?, metadata? }.
configurable is schemas/v2/configurable.schema.json: closed, nested and versioned. The request body $refs it directly; there is no allOf-merge of an open map. version is REQUIRED and is 1. It has five sections:
| Section | Keys |
|---|---|
run | recursionLimit, runTimeoutMs, maxLoopIterations, escalationThreshold |
ai | provider, model, temperature (0..2), maxTokens, credentialRef, promptOverrides, mockProvider, reasoningVerbosity (none | summary | full), maxRefusals |
distillation | tokenBudget |
budget | schemas/v2/budget-policy.schema.json — the run's budget policy |
extensions | <org>: {…} — a vendor key lives under its registered org and nowhere else |
run section
recursionLimitis clamped tolimits.maxNodeExecutions. A breach, counted in node starts, MUST emitcap.breached { kind: 'node-executions' }and fail the run withrecursion_limit_exceeded.runTimeoutMsresolves tomin(runTimeoutMs, limits.maxRunDurationMs), measured fromrun.started. A breach MUST emitcap.breached { kind: 'run-duration' }and terminate the runfailedwithrun_timeout.maxLoopIterationsresolves againstlimits.maxLoopIterations, counted in orchestrator turns. A breach MUST emitcap.breached { kind: 'loop-iterations' }and fail withloop_limit_exceeded.- An out-of-range
recursionLimitorrunTimeoutMsMUST return400 validation_errorat create. After a breach the host schedules nothing more. escalationThresholdis thelow-confidencethreshold (interrupt.md).
Limits
limits always carries clarificationRounds (per task).
- A host advertising
maxNodeExecutions,maxRunDurationMs,maxLoopIterations,maxToolRoundsormaxRepairRoundsMUST enforce it. - On any breach the host MUST emit
cap.breachedand fail the node or run. The event carriesnodeIdfor aclarificationorschemabreach. Itsobservedexceedslimitand MUST be reused on replay and fork, never recomputed. budget.maxTokensandbudget.maxCostUsdclamp tomaxBudgetTokensandmaxBudgetCostUsd.maxRequestBodyBytesis the largest REST request body accepted.
ai section
providerMUST be inaiProviders.providers, else400 validation_error.credentialRefMUST reference a provider inaiProviders.byok, else403 credential_forbidden. It never carries key material.mockProvideris test-keys-only: a host MUST refuse it on a production credential (403).maxRefusalsis the refusal ceiling (host-services.md §Structured output).
distillation section
tokenBudget resolves to min(tokenBudget, memory.distillation.maxTokenBudget); a run that cannot distill within it MUST fail atomically with token_budget_exceeded.
budget section
The effective budget is the minimum across the scopes that apply (run, workflow, agent, project), clamped per §Limits; only the run scope has a wire surface. Whichever of budget and the run section binds first fires its own cap.breached kind.
budget.reservedrecords the effective budget, and consumption is derived fromprovider.usage,agent.toolCalledandnode.retried, never measured twice. A replay reuses both.- Under either
enforcemode a host MUST emitbudget.reserved,budget.threshold-crossedandbudget.exhausted, and MAY coalescebudget.consumed. hardexhaustion underonExhaustion: failemitscap.breached(kind: budget-*) and fails the runbudget_exhausted. Underinterrupt, where the host serves it, it raises an approval whoseresumeValueadds budget, recorded by a secondbudget.reserved. Anadvisoryhost MUST NOT stop the run.- A resolved model outside
modelAllow, or inmodelDeny(which wins), is refusedbudget_model_deniedbefore the call. dimensionslists only what the host enforces and MAY omitcost. Thebudget.*events andcap.breachedMUST NOT carry rate cards, unit prices, cost breakdowns, credentials or model prose (budget-no-pricing-leak); the aggregate cost is allowed.
Validation and persistence
- An unknown root key, an unknown key inside a section, or a dotted key (
ai.provideras a string key) MUST be rejected with400 validation_error. - A host MUST persist
RunOptionson the run at creation, MUST surface the sameconfigurableto every attempt of a node, and MUST NOT allowconfigurableto change after creation. - A workflow's
configurableSchemaMUST be validated against at create time and MUST be surfaced ongetWorkflow.
tags and metadata
tagsis an opaque string array: at most 100 entries, each at most 256 characters, valid UTF-8. A host MUST NOT reject a tag on format, and MUST return400 validation_errorover the limits.metadatais a free-form JSON object. A host MUST persist it, and the engine MUST NOT consume it for any execution decision.- Both surface unchanged on
RunSnapshot.
Snapshot
getRun returns schemas/v2/run-snapshot.schema.json: the fold of the event log through the run projection. The object is closed. runId, workflowId, status, owner and eventLogSchemaVersion are REQUIRED.
| Field | Meaning |
|---|---|
owner | { tenant, workspace?, subject }, closed; subject REQUIRED (schemas/v2/subject.schema.json) |
status | Run state (below) |
eventLogSchemaVersion | The era key, integer ≥ 2 (persistence.md §"The era key") |
engineVersion | Integer |
compensationStatus | none, pending, running, completed, partial, failed, manual |
currentNodeId | Set while suspended; names the node holding the interrupt |
error | { code, message, details? } on terminal failed |
configurable, tags, metadata | The persisted RunOptions |
agent, runOrchestrator | schemas/v2/agent-ref.schema.json |
metrics.openwopCost | { usd, tokens { input, output }, model, provider, duration_ms }; absence is not zero |
Field rules:
statusis one ofpending,running,paused,waiting-approval,waiting-input,waiting-external,completed,failed,cancelling,cancelled.waiting-externalMUST be used when the suspended interrupt'skindisexternal-event.cancellingis the state between an accepted cancel and the terminalcancelled. The vocabulary grows by overview.md §0.owner: a run created before the host emitted subjects reads with the subject rule in identity.md, stamped at first read and never rewritten.compensationStatus: a host that does not advertisecompensationMUST omit it. A host that does MUST include it on every snapshot,nonewhen never requested.runOrchestratorMUST NOT change for the run's lifetime.
Caching and encoding
- The
200SHOULD carry anETagderived from the latest persistedsequence. When present it MUST change on every observable transition and be stable otherwise. - A host evaluates
If-None-Matchas RFC 9110 §13.1.2 defines it:*, or a list of entity tags any of which matches the currentETagunder weak comparison (W/is ignored on either side). This holds wherever a host honorsIf-None-Match, discovery included.
- On a match the host MUST answer 304 with no body, carrying the ETag and the Vary the 200 would carry. - The host MUST evaluate it only when the unconditional response would be 2xx. A run the caller cannot read stays 404, whatever the request carries. - A request Cache-Control: no-cache MUST NOT suppress the evaluation.
- A host MAY compress (
gzipbaseline;brandzstdonly where advertised underextensions["<org>.rest-transport"].contentEncodings, ext/restTransport). It MUST then setContent-EncodingandVary: Accept-Encoding. The decoded body is byte-identical.
List
GET /runs (gated on runList) returns { runs: RunSnapshot[], nextCursor? }: the caller's runs, newest first.
- Only runs whose tenant segment is the caller's appear, and every
runIdis bound (identity.md §5). A run the caller created MUST appear. - A page MUST NOT exceed
runList.maxPageSize. cursoris opaque. A cursor the host did not mint MUST be refused with400 validation_error.workflowIdandstatusare exact-match filters whenrunList.filtersnames them. An unadvertised filter is ignored.
Cancel
cancelRun accepts { reason? } and answers 200 { runId, status }, where status is cancelling or cancelled. The cascade MAY be asynchronous; the run emits run.cancelled when it completes.
- A cancel on a terminal run (
completed,failed,cancelled) MUST be refused with409 run_terminal. - Cancelling a parent MUST NOT silently abandon an active compensation (security-defaults.md).
run.cancelled.parentRunIdwithreason: parent-cancelledrecords a cascade from a parent.
Bulk cancel
bulkCancelRuns accepts { runIds[1..100], reason? }.
- Over the host's cap (RECOMMENDED 100) it MUST return
400 validation_errorwithdetails.maxRunIds. - The host MUST process each id independently, and MUST return
200 { results[] }in request order, even when every id failed. - The host MUST enforce authorization per id. A run the caller cannot see yields
ok: falsewith an error envelope in that entry, never a top-level403.
Per-entry errors (errors.md):
| Condition | Code |
|---|---|
| The id's tenant segment is not the caller's (identity.md §5 applies inside an entry as on a path) | id_tenant_mismatch, or not_found where existence is not leaked |
| A run in the caller's tenant the caller may not cancel | run_forbidden |
| A run already terminal | run_terminal |
ok: true carries status cancelling or cancelled; ok: false carries the error envelope.
Pause and resume
pauseRun accepts { reason?, drainPolicy? } and answers 202 { runId, status: 'paused', pausedAt? }. The transition emits run.paused, whose payload echoes the request's drainPolicy word.
drainPolicy is one of:
drain-current-node(default) — the executing node reaches a terminal first.immediate— the run is snapshotted between events. The executing attempt is cut: it has no terminal node event, a host MUST NOT recordnode.failed(or any terminal node event) for it, and the resumed run'snode.startedbegins a fresh attempt.run.pauseditself records the interruption; its payload MAY carryinterruptedNodeIdandinterruptedAttempt.
resumeRun accepts { reason? }, answers 202 { runId, status: 'running', resumedAt? }, and emits run.resumed.
Rules:
- A pause on a run that is already paused, terminal, or otherwise unpausable MUST receive
409. - A resume on a run that is not paused MUST return
409. - In both cases the code is
run_terminalwhen the run is terminal, elserun_state_conflictwithdetails.runStatusnaming the refusing status. - Only
resumeRunor a cancel exitspaused. - A replay MUST fold
run.pausedandrun.resumedas no-ops for projected state.
Fork
forkRun accepts { mode: replay | branch, fromSeq?, runOptionsOverlay? }. Events with sequence < fromSeq are fixed history; events ≥ fromSeq re-execute.
fromSeqis REQUIRED forbranchand defaults to0forreplay.runOptionsOverlayisbranch-only. Areplaywith a non-empty overlay MUST be rejected with400.- A
fromSeqnot in the source log MUST be rejected with422 fork_point_invalid.
The 201 response is { runId, sourceRunId, fromSeq?, mode, status, eventsUrl }. The child's owner is copied verbatim from the parent. Determinism, side-effect suppression, and forking an era-2 parent are in replay.md.
Diff and ancestry
diffRun returns schemas/v2/run-diff-response.schema.json: divergedAtSeq, ordered eventDiffs[], stateDiff, optional truncated.
- The diff MUST be a pure function of the two logs. Identical logs MUST yield
divergedAtSeq: nulland emptyeventDiffs. eventId,runId,timestampand other run-scoped fields MUST be excluded from comparison.- A host that diffs an in-flight prefix MUST set
truncated: true. - A caller lacking
runs:readon either run MUST receive403.
getRunAncestry returns schemas/v2/run-ancestry-response.schema.json (runId, hostId, parent or null). A client walks the chain one hop at a time via parent.wellKnownUrl.
parent is the dispatching run, cause its composition mechanism. A fork is not dispatched (its lineage is parentRunId): its parent MUST be null.
Annotations, artifacts, eval summary
Annotations
createAnnotation accepts schemas/v2/annotation-create.schema.json and returns 201 with schemas/v2/annotation.schema.json. listAnnotations returns { annotations[] }.
An annotation is a live notification (run.annotated), never a run event. It MUST NOT enter the event log and MUST be excluded from fork, replay and diff.
Artifacts
getArtifact answers application/json with an implementation-defined object or, when Accept prefers application/a2a+json, an A2A Artifact (schemas/v2/artifact.schema.json). A host SHOULD offer the latter.
- A body served as
application/a2a+jsonMUST validate against that schema, withartifactIdequal to the path's. - A
urlPart in it MUST NOT resolve beyond the caller'sartifacts:readauthorization.
Eval summary
getEvalSummary returns schemas/v2/eval-summary.schema.json for a terminal eval run, 409 while it is running, and 404 when the run is not an eval run. The summary MUST be free of task output, rubric prose and credentials.
Conversation and residency capabilities
conversationPrimitive
conversationPrimitive carries no payload: its presence is the claim (capabilities.md §2). A host that does not advertise it MUST refuse a workflow whose nodes[].typeId references core.conversationGate — at registration or at run creation — with 422 capability_required, naming the family in details.requiredCapability.
A conversation turn MAY carry parts: a non-empty array of A2A Part objects (schemas/v2/part.schema.json) that marks the turn A2A-shaped. A producer SHOULD emit it and keep content readable by consumers that predate it. A turn without parts stays valid on emission, replay and fork.
dataResidency
A host advertising dataResidency MUST honor-or-reject, and MUST NOT silently accept-and-ignore:
- accept a
residencyconstraint naming a region indataResidency.regions; - refuse one it does not advertise with
residency_unavailable.
A host that does not advertise dataResidency MAY ignore or reject a residency constraint, but MUST NOT claim to honor it.
Dead letters
A host advertising deadLetter MUST, when a run or node exhausts its retry policy:
- route it to the dead-letter sink and emit
run.dead-lettered, whosereasonis redaction-safe and which carries no credential or payload material; - keep the failed run fork-eligible for
retentionDays, and not purge it sooner; - purge it after
retentionDays, after which a fork fails as for an unknown run.
Queue messages (host-services.md §queueBus) and webhook deliveries (webhooks.md §Dead letters) have sinks of their own.
During the v1 overlap
A non-terminal run inherited from v1 continues, or is cancelled v1_pin_unsupported, per persistence.md §"Runs pinned to v1".
Sources: RFCs 0053, 0058, 0084, 0170, 0171, 0176, 0182, 0228, 0229, 0231, 0235.