> ## Documentation Index
> Fetch the complete documentation index at: https://www.plungeai.com/docs/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, orchestration/api-gateway/openapi-bots.ts, orchestration/api-gateway/docs/guide-3.0/16-bots.md, docs/ONE-API-DEVELOPER-GUIDE-2.0.md | last-synced: 2026-10-05 (every body field, event and status re-checked against GET /v1/openapi.json and routes/workflows.ts) -->
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, "user_context"?: object, "format"?: "json"|"yaml"|"markdown"|"text"}`.
Bare root `name` + `tasks` nest into a workflow, and a bare `agent: <id>` with that agent's own fields lifts to a
one-task workflow (`agent` next to `workflow` or `tasks` is `400 ambiguous_body`; a field named like an engine task key is
`400 reserved_field`). `format` (or `Accept`) picks how the answer is written; raw YAML in gets YAML out:

```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": "one-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: one-agent
      prompt: "Say: ok"
YAML
```

Response — HTTP 200 (an **acknowledgement + pointer**; once the synchronous run has stored its
result, `content` carries the final task's output too):

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

`status` is `completed`, or `incomplete` when the final task stopped at its turn budget (the result is partial).
Redeem any task's output with `workflow_id` + that task id (below); for a multi-task workflow `final_task_id` is the
last task and intermediate results are redeemable by their own task ids. The `workflow_id` is the execution id: it
is your `x-trace-id` when you sent one, and every `/v1/executions/{id}` route takes it.

Errors (envelope `{"error":{"code","message"}}`, header `X-Error-Code`): `400 missing_workflow` (none of `workflow`,
`agent`, `tasks`, or an empty YAML body), `ambiguous_body`, `reserved_field`, `invalid_json`, `invalid_yaml`,
`invalid_format`, `invalid_workflow` (the engine's structural check; the message names the problem); `403
agent_not_active` and `404 unknown_agent` (a named agent is parked or not in the registry — refused before dispatch,
re-discover the id); `409 duplicate_execution_id` (a reused `x-trace-id`); `413 payload_too_large` (over 1 MiB);
`424` (a connection-tier agent with no connection, `error.connect` says where to make it); `429 rate_limited`;
`502 engine_error`.

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

Runs a workflow, agent or bot saved under your account (the id shown in Ocean Studio, or returned by
`POST /v1/workflows`). Body is optional: `{"input"?: string, "inputs"?: object, "user_context"?: object, "format"?}`.

```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,
  "status": "completed",
  "workflow_id": "00000000-0000-4000-8000-000000000001",
  "final_task_id": "t3",
  "request_id": "...",
  "content": "…"
}
```

Errors: `404 workflow_not_found` (no such id under your identity — `GET /v1/workflows` lists yours),
`404 unknown_agent` / `403 agent_not_active` (the saved YAML names an agent that is gone or parked), `400
invalid_format`, `409 duplicate_execution_id`, `413`, `429`, `502 engine_error`. `on_behalf_of` is refused here
(`400 invalid_body`): only `POST /v1/execute` takes it.

## SSE streaming — live workflow events

Two variants, same event stream:

- `POST /v1/workflows/{id}/execute-stream` — saved workflow; body `{input?, inputs?, user_context?}`
- `POST /v1/workflows/execute-stream` — inline body (`{workflow, input?, inputs?, user_context?}` or raw YAML),
  same shapes as `/v1/workflows/execute`; all of `input`, `inputs` and `user_context` are forwarded.

```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 refusal before the first frame (a body that does not parse, an unknown or parked agent, a missing saved
workflow) is a plain JSON error envelope with the matching status (`400 invalid_workflow` / `invalid_yaml` /
`missing_workflow`, `403 agent_not_active`, `404 workflow_not_found` / `unknown_agent`, `413`, `429`,
`502 engine_error`), never an SSE frame — 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. A run that failed is `409 execution_failed` (the
message is the run's error); one that finished with no visible content is `422 empty_completion` (raise
`max_tokens` to 64 or more). `?format=` (`json`, `yaml`, `markdown`, `text`) picks the wire.

## 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). `POST /v1/executions/{id}/cancel` is the same operation.

## Saved workflows, versions and bots — `/v1/workflows` CRUD (One API 2.3.0)

The same rows Studio's sidebar and MCP `plungeai_workflow` show:

| Route | Does |
|---|---|
| `POST /v1/workflows` | save `{name, kind, yaml, description?, folder?, campaign_config?}`; `kind` is `workflow`, `agent`, `bot` or `campaign` (`campaign_config` with `kind: campaign` sets up its ledger and schedule); `201 {workflow, warnings}` |
| `GET /v1/workflows` | list yours `{workflows, total, limit, offset, has_more}` (`kind`, `search`, `folder`, `limit` 1–200 default 50, `offset`) |
| `GET\|PATCH\|DELETE /v1/workflows/{id}` | read (row + live YAML), change `name`/`description`/`yaml`/`kind`/`folder` (`null` clears `description` and `folder`), delete (`204`) |
| `GET\|POST /v1/workflows/{id}/versions` | list versions; save the current YAML as one `{label?}` |
| `POST /v1/workflows/{id}/versions/{versionId}/restore` | make a version live again (recorded as a new version) |

The YAML is validated against the CNL schema and the kind's lint BEFORE anything is written
(`422 invalid_workflow` with `errors` and `warnings`, row unchanged) — also on `PATCH` and
`restore`. A bot also has `…/permissions`, `…/workspace-files`, `…/inbox` and `…/bindings`
(policy, files, inbox, chats) and `/v1/bot-groups`: see the verb table in `SKILL.md` and
`plungeai-api-setup` → `references/bots-and-door.md`. Running a saved bot by id through
`POST /v1/execute` with `agent: <id>@n` is the Agent Call door (`plungeai-api-setup` → `references/bots-and-door.md`).

## 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": "one-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`.

## Which route for what

CNL YAML (inline or saved) runs on `POST /v1/workflows/execute`, `POST /v1/workflows/{id}/execute` and their `-stream`
twins. `POST /v1/execute` is the **Agent Call door**, not a workflow alias: the body is the program (a bare task, a
`workflow:`, or `agent: <saved id>@n`), current and supported — `plungeai-api-setup` → `references/bots-and-door.md`.
The pre-router CNL aliases of the SDK era are deprecated and not in the spec; new and generated code uses the routes above.
