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

# CNL Workflow Reference (engine v6) — PlungeAI

> CNL (Cognitive Natural Language) is PlungeAI's YAML workflow language: you declare tasks and how they compose (parallel, sequential, batch, debate, harness, …), and the engine dispatches them to platform agents with automatic data flow between steps.


<!-- sources-of-truth: orchestration/cnl-engine/schema-types.ts, orchestration/cnl-engine/validate.ts | last-synced: 2026-09-24 -->
CNL (Cognitive Natural Language) is PlungeAI's YAML workflow language: you declare tasks and how they compose (parallel, sequential, batch, debate, harness, …), and the engine dispatches them to platform agents with automatic data flow between steps. This reference is generated for engine v6 and is mechanically drift-checked against the engine source — trust it over older docs.

## Workflow shape (choose ONE)

**Form A — wrapped**
```yaml
workflow:
  name: string              # required
  version: "1.0.0"          # optional
  description: string       # optional
  inputs:                   # optional — named variables, accessible as {key}
    key: value
  tasks: [Task]             # required, ≥1
```

**Form B — flat**
```yaml
name: string
version: "1.0.0"
description: string
tasks: [Task]
input: string               # optional single-input (backwards-compat), accessible as {input}
```

Both forms are accepted everywhere (ad-hoc execution auto-wraps Form B). A `followup:` block may exist in saved workflows but is ignored by the engine.

**Fields every task may carry:** `id` (unique string — needed if later tasks reference its result), `type` (defaults to `task`), `description`, `condition` (CEL expression evaluated BEFORE dispatch; task is skipped when false), `location: cloud | local` (default cloud; `local` routes to the user's connected desktop daemon).

## Task types (10)

### `task` — single agent call
Required: `agent` (kebab-case registry id) plus `prompt` (AI agents) OR `query` (search agents). All three must be strings when present.
Optional: `retry: 0-2` — re-dispatches the same task on failure. **Idempotency warning:** a retry re-fires the FULL agent call; if the agent's side effect (payment, message send, external mutation) completed before the failure was reported, the retry duplicates it. Set retry only on read-only/idempotent agents; leave it 0 for payment-, messaging-, and automation-class agents. Retry applies ONLY to plain `task` — never to `harness`.
Agent-specific fields pass through untouched (e.g. `operationType` for exa-agent, `max_results` for search agents, `provider`/`model` for llm-agent, `digital_twin` — legacy alias `persona` — for skill-agent).
Do NOT put `subtasks` on a `task`. These near-miss keys are warned as mistakes: `input`, `instructions`, `instruction`, `message` (you meant `prompt`) and `goal` (belongs to `harness`).

```yaml
- type: "task"
  id: "research"
  agent: brave-agent
  prompt: "{input} — latest developments"
```

### `parallel` — concurrent execution
Required: `subtasks: [Task]` with ≥1 entry (`tasks:` is an accepted alias for `subtasks:`). ALL subtasks dispatch at once. The block itself must not carry `agent`/`prompt`/`query` — those belong on subtasks.

### `sequential` — serial with data chaining
Required: `subtasks: [Task]` with ≥1 entry (`tasks:` alias accepted). Each task automatically receives the previous task's output, and any prior task's result is addressable as `{data:task_id}`. The block itself must not carry `agent`/`prompt`/`query`.

### `dynamic` — generator + executors
Required: `generator: { agent, prompt? }` (agent string required) and `executors: [{ agent }]` (≥1, each with an agent string). Optional: `max` — positive integer cap on generated items. The generator produces a list; each executor runs per item. No `subtasks` here, and no root-level `agent`/`prompt`/`query`.

```yaml
- type: "dynamic"
  id: "fanout"
  generator: { agent: llm-agent, prompt: "List the top competitors of {input}, one per line" }
  executors:
    - agent: brave-agent
  max: 5
```

### `batch` — per-item pipeline over a list
Required: `tasks: [Task]` (≥1 — the per-item pipeline; batch uses `tasks`, not `subtasks`) and either `items: [...]` (non-empty static list of strings or objects) or `items_from: "task_id"` (a prior task's result provides the list).
Optional: `track: true` — persists per-item status and resumes completed items on re-run (off by default).
Inside the pipeline, `{item}` is the current item (objects are JSON-stringified) and `{item.field}` addresses object fields. Data chains automatically between the pipeline's tasks — write follow-up prompts against the flowing data (e.g. "Summarize the research above for {item}"), not `{data:...}` (per-item task ids are rewritten internally). No `generator`/`executors` here — that's `dynamic`.

### `output` — final delivery
Optional: `format: html | text | markdown`. The source content is auto-resolved from the flowing data; other fields are agent-specific. Use as the last task to shape the deliverable.

### `debate` — multi-perspective reasoning
Required: `debaters: [{ agent, position?, digital_twin? }]` with ≥2 debaters (each needs an agent string; legacy alias `persona`).
Optional: `judge: { agent, prompt? }` (agent string required when present), `rounds: 1-5` (integer, default 1).
No `subtasks`/`generator`/`executors`/`items` on a debate.

### `validate` — consensus / majority / pass-fail (MACRO)
Required: `validators: [{ agent, validation_rule? }]` (≥1, each with an agent string).
Optional: `aggregation: consensus | majority | all_pass | any_pass` (default consensus), `success_criteria: string` (injected into all validator prompts).
This is a macro: the engine expands it into a parallel block of PASS/FAIL validator tasks plus one judge task (`<id>_verdict`) carrying the aggregation rule. The hand-written expanded form is equally valid. No `subtasks`/`generator`/`executors`/`items`.

### `agent_loop` — legacy bounded loop
Exists for backwards compatibility; dispatches to the loop runtime with a recursion depth guard. **Prefer `type: harness`** — it is the supported, validated form of the same idea.

### `harness` — mission-bounded agent run (the flagship)
One goal-driven run of the loop runtime (default `harness-agent`; `agent:` optionally overrides the runtime). Use ONE harness task for open-ended work instead of many small tasks.

Required:
- `goal` — the per-run input, a string (`prompt:` is an accepted authoring alias and is folded into `goal`).
- A mission, in one of three forms:
  1. inline purpose text: `mission: "You are a market analyst. Stay on topic."` (any string with whitespace)
  2. inline object (fields below)
  3. a pre-built agent card reference: `mission_ref: card-id` (alias `pack:`, or a single-token `mission:` value). If BOTH a card ref and an inline object are set, the inline values override the card per key (the engine warns).

Inline mission object fields (all optional except `mission`):

| Field | Meaning |
|---|---|
| `mission` | Standing purpose → system-prompt frame (required in object form) |
| `persona` | Persona / digital-twin id — one voice per run (single string) |
| `skills` | Skill ids, array — injected eagerly (budget-capped), rest loadable in-loop |
| `experts` | Expert/specialist ids, array — labeled context sections |
| `backgrounds` | Always-on ambient context card ids, array |
| `plugins` | Plugin bundle ids, array — expand to skills + mcp + scripts |
| `mcp` | MCP server ids, array — connected in-loop |
| `model` / `provider` | Model/provider override |
| `effort` | `quick` \| `standard` \| `deep` — turn/parallel budget preset |
| `python_executor` | `auto` \| `pyodide` \| `anthropic` \| `gemini` — run_python routing |
| `instructions` | Extra instructions appended after skills |
| `allowed_tools` | Tool fence, array — only these reach the LLM; omitted → fail-closed default fence |
| `allowed_agents` | Agent fence for call_agent: array of ids, or `'all'` |
| `denied_agents` | Array — subtracted from `allowed_agents: all` |
| `success_criteria` | Array of self-checked statements |
| `max_turns` | Loop turn cap, positive number (`max_iterations` is a legacy alias; optional — effort/agent default applies when absent) |
| `max_parallel` | Widest fan-out one delegate call may spawn (cap-and-refuse) |
| `max_tokens` | Per-turn OUTPUT token budget for the loop's model calls (runtime default 16384) |
| `budget_usd_run` | Per-run spend cap in USD — the loop prices its running token cost each turn and stops (`stopReason: 'budget'`) once it exceeds this; absent = no cap |
| `permissions` | Map of tool → `allow` \| `ask` \| `deny` (`ask` pauses the run for human approval) |
| `permission_locks` | Admin-locked permission classes, array — a class listed here forces its `allow` up to `ask` so a bot author's own `allow` cannot silently auto-run it; sourced from the Studio execute boundary, never trusted from raw YAML for enforcement |
| `memory_owner` | Memory namespace override — **do not use; omit entirely** (policy 2026-08-23). Never put a user id in YAML: ownership lives on the workflow row and runtime identity is injected per run; a hardcoded id that isn't the runner's own is rejected by the harness guard anyway. See `orchestration/BOT-CREATION.md`. |
| `local` | `true` = this run's actions execute on the user's own computer via the desktop daemon (cloud brain, local hands; requires a connected local node) |
| `local_agents` | Which local agents may be used (array; absent + `local: true` → all connected) |

Mission fields may also be written FLAT on the task (peers of `goal:`) — the engine folds them into the mission. Validation errors you'd hit: missing/non-string `goal` ("Type 'harness' requires a 'goal' string"), no mission or card ("requires a 'mission' … or a 'mission_ref'"), object mission without a purpose string, non-array `allowed_tools`, non-positive `max_turns`, `effort` outside quick/standard/deep, capability fields (`skills`/`experts`/`backgrounds`/`plugins`/`mcp`) not arrays of strings, `persona` not a single string.

```yaml
- type: "harness"
  id: "market_scan"
  goal: "Map the top 5 vendors for {input}, with pricing and one differentiator each."
  mission:
    mission: "You are a market analyst. Research thoroughly, cite sources, stay on topic."
    effort: "standard"
    max_turns: 12
    success_criteria:
      - "5 vendors named with pricing"
      - "every claim has a source URL"
```

## Interpolation

- `{input}` — the primary input (`input:` at the workflow level, or the input given at execution)
- `{key}` — any named variable declared in `inputs:`
- `{data:task_id}` — result of a prior task by id
- `{item}` / `{item.field}` — current item inside a `batch` pipeline

Data flows automatically between sequential tasks — don't hand-wire results.

## Hard rules

- Agent names MUST match the live registry exactly (kebab-case) — discover them via registry search, never from memory. Do NOT use `brave-search` — use `brave-agent`. Execution refuses unknown or inactive agent ids.
- Never use `$variable`, `depends_on`, `outputs:`, or `parallel: true` — these don't exist.
- `parallel` / `sequential` / `batch` / `dynamic` / `debate` / `validate` blocks MUST NOT carry an `agent` field directly — agents go on the inner tasks.
- `type: task` reads `prompt:` (or `query:`) — not `input`, `instructions`, `instruction`, or `message`. `goal` belongs to `type: harness` only. `subtasks` never belongs on a `task`.
- Every `id` must be unique within its scope.
- Long prompts use YAML block scalars (`prompt: |`) — never hard-wrap a value; quote values containing colons.
- `digital_twin:` (legacy alias `persona:`) only works on `skill-agent` and on `debater`/`validator`/`judge` entries.
- Nesting is unlimited: parallel inside sequential inside parallel, etc. Keep it under ~4 levels for sanity.
- Scheduling is configured in the platform (Studio), not in YAML — there is no `schedule:` field.

## Validation errors you may see

| Message (verbatim) | Fix |
|---|---|
| `Workflow name is required and must be a string` | Add a top-level `name:` |
| `Workflow must have at least one task` | Add a `tasks:` list |
| `Invalid task type: X. Must be 'task', 'parallel', 'sequential', 'dynamic', 'batch', 'output', 'debate', 'validate', 'agent_loop', or 'harness'` | Use one of the 10 types (or omit `type:` for a plain task) |
| `Type 'harness' requires a 'goal' string` | Add `goal:` (or `prompt:` alias) to the harness task |
| `Type 'harness' requires a 'mission' (purpose text or inline object) or a 'mission_ref' (pre-built agent card id)` | Add a mission — text, object, or card ref |
| `Type 'parallel' requires a subtasks array` | Put the inner tasks under `subtasks:` |
| `Type 'dynamic' requires a generator object with at least an 'agent' field` | Add `generator: { agent: ... }` |
| `Type 'batch' requires either 'items' (array) or 'items_from' (task ID string)` | Provide the item source |
| `Type 'debate' requires at least 2 debaters` | Add a second debater |
| `Rounds must be a positive integer between 1 and 5` | Fix `rounds:` |
| `Aggregation must be one of: consensus, majority, all_pass, any_pass` | Fix `aggregation:` |
| `Type 'task' does not read 'input' — did you mean 'prompt'?` (warning) | Rename the field to `prompt:` |
| `Type 'task' should not have subtasks (use 'parallel' or 'sequential')` (warning) | Change the type or move subtasks |
| `Max must be a positive integer` | Fix `max:` on the dynamic block |
