---
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:
  - name: workflow-create
    publicAllowlist: false
    adminAllowlist: false
    riskClass: write_local
  - name: workflow-list
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: workflow-get
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: workflow-update
    publicAllowlist: false
    adminAllowlist: false
    riskClass: write_local
  - name: workflow-delete
    publicAllowlist: false
    adminAllowlist: false
    riskClass: write_local
  - name: workflow-validate
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: workflow-execute
    publicAllowlist: false
    adminAllowlist: false
    riskClass: exec
  - name: workflow-runs
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
metadata: {"platform":{"always":false,"embed":[],"pluginKey":"workflows"}}
mcp:
  command: node
  args:
    - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
    - ${PLATFORM_ROOT}/plugins/workflows/mcp/dist/index.js
  env:
    MCP_SPAWN_TEE_NAME: workflows
    LOG_DIR: ${LOG_DIR}
    PLATFORM_ROOT: ${PLATFORM_ROOT}
    ACCOUNT_ID: ${ACCOUNT_ID}
    SESSION_ID: ${SESSION_ID}
    AGENT_SLUG: ${AGENT_SLUG}
mcp-manifest: auto
---

# Workflows

> **Loading note:** `platform.always:false` in the frontmatter above refers to **prose embedding** — this file's contents are not auto-injected into every agent system prompt. It does **not** refer to MCP server loading. The workflows MCP server is always loaded in admin sessions (registered in `getMcpServers()` in `claude-agent.ts` alongside `tasks` / `scheduling` / `email`). The same binary is spawned ad-hoc by the scheduling dispatcher (`check-due-events`) for `workflow-execute` calls via `.mcp.json`; that dispatcher is armed by the installer's once-a-minute heartbeat cron (see `maxy-code-prd.md` §Scheduled tasks).

Workflows are persistent, named compositions of MCP tool calls that the user creates and the engine executes. Each step is a direct invocation of a plugin tool — the workflow runner is an MCP-tool orchestrator, never an LLM caller. Plugins that need LLM reasoning own their own SDK call inside a tool. 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 shape

Every step is `type: "tool"`:

- **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: 300)
- **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

### Declared inputs

A workflow can declare the inputs it needs before it can run. This is the front half of gather-then-execute: the contract states what the workflow requires, `workflow-manager` gathers those values from the operator, and `workflow-execute` refuses to run any step until every required input is present. Because each variable choice is resolved with the operator at collection time, the run context is flat and concrete and the linear step DAG is all execution needs.

Each entry in `inputs` has:

- **key** — the context key this input provides, referenced as `{{key}}` in step params
- **prompt** — the operator-facing question that gathers the value
- **type** — `string`, `number`, `boolean`, or `object`
- **required** — when true, execution is refused until the input is present
- **default** — optional value seeded when the input is absent; a required input with a default is satisfied without asking

The contract is a full-replacement field on both `workflow-create` and `workflow-update`, stored like step params. A workflow that declares no inputs runs exactly as before — the gate passes on an empty contract.

A scalar input referenced as `{{key}}` resolves to its value (for example `{{customerName}}` becomes `Cardinal Roofing`), not a wrapper object.

### 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, its steps, and its declared input contract
2. Pre-flight gate: applies any declared defaults, then checks that every required input is present. If any are missing it returns `status: "incomplete"` with the missing keys, runs zero steps, and creates no WorkflowRun node
3. Resolves step dependencies into execution order (topological sort)
4. For each step: resolves `{{template.variables}}` from context and prior step outputs, spawns the target plugin's MCP server, and calls the specified tool
5. Records every step's outcome as a StepResult node
6. Creates a WorkflowRun node with the overall status

The pre-flight gate runs before any WorkflowRun is created, so a refused execution leaves no orphan `running` run behind. A missing required input is caught up front by the contract rather than late, mid-pipeline, as an unresolved-template failure.

### 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 |
| `failed` | A step failed with abort policy, execution stopped |
| `incomplete` | Refused at the pre-flight gate — a required input was missing, zero steps ran, no WorkflowRun was created |

### Step status values

| Status | Meaning |
|---|---|
| `completed` | Step ran normally and produced output |
| `skipped` | Step failed but `onFailure: "skip"` kept execution going |
| `failed` | Step failed and `onFailure: "abort"` stopped execution |

(Historic `degraded` step results from an earlier runner version remain in the graph; the runner no longer produces them.)

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

Scheduled workflows are dispatched by `check-due-events`, which the installer arms with a once-a-minute heartbeat cron (see `maxy-code-prd.md` §Scheduled tasks). When the dispatcher fires, 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.

## Tools

- `workflow-create` — Create a workflow with name, description, tool steps, and an optional declared input contract
- `workflow-list` — List workflows with status and step counts
- `workflow-get` — Fetch a workflow with full step details and its declared inputs
- `workflow-update` — Update name, description, steps (full replacement), inputs (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; refuses with `status: "incomplete"` when a required input is missing
- `workflow-runs` — Query execution history with optional filters
