---
name: workflows
description: "Structured executable workflows. Named compositions of MCP tool calls with validated references, dependency ordering, failure handling, execution history, and schedule integration."
tools:
  - workflow-create
  - workflow-list
  - workflow-get
  - workflow-update
  - workflow-delete
  - workflow-validate
  - workflow-execute
  - workflow-runs
metadata: {"platform":{"always":false,"embed":[],"pluginKey":"workflows"}}
---

# Workflows

Workflows are persistent, named compositions of executable steps that the user creates and the engine executes. Steps can chain MCP tool calls and LLM reasoning into composable pipelines. Steps are validated at creation time — a workflow with unmet dependencies cannot be activated. The user manages them conversationally.

## Creating Workflows

When the user describes a recurring process, conditional action, or reusable sequence, create a workflow. Confirm the name, description, and steps with the user before committing.

### Step types

**Tool steps** (`type: "tool"`) — direct MCP tool calls:
- **plugin** — the plugin that provides the tool (e.g. `memory`, `scheduling`, `telegram`)
- **tool** — the tool name within that plugin (e.g. `contact-create`, `message`)
- **params** — parameter object with `{{outputKey.field}}` templates for values from prior steps
- **timeout** — execution timeout in seconds (default: 30, max: 120)

**LLM steps** (`type: "llm"`) — LLM reasoning via Claude Code subprocess:
- **prompt** — prompt template sent to the model. Use `{{outputKey.field}}` for data from prior steps. Instruct the model to return JSON for structured output that downstream steps can reference.
- **model** — Anthropic model ID override for this step (optional). Omit to use the account's adminModel (recommended). Only set when the step requires a specific model different from the account default.
- **maxTokens** — maximum output tokens (optional, default: 4096)
- **timeout** — execution timeout in seconds (default: 60, max: 300). LLM calls are slower than tool calls. For agentic steps, increase to 120-300.
- **mcpServers** — optional MCP server bindings for agentic steps. When present, the LLM gets multi-turn tool access. Each key is a server name, each value is `{command, args?, env?}`. See "Agentic LLM Steps" below.
- **maxTurns** — maximum agent turns (default: 1 without mcpServers, 10 with mcpServers, max 50). Controls how many reasoning+tool-use cycles the LLM can perform.
- **fallbackMode** — `"fail"` (default) or `"raw"`. Controls behaviour when the LLM is unavailable (Claude Code missing, OAuth dead, Anthropic outage, network partition). See "Degradation" below.
- **fallbackTemplate** — custom fallback text when `fallbackMode: "raw"`. Supports `{{_error}}`, `{{_priorOutputs}}`, and any `{{stepKey.field}}` reference from prior steps. Max 4 KB. Defaults to a standard banner + prior-output dump.

LLM steps spawn Claude Code as a subprocess (`claude --print`), which handles OAuth authentication internally. They never use the Anthropic API key directly — that path is exclusively for the public agent.

### Agentic LLM steps

An LLM step becomes agentic when `mcpServers` is declared. Instead of a one-shot prompt→response, the LLM can call tools provided by the bound MCP servers across multiple turns — navigating a website, filling a form based on page content, or any task where the next action depends on observing the result of the previous one.

**When to use agentic steps:** When the workflow step requires adaptive interaction — the LLM must observe, decide, and act rather than just reason over static input. If a step can be expressed as a single prompt with no tool access, use a regular LLM step instead.

**MCP server config format:** Same shape as Claude Code's `.mcp.json`:
```json
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["-y", "@playwright/mcp@latest", "--cdp-endpoint", "http://127.0.0.1:9222"]
    }
  }
}
```

**Constraints enforced at creation time:**
- `mcpServers` requires `maxTurns >= 2` (1 turn = no tool loop)
- Server commands must be bare executable names — no paths, no path traversal
- Certain environment variables (`LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, etc.) are blocked for security
- Server command binaries must exist in PATH (checked via `which`)

**Execution:** The engine writes a temporary MCP config file, spawns Claude Code with `--mcp-config <path> --strict-mcp-config --max-turns <N>`. The `--strict-mcp-config` flag ensures only the declared servers are available — no config bleed from the admin session. The temp file is cleaned up after execution regardless of outcome.

**Degradation:** Agentic steps use the same `fallbackMode` mechanism as regular LLM steps. MCP server spawn failures manifest as Claude Code exit errors and trigger the existing degradation path.

### Capability envelope

The executor can access these capabilities through agentic steps:

| Capability | MCP Server | Infrastructure |
|---|---|---|
| Browser automation | `@playwright/mcp` | Chromium with CDP on port 9222 (started by `vnc.sh`, always running) |
| Any MCP-compatible tool | Declared in step's `mcpServers` | Command must be in PATH on the device |

The executor cannot:
- Access MCP servers not explicitly declared in the step's `mcpServers`
- Maintain browser sessions across steps (each step gets a fresh MCP connection; Chromium state persists on the always-running instance)
- Run arbitrary binaries (command validation at creation time)

### Common step fields

- **label** — human-readable description of what this step does
- **dependsOn** — step positions (as string indexes) that must complete first
- **onFailure** — `abort` (default), `skip`, or `retry`
- **retryLimit** — max retries when onFailure is `retry` (max 3)
- **outputKey** — name under which this step's output is stored for use in subsequent step templates

### Capability validation

At creation time, every step's referenced plugin directory and built MCP server are checked. If any capability is missing, the workflow's status is set to `draft` with specific error messages listing which plugins or tools are unavailable. Only `active` workflows can be executed.

When the missing capability is later installed, use `workflow-validate` to re-check and promote the workflow to `active`.

### Status model

| Status | Meaning |
|--------|---------|
| `active` | All capabilities validated, ready to execute |
| `draft` | Missing capabilities — cannot execute until resolved |
| `paused` | Manually deactivated — retained but not suggested or executable |

## Discovery

Active workflows are discoverable via `workflow-list` and via `memory-search` (the vector index on the Workflow node surfaces relevant workflows alongside other graph knowledge). When the user's request matches an existing workflow, offer to execute it.

## Execution

Use `workflow-execute` to run a workflow. The engine:
1. Loads the workflow and its steps
2. Resolves step dependencies into execution order (topological sort)
3. For each step: resolves `{{template.variables}}` from context and prior step outputs, spawns the target plugin's MCP server, and calls the specified tool
4. Records every step's outcome as a StepResult node
5. Creates a WorkflowRun node with the overall status

### Template variables

Steps receive data from prior steps via named output bindings. If step 1 has `outputKey: "newContact"` and returns `{"contactId": "abc-123"}`, step 2 can reference `{{newContact.contactId}}` in its params.

The execution context can also be seeded with runtime variables via the `context` parameter on `workflow-execute`. These are available alongside step outputs.

Unresolved template variables cause the step to fail with a specific error naming the missing variable.

### Failure handling

Each step declares an `onFailure` policy:
- **abort** (default) — stop the workflow, mark the run as `failed`
- **skip** — mark this step as `skipped`, continue to the next step
- **retry** — retry up to `retryLimit` times before applying abort behaviour

### Run status

| Status | Meaning |
|--------|---------|
| `completed` | Every step succeeded |
| `partial` | Finished, but one or more steps were skipped or degraded |
| `failed` | A step failed with abort policy, execution stopped |

### Degradation (LLM step resilience)

Scheduled workflows with LLM reasoning steps must not drop silently when the LLM is unavailable. The workflow engine provides two layers of defence:

**1. Pre-flight health probe.** Before executing any step in a workflow that contains at least one LLM step, the engine spawns a trivial `claude --print "pong"` probe (10-second timeout). The result is cached for the run. This catches upstream outages (missing CLI, dead OAuth token, Anthropic unreachable, no model configured) before the engine burns any tool steps on a workflow that can't complete.

> **Known limitation — heterogeneous model workflows.** The probe validates only the first LLM step's resolved model. A workflow that uses different explicit models on different LLM steps still gets correct per-step behaviour via the spawn-failure convergence arm (a real failure on any LLM step still triggers the step's `fallbackMode`), but the probe's upfront savings only apply to the first model. If you have workflows that mix e.g. Haiku and Opus across LLM steps and you want full-run pre-flight coverage, raise it as a follow-up — current implementation is correct for single-model and default-model workflows.

**2. Per-step `fallbackMode`.** Each LLM step declares how to react when the LLM is unavailable:

- **`"fail"` (default).** The step fails per the standard `onFailure` policy (`abort`/`skip`/`retry`). When the pre-flight probe already confirmed the LLM is unavailable, retries are short-circuited — a retry of a call the engine knows will fail adds latency with no value.

- **`"raw"`.** The step emits a degraded output via the fallback template, its `StepResult.status` is marked `"degraded"`, and execution continues with the degraded output bound to the step's `outputKey`. Downstream steps that reference `{{stepKey._raw}}` consume the fallback naturally. The workflow's final `WorkflowRun.status` becomes `"partial"` whenever any step is degraded.

Because degradation converges at the fallback-render path for both probe failures and spawn-time failures, a workflow with `fallbackMode: "raw"` always gets a visible, deliverable output — even if the LLM's first call succeeds and the second hits rate limiting mid-run.

**Rules enforced at creation time:**

- `fallbackMode` is only valid on `type: "llm"` steps. Setting it on a tool step is rejected.
- `fallbackTemplate` requires `fallbackMode: "raw"`. Silent no-ops are a hazard.
- `fallbackMode: "raw"` requires `outputKey`. Without it, the degraded output has nowhere to go and downstream delivery steps can't consume the fallback.

**Default fallback template:**

```
⚠️ AI reasoning is temporarily unavailable ({{_error}}).

The workflow has fallen back to raw data.

{{_priorOutputs}}
```

Where `{{_error}}` is the probe or spawn error summary (e.g. `"timed out after 10000ms"`, `"spawn failed: ENOENT"`), and `{{_priorOutputs}}` is a serialised dump of all prior step outputs, capped at 8 KB. Authors can supply a custom `fallbackTemplate` to control the delivered message format.

**Logging.** Every probe outcome and degradation event is logged to `claude-agent-stderr` via these prefixes (grep via `logs-read.sh`):

- `[llm-probe] probing: model=<model>`
- `[llm-probe] ok: latencyMs=<n>`
- `[llm-probe] FAILED: <error> latencyMs=<n>`
- `[workflow-execute] step N DEGRADED: <error> (fallbackMode=raw)`

**Step status values:**

| Status | Meaning |
|---|---|
| `completed` | Step ran normally and produced output |
| `degraded` | LLM step fell back to raw output because the LLM was unavailable |
| `skipped` | Step failed but `onFailure: "skip"` kept execution going |
| `failed` | Step failed and `onFailure: "abort"` stopped execution |

## Execution History

Use `workflow-runs` to query execution history. Returns run status, timing, trigger type, and per-step outcome counts. The admin can ask "show me recent workflow runs" and get structured execution history.

## Schedule Integration

To trigger a workflow on a schedule, create an Event with the scheduling plugin using action dispatch:

```
action: { plugin: "workflows", tool: "workflow-execute", args: { workflowId: "..." } }
```

The existing `check-due-events` heartbeat dispatches the workflow execution at the scheduled time. The WorkflowRun captures `trigger: "schedule"`.

## Managing Workflows

- **Pause**: Set status to `paused` via `workflow-update`
- **Resume**: Set status back to `active` (only if capabilities are still valid)
- **Edit steps**: Provide the complete new step list via `workflow-update` — replaces all existing steps and re-validates capabilities
- **Re-validate**: Use `workflow-validate` after installing new plugins to check if draft workflows can now be activated
- **Delete**: Remove permanently via `workflow-delete` — cascades to steps, runs, and step results. Confirm with the user first.

## Workflow Templates

### Public Chat Review

A digest workflow that reviews recent public chat conversations and persists a structured summary for the admin's session-start briefing.

**Steps:**

| # | Type | What it does |
|---|------|-------------|
| 1 | tool | `memory:conversation-list` — fetch recent public conversations with messages inline |
| 2 | llm | Review transcripts, extract signals (feedback, complaints, feature requests, competitive mentions, unanswered questions), classify by priority |
| 3 | tool | `memory:memory-write` — persist the digest as a CreativeWork node |

**Example step definitions:**

```json
[
  {
    "type": "tool",
    "label": "Fetch recent public conversations",
    "plugin": "memory",
    "tool": "conversation-list",
    "params": {
      "agentType": "public",
      "since": "{{context.since}}",
      "includeMessages": true,
      "limit": 20
    },
    "outputKey": "conversations",
    "onFailure": "abort"
  },
  {
    "type": "llm",
    "label": "Review conversations and extract signals",
    "prompt": "Review these public chat conversations and produce a structured digest.\n\nConversations:\n{{conversations._raw}}\n\nExtract and classify:\n- Product feedback (positive or negative)\n- Feature requests\n- Complaints or frustrations\n- Competitive mentions (other products named)\n- Unanswered questions (visitor asked something the agent couldn't answer)\n- High-intent prospects (visitors showing buying signals)\n\nFor each item, include: the signal type, a one-line summary, priority (high/medium/low), and which conversation it came from.\n\nIf no notable signals were found, say so — do not fabricate items.\n\nReturn the digest as a readable summary, not raw JSON.",
    "maxTokens": 4096,
    "timeout": 120,
    "outputKey": "digest",
    "dependsOn": ["0"],
    "onFailure": "abort",
    "fallbackMode": "raw"
  },
  {
    "type": "tool",
    "label": "Persist digest to graph",
    "plugin": "memory",
    "tool": "memory-write",
    "params": {
      "labels": ["CreativeWork"],
      "properties": {
        "title": "Chat Review {{context.date}}",
        "abstract": "{{digest._raw}}"
      },
      "scope": "admin"
    },
    "dependsOn": ["1"],
    "onFailure": "abort"
  }
]
```

**Runtime context:** The `context.since` and `context.date` variables are supplied via the `context` parameter when executing. For a daily schedule, set `since` to the previous day's ISO 8601 timestamp and `date` to today's date (e.g. `2026-04-01`).

**Schedule:** To run daily, create a scheduled Event with action dispatch pointing to this workflow's `workflowId`. See "Schedule Integration" above.

**Session context:** The persisted digest appears in the admin agent's session-start briefing automatically — `loadSessionContext()` queries recent `Chat Review` CreativeWork nodes and includes them in `<previous-context>`.

## Tools

- `workflow-create` — Create a workflow with name, description, and structured steps
- `workflow-list` — List workflows with status and step counts
- `workflow-get` — Fetch a workflow with full step details
- `workflow-update` — Update name, description, steps (full replacement), or status
- `workflow-delete` — Permanently remove a workflow and all history
- `workflow-validate` — Re-check step capabilities and update status
- `workflow-execute` — Execute a workflow with optional runtime context
- `workflow-runs` — Query execution history with optional filters
