> ## 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

> The workflows plane runs a CNL workflow — one or many agent tasks orchestrated by the engine — and hands back a pointer to the result. Reach it with an ozk_ key (Authentication) at POST /v1/workflows/execute (inline) or POST /v1/workflows/:id/execute (saved). Base URL is always …


The workflows plane runs a **CNL** workflow — one or many agent tasks orchestrated by the engine —
and hands back a pointer to the result. Reach it with an `ozk_` key
([Authentication](/one-api/authentication)) at `POST /v1/workflows/execute` (inline) or
`POST /v1/workflows/:id/execute` (saved). Base URL is always `https://api.plungeai.com`.
Source: `orchestration/api-gateway/routes/workflows.ts`, engine `orchestration/cnl-engine/`.

Where the [Agents plane](/one-api/agents) runs one task, the workflows plane runs a whole DAG:
parallel fan-out, sequential stages, data flow between tasks, and bounded autonomous
`type: harness` missions.

## 10.1 CNL in one page

**CNL** (the YAML workflow language) has a `workflow` with a `name` and a `tasks[]` array. Each task
is either a leaf task or a compound block; a top-level `input:` (sibling of `workflow:`) seeds the
run. Field names are from `orchestration/cnl-engine/context.ts`, `schema-types.ts` and the real
files in `workflows/*.yaml`.

| Field | On | Meaning |
|---|---|---|
| `name` | `workflow` | workflow name |
| `tasks[]` | `workflow` | ordered list of tasks/blocks |
| `type` | task | `task` \| `parallel` \| `sequential` \| `harness` \| `dynamic` \| `batch` \| `debate` (the `TaskType` enum also has the legacy `agent_loop` and internal `output` / `validate`) |
| `id` | task | task id — the key its output is stored under |
| `agent` | leaf task | the agent id to run (must be `status:active`, or the run is fenced 404/403) |
| `prompt` | leaf task | the task input; search agents take the **literal** query |
| `operation` | leaf task | a named operation for a structured tool-agent (e.g. `serpapi-agent` `operation: amazon`) |
| *(agent params)* | leaf task | any extra card parameters as task fields (e.g. `num: 15`, `searchType: shopping`) |
| `subtasks[]` | compound block | children of a `parallel` / `sequential` / … block |

**Data flow.** There is no `depends_on` — a reference *is* the dependency:

- `{input}` injects the workflow-level input.
- `{data:<task_id>}` injects a prior task's stored output into an LLM prompt. A `sequential` block
  runs its `subtasks` in order; a `parallel` block runs them at once (`Promise.all`, no width cap —
  see the engine README "Parallelism"). Reference an upstream task and the engine orders around it.

A real 3-task workflow — a parallel search fan-out feeding one LLM synthesis (adapted from
`workflows/amazon-price-comparison.yaml`):

```yaml
workflow:
  name: price-check
  tasks:
    - type: parallel
      id: search_phase
      subtasks:
        - type: task
          id: amazon_search
          agent: serpapi-agent
          operation: amazon
          prompt: "{input}"
          num: 15
        - type: task
          id: shopping_search
          agent: serper-agent
          searchType: shopping
          prompt: "{input}"
    - type: task
      id: recommendation
      agent: one-agent
      prompt: |
        For the search "{input}", pick the top 3 products.
        Amazon results: {data:amazon_search}
        Shopping results: {data:shopping_search}
        Use only links that appear verbatim in the data.
input: "wireless headphones"
```

## 10.2 Inline execute

`POST /v1/workflows/execute` runs one body in three sizes (`routes/workflows.ts`): a bare agent call,
a `workflow:` document, or the JSON twin of either. `Content-Type` decides YAML or JSON; both produce
the same object.

**A bare agent call** — `agent:` plus the agent's own fields, exactly as its README documents them.
The door lifts it to a one-task workflow (`{ workflow: { name: <agent>, tasks: [{ id: "call", type: "task", agent, …fields }] } }`;
a pre-built agent becomes a `harness` task with the `prompt` as its goal). Top-level `format:`, `input:`
and `inputs:` stay the wire's, as in every other body; `stream:`, `sync:`, `dry_run:` and `metadata:` are
wire keys too and never reach the agent.

```bash
curl -s -X POST "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF'
agent: calculator-tool-agent
expression: "7*6"
format: markdown
EOF
```

The JSON twin is `{"agent":"calculator-tool-agent","expression":"7*6"}` → the JSON envelope below with
the result in `content`. Two refusals guard the lift: `agent:` next to `workflow:`/`tasks:` →
`400 ambiguous_body` (send one); a field named like an engine task key (`id`, `type`, `agent`,
`depends_on`, `mission_ref`) → `400 reserved_field` naming the fields. An unknown agent id →
`404 unknown_agent` here (on `POST /v1/execute`, [16.9](/one-api/bots#169-the-one-door), the same body is
`404 agent_not_found`). Every execute door refuses a body it cannot read, on the negotiated wire:
malformed JSON → `400 invalid_json` (with the parser's position), malformed YAML → `400 invalid_yaml`,
a scalar or list root → `400 invalid_body`; an empty body is *missing* (`missing_workflow`,
`missing_prompt`, `invalid_params`), not malformed.

**A `params:` wrapper.** On the one door, `POST /v1/execute` ([16.9](/one-api/bots#169-the-one-door)), the fields may also
come wrapped in `params: {…}`, the shape the tools plane uses: the door unwraps them into the agent's fields, and a
key inside `params` wins over a bare field of the same name (`routes/execute.ts`). The wrapper carries agent fields
only. `params` naming `agent`, `workflow`, `tasks`, `on_behalf_of` or a wire key (`format`, `input`, `inputs`,
`user_context`, `stream`, `sync`, `dry_run`, `metadata`) is `400 invalid_body`, because those keys stay at the root.
A `params` that is not an object stays an ordinary field. Live-verified 2026-10-14:

```bash
curl -s -X POST "https://api.plungeai.com/v1/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @- <<'EOF'
agent: calculator-tool-agent
params:
  expression: "7*6"
format: markdown
EOF
# → # Calculator: 7*6 … ## 2. Result 42
```

**A workflow** — the full `workflow:` document as a `text/yaml` body, or JSON
`{ "workflow": <object | YAML string>, "input"?: any, "inputs"?: any }`; a JSON body with top-level
`name`/`tasks` auto-nests under `workflow`. A body with none of `workflow`, `agent` or `tasks` →
`400 missing_workflow`.

Every execute body — inline, saved (10.4) and both `execute-stream` variants — also takes:

| Field | Type | Meaning |
|---|---|---|
| `user_context` | object · optional · default: derived from your request's network location (time zone, approximate place) | the caller's time zone, locale and location (`{time_zone, locale, place, latitude, longitude, accuracy_m, source}`, the `UserContext` schema); the agent uses it for local time and as the default schedule time zone. A wire key — it never reaches a bare agent call's fields. A raw YAML workflow body always takes the network default |

Success carries the pointer and, once the sync run has stored it, the final task's `content`: `{ success: true, status, workflow_id, final_task_id, request_id, content? }`. `status` is `"completed"`, or `"incomplete"` when the final task stopped at its turn budget before `task_complete` — the stored result is then partial (the results route returns it with a `[run stopped at the N-turn budget …]` note), and the execution row carries `outcome: incomplete`.
The result route (10.5) reads the same content back later, on any wire.

```bash
curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"workflow":{"name":"doc-demo","tasks":[{"type":"task","id":"t1","agent":"one-agent","prompt":"Reply with only the word: pong"}]},"input":"go"}'
```

The same run as a raw YAML body (the response mirrors the request Content-Type → YAML, see 10.6):

```bash
printf 'workflow:\n  name: doc-demo-yaml\n  tasks:\n    - type: task\n      id: t1\n      agent: one-agent\n      prompt: "Reply with only the word: pong"\n' \
| curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @-
```

## 10.3 Streaming — `execute-stream`

`POST /v1/workflows/execute-stream` (same body shapes) forces the engine to stream and relays every
engine SSE frame **verbatim**, plus a relay-injected `event: token` per provider delta
(`lib/stream-relay.ts` `workflowsEncoder`). This is the named-event stream (distinct from the
OpenAI-shaped agent stream in [Streaming](/one-api/streaming)). Event vocabulary (`context.ts`
`CNLEventType`):

| Event | When | Payload (`data:`) |
|---|---|---|
| `request_received` | run accepted | request metadata |
| `workflow_loaded` | workflow parsed | workflow name/id |
| `workflow_started` | execution begins | workflow id |
| `task_dispatched` | a task is sent to its agent | `{task_id, agent, streaming?}` |
| `streaming_started` | a task's provider stream opened | `{task_id, stream_url}` |
| `token` | a provider delta (relay-injected) | `{task_id, delta}` |
| `streaming_completed` | a task's provider stream closed | `{task_id}` |
| `task_completed` | a task finished | `{task_id, …}` |
| `task_error` | a task failed | `{task_id, error}` |
| `output_delivered` | a task output was stored | `{task_id}` |
| `parallel_start` / `parallel_complete` | a `parallel` block boundary | block id |
| `sequential_start` / `sequential_complete` | a `sequential` block boundary | block id |
| `batch_*` / `debate_*` / `dynamic_*` | block-specific progress | block payload |
| `condition_evaluated` | a task `condition` was resolved | `{result}` |
| `heartbeat` | idle keepalive | — |
| `workflow_completed` | **outcome (success)** | `{final_task_id, execution_summary}` |
| `workflow_error` | **outcome (failure)** | `{error}` |
| `workflow_result` | **last frame**, after the outcome | `{final_task_id, …}` |

The outcome frame is `workflow_completed` (or `workflow_error`); `workflow_result` is always the last
frame, on success and on failure — a client that closes on the outcome frame misses it. Live-verified
2026-10-04 the stream carried: `request_received`, `workflow_loaded`, `workflow_started`,
`task_dispatched`, `task_completed`, `workflow_completed`, `workflow_result`.

```bash
printf 'workflow:\n  name: stream-demo\n  tasks:\n    - type: task\n      id: t1\n      agent: one-agent\n      prompt: "Reply with only the word: pong"\n' \
| curl -sN "https://api.plungeai.com/v1/workflows/execute-stream" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: text/yaml' --data-binary @-
```

## 10.4 Saved workflows

A saved workflow has an id in the engine's KV (`workflow:{user}:{id}`). Create one in **Studio** or
over the [MCP plane](/one-api/mcp) (`plungeai_build_workflow` from a goal, or `plungeai_workflow`
`action: create`). Then run it by id:

```bash
# not run here — requires a saved workflow id you own
curl -s "https://api.plungeai.com/v1/workflows/<workflow_id>/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"input":"quarterly numbers"}'
```

Body: `{ "input"?: any, "inputs"?: any }`. An unknown id → `404 workflow_not_found`. A streaming
variant `POST /v1/workflows/:id/execute-stream` exists with the same body and the 10.3 event
vocabulary.

## 10.5 Results route

`GET /v1/workflows/results/:workflowId/:taskId` → `{ content, content_type, workflow_id, task_id }`,
with the same reconciliation as the agents plane: `404 not_ready` (in flight), `409
execution_failed`, `422 empty_completion`.

```bash
RESP=$(curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"workflow":{"name":"res-demo","tasks":[{"type":"task","id":"t1","agent":"one-agent","prompt":"Reply with only the word: pong"}]}}')
WF=$(printf '%s' "$RESP" | sed -n 's/.*"workflow_id":"\([^"]*\)".*/\1/p')
FT=$(printf '%s' "$RESP" | sed -n 's/.*"final_task_id":"\([^"]*\)".*/\1/p')
for i in 1 2 3 4 5 6 7 8; do
  R=$(curl -s "https://api.plungeai.com/v1/workflows/results/$WF/$FT" \
    -H "Authorization: Bearer $PLUNGE_API_KEY")
  case "$R" in *'"content"'*) echo "$R"; break;; *) sleep 2;; esac
done
```

## 10.6 Response formats

One negotiated payload, four wires (`lib/format.ts`): `json` (default), `yaml`, `markdown`, `text`.
Precedence: `body.format` / `?format=` → `Accept` header → the request Content-Type mirror
(`text/yaml` → `yaml`, else `json`). An explicit unknown `format` value → `400 invalid_format`; an
unknown `Accept` type falls through to the mirror. `markdown` renders through the same renderer the
[MCP plane](/one-api/mcp) uses; `text` is the bare result content.

On the YAML wire the envelope fields travel **inside the document**: top-level `format:`, `input:` and
`inputs:` next to `workflow:` in a `text/yaml` body (inline execute and execute-stream; the saved-workflow
execute takes the same three in a YAML or JSON body) mean exactly what the JSON frame's fields mean —
`format` has body precedence, `input` is substituted for `{input}`, `inputs` is the named map — and are
stripped before the engine sees the workflow (`routes/_util.ts` `splitRequestEnvelope`).
Every wire carries the final task's content once the **sync** execute routes have stored it: `markdown`
and `text` answer with it as the body (the pointer keys still render when it is not stored yet); `json`
and `yaml` add it as `content` next to the unchanged pointer fields.

## 10.7 The `type: harness` task

A `type: harness` task with `agent: one-agent` binds an inline **mission** onto one-agent's loop mode — a
bounded ReAct agent with a tool fence, iteration cap, long-term memory, and capability injection. All fields and
a full example are in [Skills, plugins & capability injection](/one-api/skills-plugins-capabilities);
in short:

```yaml
workflow:
  name: research-brief
  tasks:
    - type: harness
      id: brief
      agent: one-agent
      goal: "One-paragraph brief on the latest Anthropic model; every claim a source URL."
      mission: "You are a research analyst. Produce sourced, factual briefs."
      allowed_tools: [web_search, web_fetch, task_complete]
      success_criteria:
        - "Every factual claim is followed by a source URL."
      max_iterations: 8
      skills: [research]
```

## 10.8 Legacy aliases, limits & headers

**Deprecated aliases** (same auth, same workflow-plane handlers; prefer `/v1/workflows/*`):
`POST /v1/cnl/execute`, `POST /v1/cnl/execute-stream` (`api-gateway.ts`).

**`POST /v1/execute` is not an alias.** It is the Agent Call door, level 3 of the API
([The one door](/one-api/bots#169-the-one-door)). It still takes every inline body of 10.2 (a `workflow:`, root
`tasks:`, a bare building-block agent) and adds `agent: <saved id>[@n]`, `dry_run` and `stream`. An
unknown `agent:` there is `404 agent_not_found`; a task inside a `workflow:` body that names an unknown
agent is still `404 unknown_agent` (below). The level 2 routes of this chapter are unchanged.

**Limits** (see [Errors, limits & headers](/one-api/errors-limits) for the full table):

- Body cap: `MAX_REQUEST_SIZE`, default **1 MiB** → `413 payload_too_large`.
- Replayed `x-trace-id` UUID → `409 duplicate_execution_id` (send a fresh UUID or omit the header).
- `parallel` fan-out has no engine-side width cap (`Promise.all`); throughput is bounded by the
  downstream agent service, not the engine.
- `400 invalid_workflow` for a malformed CNL document; `404 unknown_agent` / `403 agent_not_active`
  when a task names an unknown or non-active agent (fenced before the run opens).
- Every response carries `x-request-id` (= the trace id); async/YAML/markdown responses also carry
  `X-Execution-Id`. See [Traces & observability](/one-api/traces-observability).

## 10.9 Cancelling a run

`POST /v1/workflows/executions/:id/cancel` stops a running execution. Cancellation is
**cooperative** (`core/agent-core/cancel.ts`, `routes/workflows.ts`): it writes a marker in
SharedMemory that the engine reads between tasks and the harness loop reads at the start of each
turn and before each tool batch, then stamps the row `cancelled`. An in-flight model or tool call
is not interrupted — the run stops at the next boundary, so cancellation may lag one turn.

Because `POST /v1/workflows/execute` is synchronous (it returns when the run is done), cancel is for
runs you can reach from another request. Pass your own `x-trace-id` (a UUID) on the execute call —
it becomes the execution id — and use that id here from a second client.

| Response | Meaning |
|---|---|
| `200 { success: true, cancelled: <id>, markers: [<id>, …] }` | Marker written under each run id (row id + the engine's cln id when it differs); row stamped `cancelled`. |
| `404 not_found` | No such execution for this key. A run owned by another key answers 404 too — the API never confirms a foreign run exists. |
| `409 not_running` | The execution already finished, failed, or was cancelled. |

This example mutates state (it cancels a run), so it is not auto-verified:

```bash
# Start a run with a trace id you choose, then cancel it from elsewhere with that same id.
TRACE=$(uuidgen)
curl -s "https://api.plungeai.com/v1/workflows/execute" \
  -H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
  -H "x-trace-id: $TRACE" \
  -d '{"workflow":{"name":"cancel-demo","tasks":[{"type":"task","id":"t1","agent":"one-agent","prompt":"Count slowly to a thousand."}]}}' &

curl -s -X POST "https://api.plungeai.com/v1/workflows/executions/$TRACE/cancel" \
  -H "Authorization: Bearer $PLUNGE_API_KEY"
# → {"success":true,"cancelled":"<TRACE>","markers":["<TRACE>"]}
```

Over MCP the same action is `plungeai_executions` with `action: "cancel"` and the `execution_id`
(`orchestration/mcp-gateway/tools.ts`): a foreign or unknown id answers `Execution not found.`, a
finished run answers that it is not running, and a running one is stopped at its next boundary.
