# Scenario walkthroughs

> Source: https://openwop.dev/scenarios/ · OpenWOP, the open-source protocol for multi-agent workflow orchestration.

Three end-to-end examples of OpenWOP runs, showing the workflow definition, the wire-level events the host emits, and what the client sees. Each scenario links back to the normative spec sections that govern its surface.

Reading time: ~15 minutes total. Each scenario is independent — skim the one that matches what you're building.

## Scenario A — Single agent with human approval

**What it shows:** the most common pattern — a supervisor agent makes a decision, asks for human confirmation, resumes when the human answers, and terminates.

**Spec surfaces touched:** [run lifecycle](https://openwop.dev/spec/v2/core/runs.html), [interrupts](https://openwop.dev/spec/v2/core/interrupt.html), [stream modes](https://openwop.dev/spec/v2/core/events.html).

### Workflow definition

```json
{
  "id": "wf-approve-and-act",
  "version": 1,
  "nodes": [
    { "id": "decide",  "typeId": "core.orchestrator.supervisor" },
    { "id": "ask",     "typeId": "core.hitl.clarify" },
    { "id": "act",     "typeId": "core.http.request" }
  ],
  "edges": [
    { "from": "decide", "to": "ask" },
    { "from": "ask",    "to": "act" }
  ],
  "channels": [
    { "name": "decision", "type": "string" },
    { "name": "approval", "type": "boolean" }
  ]
}
```

### Wire trace

Client creates the run:

```http
POST /runs
OpenWOP-Version: 2
Authorization: Bearer …
Content-Type: application/json
Idempotency-Key: ck-2026-05-21-001

{ "workflowId": "wf-approve-and-act", "inputs": { "url": "https://api.example.com/charge" } }
```

Host responds:

```http
HTTP/1.1 201 Created
{ "runId": "run-abc", "status": "pending", "eventsUrl": "/runs/run-abc/events" }
```

Client opens an SSE stream:

```http
GET /runs/run-abc/events?streamMode=updates
OpenWOP-Version: 2
Accept: text/event-stream
```

Events flow:

```
event: run.started        { "runId": "run-abc", "ts": "2026-05-21T18:00:00Z" }
event: runOrchestrator.decided { "agentId": "supervisor",
                               "decision": { "kind": "ask-user", "prompt": "Charge $42 to example.com?" } }
event: interrupt.requested    { "kind": "clarification", "key": "ask-approval",
                               "data": { "questions": [{ "id": "approval", "question": "Charge $42 to example.com?" }] } }
```

The run is now `waiting-input`. The host serializes its state and waits.

### Human answers, run resumes

```http
POST /runs/run-abc/interrupts/ask
{ "resumeValue": { "approval": true } }
```

Events continue:

```
event: interrupt.resolved { "nodeId": "ask", "interruptId": "…", "kind": "clarification" }
event: node.started       { "nodeId": "act", "typeId": "core.http.request" }
event: node.completed     { "nodeId": "act", "outputs": { "status": 200 } }
event: run.completed      { "outputs": { "status": 200 } }
```

The `runId` is stable across the pause; deep linking, replay, and fork all work against the same id.

### What just happened, normatively

1. `POST /runs` with `Idempotency-Key` accepted per [`idempotency.md`](https://openwop.dev/spec/v2/core/idempotency.html). The same key on retry returns the existing run.
2. The orchestrator emitted `runOrchestrator.decided { decision: { kind: "ask-user" } }` per [RFC 0006](https://openwop.dev/rfcs/0006-orchestrator.html) §B–§C.
3. `interrupt.requested` with `kind: "clarification"` per [`interrupt.md`](https://openwop.dev/spec/v2/core/interrupt.html) §Payload. The run is in `waiting-input` status.
4. `POST /runs/{runId}/interrupts/{nodeId}` accepts the answer per [`interrupt.md`](https://openwop.dev/spec/v2/core/interrupt.html) §"Resolve surfaces." The host MUST validate the `resumeValue` against the interrupt's `resumeSchema`, when one is declared, before resuming.
5. SSE event ordering preserved across the suspension — `sequence` is strictly increasing per run, and a client that reconnects with `Last-Event-ID` receives every later event in log order per [`events.md`](https://openwop.dev/spec/v2/core/events.html).

---

## Scenario B — Multi-agent fan-out with envelope retries

**What it shows:** a supervisor dispatches to two worker agents in sequence, each producing a typed envelope. The second agent's first envelope fails schema validation and the host retries automatically before the run continues.

**Spec surfaces touched:** [multi-agent execution](https://openwop.dev/spec/v2/core/capabilities.html#multiagent), [AI envelope](https://openwop.dev/spec/v2/core/capabilities.html#aienvelope), [RFC 0032](https://openwop.dev/rfcs/0032-envelope-reliability-events.html) reliability events.

### Workflow definition

```json
{
  "id": "wf-research-and-summarize",
  "version": 1,
  "nodes": [
    { "id": "supervise", "typeId": "core.orchestrator.supervisor" },
    { "id": "research",  "typeId": "core.openwop.agents.deep-research",
      "config": { "envelope": "research.findings" } },
    { "id": "summarize", "typeId": "core.llm.chat",
      "config": { "envelope": "summary.brief", "model": "claude-sonnet-4-6" } }
  ],
  "edges": [
    { "from": "supervise", "to": "research"  },
    { "from": "supervise", "to": "summarize" }
  ]
}
```

`fanOutPolicy: "sequential"` is the default per [RFC 0022](https://openwop.dev/rfcs/0022-dispatch-input-output-mapping.html); see that RFC for the parallel variant.

### Wire trace

Run starts. Supervisor dispatches to `research`:

```
event: run.started
event: runOrchestrator.decided { "agentId": "supervisor",
                               "decision": { "kind": "next-worker", "nextWorkerIds": ["research", "summarize"] } }
event: node.started       { "nodeId": "research", "typeId": "core.openwop.agents.deep-research" }
event: agent.toolCalled   { "callId": "tc-001", "agentId": "research", "toolName": "web.search", "argsHash": "sha256:…" }
event: agent.toolReturned { "callId": "tc-001", "agentId": "research", "toolName": "web.search", "status": "ok" }
event: node.completed     { "nodeId": "research" }
```

Supervisor dispatches to `summarize`. First attempt at the summary envelope fails schema validation:

```
event: node.started       { "nodeId": "summarize", "typeId": "core.llm.chat" }
                          ← attempt 1 emits a summary.brief envelope that fails schema validation
event: envelope.retry.attempted { "nodeId": "summarize", "attempt": 2, "reason": "schema-violation" }
event: node.completed     { "nodeId": "summarize" }
event: run.completed      { "outputs": { … } }
```

### What just happened, normatively

1. `runOrchestrator.decided { decision: { kind: "next-worker", nextWorkerIds: [...] } }` is the supervisor decision per [RFC 0006](https://openwop.dev/rfcs/0006-orchestrator.html) §B–§C; the planner→worker handoff it drives is [RFC 0037](https://openwop.dev/rfcs/0037-multi-agent-execution-model.html) §B.
2. `core.dispatch` iterates `nextWorkerIds` sequentially per [RFC 0022](https://openwop.dev/rfcs/0022-dispatch-input-output-mapping.html) §D. The parent variable bag is the cross-worker handoff channel per §D.
3. The invalid first attempt triggers a host-side retry per [RFC 0032](https://openwop.dev/rfcs/0032-envelope-reliability-events.html) §B.1 — `envelope.retry.attempted` (SHOULD-tier; the first attempt emits nothing) carries the attempt count and the canonical `reason`.
4. If the retry budget were exhausted, the host would emit `envelope.retry.exhausted` (a MUST under RFC 0032 §B.2) and the run would fail with `envelope_invalid` (see [Error codes](https://openwop.dev/errors/), HTTP 422). Here the second attempt validated.
5. `agent.toolCalled` + `agent.toolReturned` pair via shared `callId` per [RFC 0002](https://openwop.dev/rfcs/0002-agent-identity-and-reasoning-events.html). On a host advertising `toolHooks.prePostEvents` ([RFC 0064](https://openwop.dev/rfcs/0064-tool-invocation-hooks-and-authorization.html)), the call carries `argsHash` — SHA-256 over the JCS-canonicalized args with secrets already redacted (SR-1), the content-free alternative to raw `inputs` — and the return carries `status`.

---

## Scenario C — Suspend, restart, replay-fork from checkpoint

**What it shows:** a long-running workflow is interrupted by a host restart, comes back up from its event log, and the operator forks a new run from a historical checkpoint to test a policy change.

**Spec surfaces touched:** [replay](https://openwop.dev/spec/v2/core/replay.html), [storage adapters](https://openwop.dev/spec/v2/core/persistence.html), [version negotiation](https://openwop.dev/spec/v2/core/versioning.html).

### Setup

A run started yesterday with the SQLite reference host. Mid-run, the host process restarts.

```
event: run.started        { "runId": "run-xyz" }                  sequence 0
event: node.started       { "nodeId": "fetch-data" }              sequence 1
event: node.completed     { "nodeId": "fetch-data" }              sequence 2
event: node.started       { "nodeId": "transform" }               sequence 3
event: node.completed     { "nodeId": "transform" }               sequence 4
                          ← host process exits here
```

### Host comes back up

On startup, the host scans its event log for runs in non-terminal status. For `run-xyz` it finds the last persisted event and continues, with no client action:

```
event: node.started       { "nodeId": "publish", "typeId": "core.http.request" }    sequence 5
event: node.completed     { "nodeId": "publish" }                                   sequence 6
event: run.completed      { "outputs": { … } }                                      sequence 7
```

No `run.resumed` is emitted: that event marks the exit from `paused`, and a restart is not a pause. Every event carries its `sequence`, so a client whose stream dropped reconnects with `Last-Event-ID` set to the last sequence it saw and receives exactly the events after it — which is how it deduplicates if its downstream had partial state ([`events.md`](https://openwop.dev/spec/v2/core/events.html) §"SSE frames").

### Operator forks from a historical checkpoint

A policy change in the `transform` node means the operator wants to re-run the second half of yesterday's pipeline with the new logic, without re-fetching data.

```http
POST /runs/run-xyz:fork
{ "mode": "branch",
  "fromSeq": 4,
  "runOptionsOverlay": { "configurable": { "transformMode": "v2" } } }
```

Host responds:

```http
HTTP/1.1 201 Created
{ "runId": "run-xyz-fork-1",
  "sourceRunId": "run-xyz",
  "mode": "branch",
  "fromSeq": 4,
  "status": "pending",
  "eventsUrl": "https://api.example.com/runs/run-xyz-fork-1/events" }
```

The new run inherits events 0..3 from the parent as fixed history (all of `fetch-data`, and the start of `transform`), then begins fresh execution from event 4 — `transform`'s result onward — with the new `transformMode: "v2"`. The original `run-xyz` is unchanged.

### What just happened, normatively

1. `sequence` is strictly increasing per run and persisted logs are never renumbered ([`events.md`](https://openwop.dev/spec/v2/core/events.html) §"The envelope"). A host that accepted work MUST resume it after the accepting process dies, without client action, per [`persistence.md`](https://openwop.dev/spec/v2/core/persistence.html) §"Durable acceptance and recovery."
2. Recovery is not a pause: `run.resumed` is emitted only when a run leaves `paused` ([`runs.md`](https://openwop.dev/spec/v2/core/runs.html) §"Pause and resume"), and it carries no restart cursor — the client's cursor is `Last-Event-ID`.
3. `POST /runs/{runId}:fork` is normative for replay-fork per [`runs.md`](https://openwop.dev/spec/v2/core/runs.html) §"Fork" and [`replay.md`](https://openwop.dev/spec/v2/core/replay.html). Events with `sequence < fromSeq` are fixed history; events from `fromSeq` on are re-executed. In `replay` mode that prefix is byte-equivalent to the parent (§"Byte-equivalence of the prefix"); a `branch` like this one starts from the projected state at `fromSeq` with the caller's `runOptionsOverlay` and is not deterministic by design.
4. The fork carries a new `runId` so client-side deep links to the parent remain stable.
5. Per-region clock fields MAY differ on the fork (the fork executes "now", not at the parent's event times) per [`replay.md`](https://openwop.dev/spec/v2/core/replay.html) §"Byte-equivalence of the prefix" (RFC 0036 §E).

---

## What these scenarios skip

Three things deliberately left out, all linked here for the curious:

- **Auth handshake** — every request carries `Authorization: Bearer …`. See [Auth profiles](https://openwop.dev/spec/v2/core/identity.html) for OAuth2 / OIDC / mTLS variants.
- **Webhook delivery** — the SSE stream above is the synchronous view; production hosts also deliver every event over HMAC-signed webhooks per [`webhooks.md`](https://openwop.dev/spec/v2/core/webhooks.html).
- **BYOK secret resolution** — the `summarize` node in Scenario B used a host-managed Anthropic key. For BYOK, see the [`aiProviders`](https://openwop.dev/spec/v2/core/capabilities.html#aiproviders) `byok` facet and the `ai.credentialRef` run option in [`runs.md`](https://openwop.dev/spec/v2/core/runs.html) §"Run options."

## Where to go next

| Want to | Go to |
|---|---|
| See the actual workflow-definition schema | [`/schemas/workflow-definition.schema.json`](https://openwop.dev/schemas/workflow-definition.schema.json) |
| Walk through the full REST endpoint catalog | [`/api/rest/`](https://openwop.dev/api/rest/) |
| See the production-shaped host that does all this end-to-end | [Postgres reference host](https://github.com/openwop/openwop-examples/tree/main/examples/hosts/postgres) |
| Run scenario A against the live demo | [app.openwop.dev](https://app.openwop.dev/) |
