> ## Documentation Index
> Fetch the complete documentation index at: https://docs.plungeai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflows plane — /v1/workflows

> Run multi-agent CNL workflows: inline (send the workflow document in the request), saved (reference a workflow id saved in Ocean Studio), each with an optional SSE streaming variant, plus a results-redemption route.


<!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/workflows.ts, docs/ONE-API-DEVELOPER-GUIDE-2.0.md | last-synced: 2026-09-23 -->
Run multi-agent CNL workflows: **inline** (send the workflow document in the
request), **saved** (reference a workflow id saved in Ocean Studio), each with
an optional **SSE streaming** variant, plus a results-redemption route.

Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here. Tenancy is
automatic — the engine keys every run to your key's identity; saved workflows
and results resolve only within it.

Authoring CNL YAML itself (task types, `parallel`/`sequential`, harness
missions) is this `plungeai-workflows` skill's job; this file covers the HTTP
surface for executing it.

## POST /v1/workflows/execute — inline execution

Two body formats:

**JSON** — `{"workflow": <object or YAML string>, "input"?: string, "inputs"?: object}`:

```bash
curl -s -X POST https://api.plungeai.com/v1/workflows/execute \
  -H "Authorization: Bearer ozk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workflow": {
      "name": "demo",
      "tasks": [
        {"type": "task", "id": "t1", "agent": "llm-agent", "prompt": "Say: ok"}
      ]
    },
    "input": "x"
  }'
```

**Raw YAML** — send the whole workflow document as the body with
`Content-Type: text/yaml`:

```bash
curl -s -X POST https://api.plungeai.com/v1/workflows/execute \
  -H "Authorization: Bearer ozk_YOUR_KEY" \
  -H "Content-Type: text/yaml" \
  --data-binary @- <<'YAML'
workflow:
  name: demo
  tasks:
    - type: task
      id: t1
      agent: llm-agent
      prompt: "Say: ok"
YAML
```

Response — HTTP 200 (an **acknowledgement + pointer**, not the content):

```json
{
  "success": true,
  "workflow_id": "00000000-0000-4000-8000-000000000001",
  "final_task_id": "t1",
  "request_id": "00000000-0000-4000-8000-000000000004"
}
```

Redeem the actual output with `workflow_id` + `final_task_id` (below). For a
multi-task workflow, `final_task_id` is the last task — intermediate task
results are redeemable by their own task ids.

Errors: `400 missing_workflow` (no `workflow` in a JSON body / empty YAML),
`502 engine_error` (engine reported failure; message included),
`404 workflow_not_found` — yes, on the **inline** route too: any engine
failure whose message contains "not found" (typically an unknown agent id →
"Service binding not found") maps to 404. Re-discover the agent id; don't hunt
for a missing saved workflow.

## POST /v1/workflows/{id}/execute — saved workflow

Runs a workflow saved under your account (the id shown in Ocean Studio).
Body is optional: `{"input"?: string, "inputs"?: object}`.

```bash
curl -s -X POST https://api.plungeai.com/v1/workflows/wf-morning-brief/execute \
  -H "Authorization: Bearer ozk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": "today"}'
```

```json
{
  "success": true,
  "workflow_id": "00000000-0000-4000-8000-000000000001",
  "final_task_id": "t3",
  "request_id": "..."
}
```

Errors: `404 workflow_not_found` (no such id under your identity — list your
saved workflows in Studio, or re-check the id), `502 engine_error`.

## SSE streaming — live workflow events

Two variants, same event stream:

- `POST /v1/workflows/{id}/execute-stream` — saved workflow (in the OpenAPI spec)
- `POST /v1/workflows/execute-stream` — inline body, same formats as
  `/v1/workflows/execute`, fully documented in the OpenAPI spec
  (`orchestration/api-gateway/openapi.ts`) alongside the saved-workflow
  variant. Caveat: this inline variant forwards `input` but **silently drops
  `inputs`** (the JSON branch never reads `body.inputs`, unlike the YAML
  branch which does — `orchestration/api-gateway/routes/workflows.ts`) — need
  multi-input? Use `/v1/workflows/execute` or `/v1/workflows/{id}/execute-stream`
  (both forward `inputs`).

```bash
curl -N -X POST https://api.plungeai.com/v1/workflows/wf-morning-brief/execute-stream \
  -H "Authorization: Bearer ozk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": "today"}'
```

Response is `text/event-stream` with **named events** — each frame is
`event: <type>` + `data: <json>`, and the data payload always carries
`{workflow_id, execution_id, timestamp, type, data, elapsed_ms, duration_ms?}`
(the event-specific fields live under `data`). Event sequence:

```
request_received → workflow_loaded → workflow_started
  → task_dispatched → task_completed   (per task)
  → heartbeat                          (every ~25s during a task >20s — expect it,
                                        or idle timeouts will kill long streams)
  → workflow_completed                 (success only; execution summary + metrics)
  → workflow_result                    (terminal, ALWAYS check its success field)
```

On failure the engine emits `workflow_error` (error + progress counters) and
the terminal `workflow_result` carries `success: false` + `error` — code that
only checks for the `workflow_result` event name reports failed runs as done.
Other event types appear for the matching task types (`parallel_start`,
`batch_progress`, `debate_*`, …) — switch on the ones you need, ignore the
rest.

A body the engine can't parse returns **HTTP 400 whose body is itself an SSE
`error` event** — check the status before treating the body as a stream.

Consume with any SSE client (`EventSource` won't do — it's a POST; use
`fetch` + a stream reader in JS, or `curl -N` / `httpx.stream` elsewhere) and
parse the `event:` lines, not just `data:` lines. The stream is pass-through
and unbuffered; `x-request-id` is stamped on the response headers. Don't rely
on a dropped stream's run surviving — the engine runs the workflow inside the
stream; for durability use `/v1/workflows/execute` + the results route.

## GET /v1/workflows/results/{workflowId}/{taskId} — redeem a task result

```bash
curl -s https://api.plungeai.com/v1/workflows/results/00000000-0000-4000-8000-000000000005/t1 \
  -H "Authorization: Bearer ozk_YOUR_KEY"
```

Ready — HTTP 200:

```json
{
  "content": "ok",
  "content_type": "text/plain",
  "workflow_id": "00000000-0000-4000-8000-000000000001",
  "task_id": "t1"
}
```

Not ready — HTTP 404 `{"error":{"code":"not_ready",...}}`. Poll with backoff
(2s+, growing); `not_ready` while a workflow runs is normal.

## POST /v1/workflows/executions/{id}/cancel — cancel a running execution

Cooperative cancel: writes the SharedMemory cancel marker the engine and agent
loops poll at their next turn/tool boundary, then stamps the execution row
`cancelled`. `{id}` is the execution id — the same value returned as
`workflow_id` from `/v1/workflows/execute` (or the `x-trace-id` you sent on
that call). Since `/v1/workflows/execute` is synchronous, send the cancel from
a second connection while the first is still running.

```bash
curl -s -X POST https://api.plungeai.com/v1/workflows/executions/00000000-0000-4000-8000-000000000005/cancel \
  -H "Authorization: Bearer ozk_YOUR_KEY"
```

```json
{
  "success": true,
  "cancelled": "00000000-0000-4000-8000-000000000005",
  "markers": ["00000000-0000-4000-8000-000000000005"]
}
```

Errors: `404 not_found` (no such execution under your identity — wrong owner
never confirms the execution exists), `409 not_running` (already finished or
already cancelled).

## Fire-and-redeem pattern (Python)

```python
import time, requests

BASE = "https://api.plungeai.com"
H = {"Authorization": "Bearer ozk_YOUR_KEY"}

ack = requests.post(f"{BASE}/v1/workflows/execute", headers=H, json={
    "workflow": {"name": "demo", "tasks": [
        {"type": "task", "id": "t1", "agent": "llm-agent", "prompt": "Say: ok"}]},
}).json()

wf, task = ack["workflow_id"], ack["final_task_id"]
for i in range(1, 9):
    r = requests.get(f"{BASE}/v1/workflows/results/{wf}/{task}", headers=H)
    if r.status_code == 200:
        print(r.json()["content"]); break
    time.sleep(2 * i)
```

## Trace correlation

Send `x-trace-id: <your-id>` on any execute call; every engine span of the run
(dispatches, completions, the result) lands under that id at
`GET /v1/traces/<your-id>` — see `plungeai-results-traces`.

## Legacy aliases (deprecated — do not use in new code)

`POST /v1/execute`, `POST /v1/cnl/execute`, `POST /v1/cnl/execute-stream` are
pre-router aliases of the inline workflow handlers, kept so old integrations
don't break. Same auth, same bodies, same responses. New and generated code
targets `/v1/workflows/*` only.
