OpenWOP openwop.dev

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 runId whose tenant segment is not the caller's with 403 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.

OperationMethod and pathScopeGate
createRunPOST /runsruns:create—
getRunGET /runs/{runId}runs:read—
listRunsGET /runsruns:readrunList
streamRunEventsGET /runs/{runId}/eventsruns:readevents.md
pollRunEventsGET /runs/{runId}/events/pollruns:readevents.md
cancelRunPOST /runs/{runId}/cancelruns:cancel—
bulkCancelRunsPOST /runs:bulk-cancelruns:cancel—
pauseRunPOST /runs/{runId}:pauseruns:cancel—
resumeRunPOST /runs/{runId}:resumeruns:cancel—
forkRunPOST /runs/{runId}:forkruns:create + runs:readreplay
diffRunGET /runs/{runId}:diff?against=runs:read on bothOPTIONAL
getRunAncestryGET /runs/{runId}/ancestryruns:readmultiAgent.executionModel.crossHostCausation.ancestryEndpointSupported
createAnnotation / listAnnotationsPOST / GET /runs/{runId}/annotationsruns:annotate / runs:readfeedback
getArtifactGET /runs/{runId}/artifacts/{artifactId}artifacts:read—
getEvalSummaryGET /runs/{runId}/eval-summaryruns:readagents.evalSuite
getRunCompensationGET /runs/{runId}/compensationruns:readcompensation
getRunEffectsGET /runs/{runId}/effectsruns:readidempotency

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 unless mode: eval.
  • inputs, residency, tenantId, scopeId.
  • callbackUrl — see interrupt.md §Callback delivery. A refused value is 400 validation_error with details.field: "callbackUrl".
  • mode, evalSuiteRef, agentId.
  • runSecrets — run-supplied secrets, accepted only by a host advertising secrets.runSecrets (host-services.md §Run-supplied secrets). It is never part of RunOptions.
  • The RunOptions fields configurable, tags, metadata. A body without RunOptions MUST be accepted as if it were {}.

Request headers

HeaderRule
Idempotency-KeyRECOMMENDED. A replayed create MUST NOT create a second run, and carries OpenWOP-Idempotent-Replay: true.
OpenWOP-Dedup: enforceThe host MUST reject a duplicate (tenantId, scopeId) with 409 run_already_active and Retry-After.
OpenWOP-Force-Engine-VersionTest 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.

  • eventsUrl and statusUrl MUST 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, or http:// on an https:// origin, is non-conformant.
  • The base that minted runId MUST resolve it: GET /runs/{runId} and GET /runs/{runId}/events/poll at that base MUST answer 200 for the returned id, percent-encoded per identity.md §5.

Refusals

  • mode: eval makes evalSuiteRef and agentId REQUIRED. It starts an eval-suite projection that emits the content-free eval.* family and terminates with an EvalSummary. A host that does not advertise agents.evalSuite MUST reject it 422 capability_not_provided.
  • A host advertising dataResidency MUST reject a residency.region outside dataResidency.regions with 422 residency_unavailable and 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.onExhaustion serves exactly those values. It MUST reject any other budget.onExhaustion with 422 capability_not_provided and 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:

SectionKeys
runrecursionLimit, runTimeoutMs, maxLoopIterations, escalationThreshold
aiprovider, model, temperature (0..2), maxTokens, credentialRef, promptOverrides, mockProvider, reasoningVerbosity (none | summary | full), maxRefusals
distillationtokenBudget
budgetschemas/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

  • recursionLimit is clamped to limits.maxNodeExecutions. A breach, counted in node starts, MUST emit cap.breached { kind: 'node-executions' } and fail the run with recursion_limit_exceeded.
  • runTimeoutMs resolves to min(runTimeoutMs, limits.maxRunDurationMs), measured from run.started. A breach MUST emit cap.breached { kind: 'run-duration' } and terminate the run failed with run_timeout.
  • maxLoopIterations resolves against limits.maxLoopIterations, counted in orchestrator turns. A breach MUST emit cap.breached { kind: 'loop-iterations' } and fail with loop_limit_exceeded.
  • An out-of-range recursionLimit or runTimeoutMs MUST return 400 validation_error at create. After a breach the host schedules nothing more.
  • escalationThreshold is the low-confidence threshold (interrupt.md).

Limits

limits always carries clarificationRounds (per task).

  • A host advertising maxNodeExecutions, maxRunDurationMs, maxLoopIterations, maxToolRounds or maxRepairRounds MUST enforce it.
  • On any breach the host MUST emit cap.breached and fail the node or run. The event carries nodeId for a clarification or schema breach. Its observed exceeds limit and MUST be reused on replay and fork, never recomputed.
  • budget.maxTokens and budget.maxCostUsd clamp to maxBudgetTokens and maxBudgetCostUsd.
  • maxRequestBodyBytes is the largest REST request body accepted.

ai section

  • provider MUST be in aiProviders.providers, else 400 validation_error.
  • credentialRef MUST reference a provider in aiProviders.byok, else 403 credential_forbidden. It never carries key material.
  • mockProvider is test-keys-only: a host MUST refuse it on a production credential (403).
  • maxRefusals is 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.reserved records the effective budget, and consumption is derived from provider.usage, agent.toolCalled and node.retried, never measured twice. A replay reuses both.
  • Under either enforce mode a host MUST emit budget.reserved, budget.threshold-crossed and budget.exhausted, and MAY coalesce budget.consumed.
  • hard exhaustion under onExhaustion: fail emits cap.breached (kind: budget-*) and fails the run budget_exhausted. Under interrupt, where the host serves it, it raises an approval whose resumeValue adds budget, recorded by a second budget.reserved. An advisory host MUST NOT stop the run.
  • A resolved model outside modelAllow, or in modelDeny (which wins), is refused budget_model_denied before the call.
  • dimensions lists only what the host enforces and MAY omit cost. The budget.* events and cap.breached MUST 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.provider as a string key) MUST be rejected with 400 validation_error.
  • A host MUST persist RunOptions on the run at creation, MUST surface the same configurable to every attempt of a node, and MUST NOT allow configurable to change after creation.
  • A workflow's configurableSchema MUST be validated against at create time and MUST be surfaced on getWorkflow.

tags and metadata

  • tags is 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 return 400 validation_error over the limits.
  • metadata is 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.

FieldMeaning
owner{ tenant, workspace?, subject }, closed; subject REQUIRED (schemas/v2/subject.schema.json)
statusRun state (below)
eventLogSchemaVersionThe era key, integer ≥ 2 (persistence.md §"The era key")
engineVersionInteger
compensationStatusnone, pending, running, completed, partial, failed, manual
currentNodeIdSet while suspended; names the node holding the interrupt
error{ code, message, details? } on terminal failed
configurable, tags, metadataThe persisted RunOptions
agent, runOrchestratorschemas/v2/agent-ref.schema.json
metrics.openwopCost{ usd, tokens { input, output }, model, provider, duration_ms }; absence is not zero

Field rules:

  • status is one of pending, running, paused, waiting-approval, waiting-input, waiting-external, completed, failed, cancelling, cancelled. waiting-external MUST be used when the suspended interrupt's kind is external-event. cancelling is the state between an accepted cancel and the terminal cancelled. 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 advertise compensation MUST omit it. A host that does MUST include it on every snapshot, none when never requested.
  • runOrchestrator MUST NOT change for the run's lifetime.

Caching and encoding

  • The 200 SHOULD carry an ETag derived from the latest persisted sequence. When present it MUST change on every observable transition and be stable otherwise.
  • A host evaluates If-None-Match as RFC 9110 §13.1.2 defines it: *, or a list of entity tags any of which matches the current ETag under weak comparison (W/ is ignored on either side). This holds wherever a host honors If-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 (gzip baseline; br and zstd only where advertised under extensions["<org>.rest-transport"].contentEncodings, ext/restTransport). It MUST then set Content-Encoding and Vary: 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 runId is bound (identity.md §5). A run the caller created MUST appear.
  • A page MUST NOT exceed runList.maxPageSize.
  • cursor is opaque. A cursor the host did not mint MUST be refused with 400 validation_error.
  • workflowId and status are exact-match filters when runList.filters names 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 with 409 run_terminal.
  • Cancelling a parent MUST NOT silently abandon an active compensation (security-defaults.md).
  • run.cancelled.parentRunId with reason: parent-cancelled records 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_error with details.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: false with an error envelope in that entry, never a top-level 403.

Per-entry errors (errors.md):

ConditionCode
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 cancelrun_forbidden
A run already terminalrun_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 record node.failed (or any terminal node event) for it, and the resumed run's node.started begins a fresh attempt. run.paused itself records the interruption; its payload MAY carry interruptedNodeId and interruptedAttempt.

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_terminal when the run is terminal, else run_state_conflict with details.runStatus naming the refusing status.
  • Only resumeRun or a cancel exits paused.
  • A replay MUST fold run.paused and run.resumed as no-ops for projected state.

Fork

forkRun accepts { mode: replay | branch, fromSeq?, runOptionsOverlay? }. Events with sequence < fromSeq are fixed history; events ≥ fromSeq re-execute.

  • fromSeq is REQUIRED for branch and defaults to 0 for replay.
  • runOptionsOverlay is branch-only. A replay with a non-empty overlay MUST be rejected with 400.
  • A fromSeq not in the source log MUST be rejected with 422 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: null and empty eventDiffs.
  • eventId, runId, timestamp and 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:read on either run MUST receive 403.

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+json MUST validate against that schema, with artifactId equal to the path's.
  • A url Part in it MUST NOT resolve beyond the caller's artifacts:read authorization.

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 residency constraint naming a region in dataResidency.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, whose reason is 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.

Next in Core.

Edit this page on GitHub ↗

  • IdentityWho owns a run, how each lane binds and revokes, and the grammar of links, ids and…
  • OpenWOP v2 Core — Overviewspec/v2/core/ is what a host implements to pass the 2.0.0 floor: reading order, the six…
  • ErrorsEvery error a v2 host returns is a row in one registry. A client routes on error, never…
  • Interruptinterrupt is how a run waits on something outside itself; every kind shares one payload…