# Veritas Kanban Workflow Engine — API Reference

**Version**: v3.3  
**Last Updated**: 2026-02-15  
**Base URL**: `http://localhost:3001/api`

---

## Table of Contents

1. [Authentication](#authentication)
2. [Workflow CRUD](#workflow-crud)
3. [Workflow Runs](#workflow-runs)
4. [Gate Operations](#gate-operations)
5. [Tool Policies](#tool-policies)
6. [Sandbox Policies](#sandbox-policies)
7. [Task Dependencies](#task-dependencies) (NEW — v3.3)
8. [Crash-Recovery Checkpointing](#crash-recovery-checkpointing) (NEW — v3.3)
9. [Observational Memory](#observational-memory) (NEW — v3.3)
10. [Agent Filter](#agent-filter) (NEW — v3.3)
11. [WebSocket Events](#websocket-events)
12. [TypeScript Interfaces](#typescript-interfaces)
13. [Error Responses](#error-responses)

---

## Authentication

All API endpoints currently accept an optional authentication key via `x-api-key` header. RBAC permissions are enforced based on workflow ACLs (Access Control Lists).

**Header**:

```
x-api-key: <your-api-key>
```

**Permissions**:

- `view` — View workflow definitions and runs
- `create` — Create new workflows
- `edit` — Update workflow definitions
- `delete` — Delete workflows (owner only)
- `execute` — Start workflow runs, approve gates

---

## Workflow CRUD

### GET /api/workflows

List all workflows (metadata only, filtered by user permissions).

**Request**:

```bash
curl http://localhost:3001/api/workflows
```

**Response**:

```json
[
  {
    "id": "feature-dev",
    "name": "Feature Development Workflow",
    "version": 2,
    "description": "End-to-end feature development pipeline"
  },
  {
    "id": "security-audit",
    "name": "Security Audit & Remediation",
    "version": 1,
    "description": "Scan, prioritize, and fix security issues"
  }
]
```

**Status Codes**:

- `200 OK` — Success

**Permissions**: Any authenticated user can list workflows they have `view` permission for.

---

### GET /api/workflows/:id

Get a specific workflow definition (full YAML content).

**Request**:

```bash
curl http://localhost:3001/api/workflows/feature-dev
```

**Response**:

```json
{
  "id": "feature-dev",
  "name": "Feature Development Workflow",
  "version": 2,
  "description": "End-to-end feature development pipeline",
  "config": {
    "timeout": 7200,
    "fresh_session_default": true,
    "progress_file": "progress.md",
    "telemetry_tags": ["workflow", "feature-dev"]
  },
  "agents": [
    {
      "id": "planner",
      "name": "Planner",
      "role": "planner",
      "model": "github-copilot/claude-opus-4.6",
      "description": "Decomposes tasks into user stories"
    },
    {
      "id": "developer",
      "name": "Developer",
      "role": "developer",
      "model": "github-copilot/claude-sonnet-4.5",
      "description": "Implements features"
    }
  ],
  "steps": [
    {
      "id": "plan",
      "name": "Plan: Decompose into stories",
      "type": "agent",
      "agent": "planner",
      "input": "Decompose this task into stories...",
      "output": {
        "file": "plan.yml"
      },
      "acceptance_criteria": ["stories:"],
      "on_fail": {
        "retry": 2,
        "escalate_to": "human"
      },
      "timeout": 600
    }
  ],
  "variables": {
    "repo_path": "{{task.git.worktreePath}}",
    "test_command": "npm test"
  }
}
```

**Status Codes**:

- `200 OK` — Success
- `404 Not Found` — Workflow not found
- `403 Forbidden` — No view permission

**Headers**:

```http
ETag: "workflow:feature-dev:2"
X-Resource-Revision: 2
```

**Permissions**: Requires `view` permission.

---

### GET /api/workflows/:id/access

Resolve workflow-specific actions and provenance for the current identity. Clients use this server-owned result to distinguish editable user workflows from built-in or shared read-only definitions.

**Request**:

```bash
curl http://localhost:3001/api/workflows/feature-dev/access
```

**Response**:

```json
{
  "workflowId": "feature-dev",
  "canView": true,
  "canEdit": false,
  "canExecute": true,
  "canDuplicate": true,
  "readOnlyReason": "Built-in workflows are read-only. Duplicate this workflow to customize it.",
  "provenance": {
    "kind": "built-in",
    "owner": "system",
    "createdBy": "system",
    "updatedBy": "system",
    "createdAt": "2026-02-09T12:00:00Z",
    "updatedAt": "2026-02-09T14:30:00Z"
  }
}
```

`provenance.kind` is `built-in`, `user-owned`, or `shared`. `canEdit` and `canExecute` combine the authenticated request permissions with the workflow ACL decision. `canDuplicate` reflects whether the authenticated request may create workflows. A read-only response includes an actionable reason suitable for the workflow browser.

**Status Codes**:

- `200 OK` — Access and provenance resolved
- `403 Forbidden` — No view permission
- `404 Not Found` — Workflow not found

**Permissions**: Requires `workflow:read` and workflow-level `view` permission.

---

### POST /api/workflows

Create a new workflow.

**Request**:

```bash
curl -X POST http://localhost:3001/api/workflows \
  -H "Content-Type: application/json" \
  -d '{
    "id": "hello-world",
    "name": "Hello World Workflow",
    "version": 1,
    "description": "A simple test workflow",
    "agents": [
      {
        "id": "writer",
        "name": "Writer",
        "role": "developer",
        "model": "github-copilot/claude-sonnet-4.5",
        "description": "Writes messages"
      }
    ],
    "steps": [
      {
        "id": "greet",
        "name": "Greet user",
        "type": "agent",
        "agent": "writer",
        "input": "Write a hello message",
        "output": {
          "file": "greeting.md"
        }
      }
    ]
  }'
```

**Response**:

```json
{
  "success": true,
  "workflowId": "hello-world"
}
```

**Status Codes**:

- `201 Created` — Workflow created successfully
- `400 Bad Request` — Validation error (missing required fields, invalid references)
- `409 Conflict` — Workflow ID already exists

**Permissions**: Any authenticated user can create workflows (becomes owner).

**Validation**:

- `id`: Required, alphanumeric + dashes, max 100 characters
- `name`: Required, max 200 characters
- `version`: Required, integer ≥ 0
- `description`: Required, max 2000 characters
- `agents`: Required, 1-20 agents
- `steps`: Required, 1-50 steps
- All `step.agent` references must match an `agents.id`
- All `on_fail.retry_step` references must match a `steps.id`

---

### PUT /api/workflows/:id

Update an existing workflow (auto-increments version).

**Request**:

```bash
curl -X PUT http://localhost:3001/api/workflows/hello-world \
  -H "Content-Type: application/json" \
  -H 'If-Match: "workflow:hello-world:1"' \
  -d '{
    "id": "hello-world",
    "name": "Hello World Workflow v2",
    "version": 1,
    "description": "Updated workflow with farewell step",
    "agents": [
      {
        "id": "writer",
        "name": "Writer",
        "role": "developer",
        "model": "github-copilot/claude-sonnet-4.5",
        "description": "Writes messages"
      }
    ],
    "steps": [
      {
        "id": "greet",
        "name": "Greet user",
        "type": "agent",
        "agent": "writer",
        "input": "Write a hello message",
        "output": {
          "file": "greeting.md"
        }
      },
      {
        "id": "farewell",
        "name": "Say goodbye",
        "type": "agent",
        "agent": "writer",
        "input": "Write a goodbye message",
        "output": {
          "file": "farewell.md"
        }
      }
    ]
  }'
```

**Response**:

```json
{
  "success": true,
  "version": 2
}
```

**Status Codes**:

- `200 OK` — Workflow updated successfully
- `400 Bad Request` — Validation error or ID mismatch
- `404 Not Found` — Workflow not found
- `403 Forbidden` — No edit permission
- `409 Conflict` — Workflow has changed since the supplied `If-Match` revision

**Permissions**: Requires `edit` permission.

**Notes**:

- Version is auto-incremented (ignore `version` in request body)
- Workflow `version` is also the optimistic-concurrency revision. Read the
  workflow ETag, then send it back with `If-Match` on update.
- Workflow definitions include `createdBy`, `updatedBy`, `createdAt`, and
  `updatedAt` when saved through the API.
- Active runs continue with their snapshotted version (no interruption)
- Changes are logged to `.veritas-kanban/workflows/.audit.jsonl`

**Conflict response**:

```json
{
  "code": "CONFLICT",
  "message": "workflow hello-world has changed since it was loaded. Reload and retry with the latest revision.",
  "details": {
    "resourceType": "workflow",
    "resourceId": "hello-world",
    "expectedRevision": 1,
    "currentRevision": 2,
    "current": {
      "id": "hello-world",
      "version": 2,
      "description": "Latest workflow body"
    }
  }
}
```

---

### DELETE /api/workflows/:id

Delete a workflow.

**Request**:

```bash
curl -X DELETE http://localhost:3001/api/workflows/hello-world
```

**Response**:

```
(Empty body, 204 status)
```

**Status Codes**:

- `204 No Content` — Workflow deleted successfully
- `404 Not Found` — Workflow not found
- `403 Forbidden` — Not owner (only owners can delete)

**Permissions**: Requires `delete` permission (owner only).

**Notes**:

- Deletes workflow YAML file
- Does NOT delete historical run data (runs remain accessible)
- Audit event logged

---

## Workflow Authoring

### POST /api/workflows/authoring/dry-run

Validate an unsaved workflow without starting a run. The response includes lint
messages, executable checks, and a skill audit for referenced shared skills.

**Request**:

```bash
curl -X POST http://localhost:3001/api/workflows/authoring/dry-run \
  -H "Content-Type: application/json" \
  -d '{
    "workflow": {
      "id": "release-helper",
      "name": "Release Helper",
      "version": 1,
      "description": "Uses a shared release skill",
      "agents": [
        {
          "id": "runner",
          "name": "Runner",
          "role": "developer",
          "tools": ["Read", "skill:release-helper"]
        }
      ],
      "steps": [{ "id": "run", "name": "Run", "type": "agent", "agent": "runner" }]
    },
    "context": { "clientMode": "remote" }
  }'
```

**Response excerpt**:

```json
{
  "status": "blocked",
  "canRun": false,
  "checks": [{ "id": "skill", "label": "Skill audit", "status": "fail" }],
  "messages": [
    {
      "category": "skill",
      "severity": "error",
      "message": "Skill Release Helper has no persisted scan and cannot run in remote mode."
    }
  ],
  "skillAudit": {
    "status": "fail",
    "mode": "remote",
    "references": [
      {
        "reference": "release-helper",
        "skillId": "release-helper",
        "status": "blocked",
        "message": "Skill Release Helper has no persisted scan and cannot run in remote mode."
      }
    ]
  }
}
```

The skill audit recognizes `skill:<id>` and `skill/<id>` references in agents,
tools, steps, variables, inputs, and descriptions. Local mode warns on unscanned
skills. Remote and cloud modes fail missing, unscanned, or blocked skills unless
the skill has an active reviewed exception.

Dry-run responses may include `pipelineSummary` when a workflow declares
`pipeline`. Pipeline lint validates the parent agent, subagent role references,
deliverable contracts, verification steps, dependencies, and whether each role is
used by a workflow step or parallel substep.

### GET /api/workflows/recipes

Returns reusable workflow recipes. v5 recipes include `.openclaw Audit`, which
materializes an orchestrated pipeline with config, storage, security, docs, and
follow-up task subagent roles.

### POST /api/workflows/recipes/:recipeId/materialize

Materializes a recipe into workflow JSON/YAML and a preview. When a recipe has a
pipeline, `preview.pipeline` contains role, status, scope, deliverable,
dependency, verification, and telemetry-budget metadata for the authoring UI.

### POST /api/workflows/:id/dry-run

Dry-run a saved workflow definition with the same response shape and context
rules as `/api/workflows/authoring/dry-run`.

---

## Workflow Runs

### POST /api/workflows/:id/runs

Start a new workflow run.

**Request**:

```bash
curl -X POST http://localhost:3001/api/workflows/feature-dev/runs \
  -H "Content-Type: application/json" \
  -d '{
  "taskId": "US-42",
  "context": {
    "clientMode": "remote",
    "priority": "high",
    "deadline": "2026-02-15"
  }
}'
```

**Request Body**:

```typescript
{
  taskId?: string;           // Optional: VK task ID to associate with run
  context?: {
    clientMode?: "local" | "remote" | "cloud"; // Optional: workflow skill gate mode
    [key: string]: unknown;
  };
  budget?: {
    enabled?: boolean;
    limits?: {
      totalTokens?: number;
      costUsd?: number;
      toolCalls?: number;
      runtimeSeconds?: number;
      idleRuntimeSeconds?: number;
      retries?: number;
      fanOut?: number;
    };
    softThresholdPercent?: number;
    hardAction?: "pause" | "require-approval" | "downgrade" | "cancel";
    downgradeModel?: string;
  };
}
```

Before a run starts, the server dry-runs the saved workflow and blocks remote or
cloud execution when a referenced shared skill is missing, unscanned, or blocked.
The run context includes the resulting `skillAudit` summary when execution is
allowed. Workflows with `pipeline` metadata also include `context.pipeline`,
which rolls subagent role status and time/token telemetry into the run record.

Run budgets are merged with workspace, workflow, and workflow-agent defaults
using the strictest positive limit. Soft thresholds write `budget-policy`
governance traces. Hard thresholds pause/block, require approval, downgrade the
model route, or cancel according to the effective policy.

The workflow root reserves durable admission capacity before the run becomes
active. Every provider-backed step then obtains a child reservation against
its resolved provider and selected host before its attempt becomes running.
The response exposes the root binding and the latest step decision without
including prompts or credentials. Use `/api/admission?workflowRunId=<run-id>`
or `vk admission list --workflow-run <run-id>` for current lease and limiting
policy details.

**Response**:

```json
{
  "id": "run_20260209_abc123",
  "workflowId": "feature-dev",
  "workflowVersion": 2,
  "taskId": "US-42",
  "status": "running",
  "currentStep": "plan",
  "context": {
    "task": {
      "id": "US-42",
      "title": "Implement user registration",
      "description": "Add registration endpoint with email validation"
    },
    "priority": "high",
    "deadline": "2026-02-15"
  },
  "startedAt": "2026-02-09T12:00:00Z",
  "steps": [
    {
      "stepId": "plan",
      "status": "running",
      "agent": "planner",
      "startedAt": "2026-02-09T12:00:00Z",
      "retries": 0
    },
    {
      "stepId": "implement",
      "status": "pending",
      "retries": 0
    }
  ]
}
```

**Status Codes**:

- `201 Created` — Run started successfully
- `400 Bad Request` — Validation error
- `404 Not Found` — Workflow not found
- `403 Forbidden` — No execute permission

**Permissions**: Requires `execute` permission.

**Notes**:

- Workflow execution begins immediately (asynchronous)
- Monitor progress via WebSocket or polling `/api/workflow-runs/:id`
- Run state persisted to `.veritas-kanban/workflow-runs/{runId}/run.json`

---

### GET /api/workflow-runs

List workflow runs with optional filters.

**Query Parameters**:

- `workflowId` (string, optional) — Filter by workflow ID
- `taskId` (string, optional) — Filter by task ID
- `status` (string, optional) — Filter by status: `pending`, `running`, `blocked`, `completed`, `failed`

**Request**:

```bash
# All runs
curl http://localhost:3001/api/workflow-runs

# Runs for a specific workflow
curl "http://localhost:3001/api/workflow-runs?workflowId=feature-dev"

# Runs for a specific task
curl "http://localhost:3001/api/workflow-runs?taskId=US-42"

# Failed runs only
curl "http://localhost:3001/api/workflow-runs?status=failed"
```

**Response**:

```json
[
  {
    "id": "run_20260209_abc123",
    "workflowId": "feature-dev",
    "workflowVersion": 2,
    "taskId": "US-42",
    "status": "completed",
    "startedAt": "2026-02-09T12:00:00Z",
    "completedAt": "2026-02-09T12:45:00Z",
    "duration": 2700,
    "stepsCompleted": 7,
    "stepsTotal": 7
  },
  {
    "id": "run_20260209_def456",
    "workflowId": "security-audit",
    "workflowVersion": 1,
    "status": "running",
    "currentStep": "fix",
    "startedAt": "2026-02-09T13:00:00Z",
    "stepsCompleted": 2,
    "stepsTotal": 5
  }
]
```

**Status Codes**:

- `200 OK` — Success

**Permissions**: Filtered by workflow `view` permissions.

---

### GET /api/workflow-runs/:id

Get full details of a specific workflow run.

**Request**:

```bash
curl http://localhost:3001/api/workflow-runs/run_20260209_abc123
```

**Response**:

```json
{
  "id": "run_20260209_abc123",
  "workflowId": "feature-dev",
  "workflowVersion": 2,
  "taskId": "US-42",
  "status": "completed",
  "currentStep": null,
  "context": {
    "task": { "id": "US-42", "title": "..." },
    "plan": { "stories": [...] },
    "implement": { "changes": "..." }
  },
  "startedAt": "2026-02-09T12:00:00Z",
  "completedAt": "2026-02-09T12:45:00Z",
  "lastCheckpoint": "2026-02-09T12:45:00Z",
  "steps": [
    {
      "stepId": "plan",
      "status": "completed",
      "agent": "planner",
      "sessionKey": "session_xyz",
      "startedAt": "2026-02-09T12:00:00Z",
      "completedAt": "2026-02-09T12:10:00Z",
      "duration": 600,
      "retries": 0,
      "output": ".veritas-kanban/workflow-runs/run_20260209_abc123/step-outputs/plan.yml"
    },
    {
      "stepId": "implement",
      "status": "completed",
      "agent": "developer",
      "startedAt": "2026-02-09T12:10:00Z",
      "completedAt": "2026-02-09T12:35:00Z",
      "duration": 1500,
      "retries": 1,
      "output": ".veritas-kanban/workflow-runs/run_20260209_abc123/step-outputs/implement-0.md",
      "loopState": {
        "totalIterations": 5,
        "currentIteration": 5,
        "completedIterations": 5,
        "failedIterations": 0
      }
    }
  ]
}
```

**Status Codes**:

- `200 OK` — Success
- `404 Not Found` — Run not found
- `403 Forbidden` — No view permission

**Permissions**: Requires `view` permission on the workflow.

---

### GET /api/workflow-runs/active

Get currently running workflow runs only.

**Request**:

```bash
curl http://localhost:3001/api/workflow-runs/active
```

**Response**:

```json
[
  {
    "id": "run_20260209_def456",
    "workflowId": "security-audit",
    "workflowVersion": 1,
    "status": "running",
    "currentStep": "fix",
    "startedAt": "2026-02-09T13:00:00Z",
    "stepsCompleted": 2,
    "stepsTotal": 5
  }
]
```

**Status Codes**:

- `200 OK` — Success

**Permissions**: Filtered by workflow `view` permissions.

**Notes**: Returns metadata only (not full run state).

---

### GET /api/workflow-runs/stats

Get aggregated workflow statistics for a given period.

**Query Parameters**:

- `period` (string, optional) — Period for stats: `24h`, `7d`, `30d` (default: `7d`)

**Request**:

```bash
curl "http://localhost:3001/api/workflow-runs/stats?period=7d"
```

**Response**:

```json
{
  "period": "7d",
  "totalWorkflows": 5,
  "activeRuns": 2,
  "completedRuns": 42,
  "failedRuns": 8,
  "avgDuration": 1800000,
  "successRate": 0.84,
  "perWorkflow": [
    {
      "workflowId": "feature-dev",
      "workflowName": "Feature Development Workflow",
      "runs": 25,
      "completed": 20,
      "failed": 5,
      "successRate": 0.8,
      "avgDuration": 1800000
    },
    {
      "workflowId": "security-audit",
      "workflowName": "Security Audit & Remediation",
      "runs": 17,
      "completed": 15,
      "failed": 2,
      "successRate": 0.88,
      "avgDuration": 900000
    }
  ]
}
```

**Field Descriptions**:

- `avgDuration` — Average duration in milliseconds
- `successRate` — Decimal (0.0 to 1.0) representing percentage
- `perWorkflow` — Per-workflow breakdown

**Status Codes**:

- `200 OK` — Success
- `400 Bad Request` — Invalid period value

**Permissions**: Filtered by workflow `view` permissions.

---

### POST /api/workflow-runs/:id/resume

Resume a blocked workflow run (after human approval or escalation).

**Request**:

```bash
curl -X POST http://localhost:3001/api/workflow-runs/run_20260209_abc123/resume \
  -H "Content-Type: application/json" \
  -d '{
    "context": {
      "reviewerComments": "Looks good, proceed"
    }
  }'
```

**Request Body**:

```typescript
{
  context?: Record<string, unknown>;  // Optional: Additional context for resume
}
```

**Response**:

```json
{
  "id": "run_20260209_abc123",
  "workflowId": "feature-dev",
  "status": "running",
  "currentStep": "deploy",
  ...
}
```

**Status Codes**:

- `200 OK` — Run resumed successfully
- `400 Bad Request` — Run not blocked (current status: running/completed/failed)
- `404 Not Found` — Run not found
- `403 Forbidden` — No execute permission

**Permissions**: Requires `execute` permission on the workflow.

**Notes**:

- Only runs with status `blocked` can be resumed
- Workflow execution continues from where it left off
- Context can be updated during resume

---

## Gate Operations

### POST /api/workflow-runs/:runId/steps/:stepId/approve

Approve a gate step (allows workflow to continue).

**Request**:

```bash
curl -X POST http://localhost:3001/api/workflow-runs/run_20260209_abc123/steps/quality-gate/approve
```

**Response**:

```json
{
  "id": "run_20260209_abc123",
  "workflowId": "feature-dev",
  "status": "running",
  "context": {
    "_gateApproval": {
      "stepId": "quality-gate",
      "approved": true,
      "approvedBy": "user-123",
      "approvedAt": "2026-02-09T14:00:00Z"
    }
  },
  ...
}
```

**Status Codes**:

- `200 OK` — Gate approved, run resumed
- `400 Bad Request` — Step not awaiting approval or not a gate step
- `404 Not Found` — Run or step not found
- `403 Forbidden` — No execute permission

**Permissions**: Requires `execute` permission on the workflow.

**Notes**:

- Only works for steps with `type: gate` and status `failed`
- Approval context is added to run context
- Workflow continues execution

---

### POST /api/workflow-runs/:runId/steps/:stepId/reject

Reject a gate step (marks workflow as failed).

**Request**:

```bash
curl -X POST http://localhost:3001/api/workflow-runs/run_20260209_abc123/steps/quality-gate/reject
```

**Response**:

```json
{
  "id": "run_20260209_abc123",
  "workflowId": "feature-dev",
  "status": "failed",
  "error": "Step quality-gate rejected by user-123",
  "completedAt": "2026-02-09T14:00:00Z",
  ...
}
```

**Status Codes**:

- `200 OK` — Gate rejected, run marked as failed
- `400 Bad Request` — Step not awaiting approval or not a gate step
- `404 Not Found` — Run or step not found
- `403 Forbidden` — No execute permission

**Permissions**: Requires `execute` permission on the workflow.

---

### GET /api/workflow-runs/:runId/steps/:stepId/status

Get detailed status of a specific step (useful for parallel sub-steps).

**Request**:

```bash
curl http://localhost:3001/api/workflow-runs/run_20260209_abc123/steps/implement/status
```

**Response**:

```json
{
  "stepId": "implement",
  "status": "completed",
  "agent": "developer",
  "sessionKey": "session_xyz",
  "startedAt": "2026-02-09T12:10:00Z",
  "completedAt": "2026-02-09T12:35:00Z",
  "duration": 1500,
  "retries": 1,
  "output": ".veritas-kanban/workflow-runs/run_20260209_abc123/step-outputs/implement-0.md",
  "loopState": {
    "totalIterations": 5,
    "currentIteration": 5,
    "completedIterations": 5,
    "failedIterations": 0
  }
}
```

**Status Codes**:

- `200 OK` — Success
- `404 Not Found` — Run or step not found
- `403 Forbidden` — No view permission

**Permissions**: Requires `view` permission on the workflow.

---

## Tool Policies

### GET /api/tool-policies

List all tool policies (default + custom).

**Request**:

```bash
curl http://localhost:3001/api/tool-policies
```

**Response**:

```json
[
  {
    "role": "planner",
    "allowed": ["read", "web_search", "web_fetch", "browser", "image", "nodes"],
    "denied": ["write", "edit", "exec", "message"],
    "description": "Analysis and planning — read-only access"
  },
  {
    "role": "developer",
    "allowed": ["*"],
    "denied": [],
    "description": "Feature implementation — full access"
  },
  {
    "role": "custom-auditor",
    "allowed": ["read", "web_search"],
    "denied": ["exec", "write", "edit"],
    "description": "Security auditor — read-only with web access"
  }
]
```

**Status Codes**:

- `200 OK` — Success

**Permissions**: Public (no authentication required).

---

### GET /api/tool-policies/:role

Get a specific tool policy by role.

**Request**:

```bash
curl http://localhost:3001/api/tool-policies/planner
```

**Response**:

```json
{
  "role": "planner",
  "allowed": ["read", "web_search", "web_fetch", "browser", "image", "nodes"],
  "denied": ["write", "edit", "exec", "message"],
  "description": "Analysis and planning — read-only access"
}
```

**Status Codes**:

- `200 OK` — Success
- `404 Not Found` — Policy not found

**Permissions**: Public.

---

### POST /api/tool-policies

Create a new custom tool policy.

**Request**:

```bash
curl -X POST http://localhost:3001/api/tool-policies \
  -H "Content-Type: application/json" \
  -d '{
    "role": "custom-auditor",
    "allowed": ["read", "web_search", "web_fetch", "browser"],
    "denied": ["exec", "write", "edit"],
    "description": "Security auditor — read-only with web access"
  }'
```

**Request Body**:

```typescript
{
  role: string;              // Required: role name (alphanumeric + dash/underscore, max 50 chars)
  allowed: string[];         // Required: tool names (use '*' for all tools)
  denied: string[];          // Required: tool names (takes precedence over allowed)
  description: string;       // Required: what this role does (max 500 chars)
}
```

**Response**:

```json
{
  "success": true,
  "role": "custom-auditor"
}
```

**Status Codes**:

- `201 Created` — Policy created successfully
- `400 Bad Request` — Validation error
- `409 Conflict` — Role already exists

**Permissions**: Any authenticated user can create custom policies.

**Validation**:

- `role`: Required, alphanumeric + dash/underscore, max 50 characters
- `allowed`: Required array, max 100 tools
- `denied`: Required array (can be empty), max 100 tools
- `description`: Required, max 500 characters
- Cannot use reserved role names (planner, developer, reviewer, tester, deployer)

---

### PUT /api/tool-policies/:role

Update an existing tool policy (including defaults).

**Request**:

```bash
curl -X PUT http://localhost:3001/api/tool-policies/custom-auditor \
  -H "Content-Type: application/json" \
  -d '{
    "role": "custom-auditor",
    "allowed": ["read", "web_search"],
    "denied": ["exec", "write", "edit", "message"],
    "description": "Updated auditor policy"
  }'
```

**Response**:

```json
{
  "success": true,
  "role": "custom-auditor"
}
```

**Status Codes**:

- `200 OK` — Policy updated successfully
- `400 Bad Request` — Validation error or role mismatch
- `404 Not Found` — Policy not found

**Permissions**: Any authenticated user can update policies.

**Notes**: Default policies can be edited but not deleted.

---

### DELETE /api/tool-policies/:role

Delete a custom tool policy.

**Request**:

```bash
curl -X DELETE http://localhost:3001/api/tool-policies/custom-auditor
```

**Response**:

```
(Empty body, 204 status)
```

**Status Codes**:

- `204 No Content` — Policy deleted successfully
- `400 Bad Request` — Cannot delete default policy
- `404 Not Found` — Policy not found

**Permissions**: Any authenticated user can delete custom policies.

**Notes**: Default policies (planner, developer, reviewer, tester, deployer) cannot be deleted.

---

### POST /api/tool-policies/:role/validate

Validate if a specific tool is allowed for a role.

**Request**:

```bash
curl -X POST http://localhost:3001/api/tool-policies/planner/validate \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "exec"
  }'
```

**Request Body**:

```typescript
{
  tool: string; // Required: tool name to validate
}
```

**Response**:

```json
{
  "role": "planner",
  "tool": "exec",
  "allowed": false,
  "reason": "Tool 'exec' is in the denied list"
}
```

**Status Codes**:

- `200 OK` — Success
- `404 Not Found` — Policy not found

**Permissions**: Public.

---

## Sandbox Policies

Workflow agents can set `sandboxPresetId` to select a sandbox policy preset for
that role. The workflow executor dry-runs the preset against the selected
provider before launching the step. Required unsupported controls block the
step before execution and write a `sandbox-policy` governance trace; advisory
unsupported controls continue with warnings.

Use `/api/sandbox-policies/validate` to preflight a workflow agent's preset in
the authoring UI or custom automation. Public preflights must include a
`providerRuntimeManifestDigest` currently registered by a connected agent host;
the API rejects stale, disconnected, unknown, or provider-mismatched manifests.
Presets can also be assigned visually in the workflow authoring panel.

Sandbox policies are complementary to tool policies:

- Tool policies decide which Veritas/OpenClaw tools a workflow role may use.
- Sandbox policies decide what the underlying provider process may access at
  launch time: filesystem paths, network egress, environment variables, and
  credentials.

---

## Budget Policies

Workflow definitions can set `config.budget` for workflow-wide defaults and
`agents[].budget` for stricter role-specific caps. Launch callers can also pass
a stricter `budget` override to `POST /api/workflows/:id/runs`.

Supported budget limits:

- `totalTokens`, `inputTokens`, and `outputTokens`
- `costUsd`
- `toolCalls`
- `runtimeSeconds` and `idleRuntimeSeconds`
- `retries`
- `fanOut`

Budget evaluation is policy enforcement, not dashboard-only analytics. Soft
thresholds create visible warnings and `budget-policy` governance traces. Hard
thresholds enforce the configured action:

- `pause` or `require-approval` blocks the workflow run for operator review.
- `downgrade` records a routed decision and applies `downgradeModel` to Codex
  workflow steps.
- `cancel` fails the run immediately.

The run record includes `budget.usage`, `budget.thresholdEvents`, `budget.traceIds`,
and `budget.modelOverride` so run detail, timelines, and completion packets can
show exactly what happened.

---

## Task Dependencies

### GET /api/tasks/:id/dependencies

Get the full dependency graph for a task (recursive tree traversal).

**Request**:

```bash
curl http://localhost:3001/api/tasks/US-42/dependencies
```

**Response**:

```json
{
  "task": "US-42",
  "depends_on": [
    {
      "id": "US-40",
      "title": "Create database schema",
      "status": "done",
      "depends_on": []
    },
    {
      "id": "US-41",
      "title": "Implement auth middleware",
      "status": "in-progress",
      "depends_on": [
        {
          "id": "US-39",
          "title": "Setup JWT library",
          "status": "done",
          "depends_on": []
        }
      ]
    }
  ],
  "blocks": [
    {
      "id": "US-43",
      "title": "Add user permissions",
      "status": "todo",
      "blocks": []
    }
  ]
}
```

**Status Codes**:

- `200 OK` — Success
- `404 Not Found` — Task not found
- `400 Bad Request` — Circular dependency detected

**Notes**:

- Returns recursive tree with all upstream (depends_on) and downstream (blocks) dependencies
- Cycle detection prevents infinite loops
- Batch-loaded for performance (no N+1 queries)

---

### POST /api/tasks/:id/dependencies

Add a dependency to a task.

**Request**:

```bash
curl -X POST http://localhost:3001/api/tasks/US-42/dependencies \
  -H "Content-Type: application/json" \
  -d '{
    "dependsOn": "US-40",
    "direction": "depends_on"
  }'
```

**Request Body**:

```typescript
{
  dependsOn: string; // Required: task ID of the dependency
  direction: 'depends_on' | 'blocks'; // Required: direction of dependency
}
```

**Status Codes**:

- `200 OK` — Dependency added
- `400 Bad Request` — Would create circular dependency
- `404 Not Found` — Task not found

**Notes**:

- `depends_on`: This task depends on the specified task
- `blocks`: This task blocks the specified task
- Validates for cycles before adding

---

### DELETE /api/tasks/:id/dependencies/:dependencyId

Remove a dependency from a task.

**Request**:

```bash
curl -X DELETE http://localhost:3001/api/tasks/US-42/dependencies/US-40?direction=depends_on
```

**Query Parameters**:

- `direction` (required): `depends_on` or `blocks`

**Status Codes**:

- `200 OK` — Dependency removed
- `404 Not Found` — Task or dependency not found

---

## Crash-Recovery Checkpointing

### POST /api/tasks/:id/checkpoint

Save checkpoint state for a task.

**Request**:

```bash
curl -X POST http://localhost:3001/api/tasks/US-42/checkpoint \
  -H "Content-Type: application/json" \
  -d '{
    "state": {
      "current_step": 3,
      "completed": ["step1", "step2"],
      "api_key": "sk-1234567890",
      "context": "Working on user authentication"
    }
  }'
```

**Request Body**:

```typescript
{
  state: any; // Required: checkpoint state (auto-sanitized for secrets)
}
```

**Response**:

```json
{
  "success": true,
  "checkpoint": {
    "taskId": "US-42",
    "state": {
      "current_step": 3,
      "completed": ["step1", "step2"],
      "api_key": "[REDACTED]",
      "context": "Working on user authentication"
    },
    "createdAt": "2026-02-15T12:00:00Z",
    "expiresAt": "2026-02-16T12:00:00Z",
    "resumeCount": 0
  }
}
```

**Status Codes**:

- `200 OK` — Checkpoint saved
- `400 Bad Request` — State exceeds 1MB limit
- `404 Not Found` — Task not found

**Notes**:

- Auto-sanitizes 20+ secret patterns (API keys, tokens, passwords, etc.)
- 1MB size limit enforced
- 24h expiry with automatic cleanup
- Secrets are sanitized in response but preserved in file for resume

---

### GET /api/tasks/:id/checkpoint

Resume checkpoint state for a task.

**Request**:

```bash
curl http://localhost:3001/api/tasks/US-42/checkpoint
```

**Response**:

```json
{
  "success": true,
  "checkpoint": {
    "taskId": "US-42",
    "state": {
      "current_step": 3,
      "completed": ["step1", "step2"],
      "api_key": "[REDACTED]",
      "context": "Working on user authentication"
    },
    "createdAt": "2026-02-15T12:00:00Z",
    "expiresAt": "2026-02-16T12:00:00Z",
    "resumeCount": 1
  }
}
```

**Status Codes**:

- `200 OK` — Checkpoint retrieved (increments resumeCount)
- `404 Not Found` — Task or checkpoint not found
- `410 Gone` — Checkpoint expired

**Notes**:

- Each GET increments the resumeCount
- Secrets are sanitized in response
- Use for sub-agent context injection

---

### DELETE /api/tasks/:id/checkpoint

Clear checkpoint state for a task.

**Request**:

```bash
curl -X DELETE http://localhost:3001/api/tasks/US-42/checkpoint
```

**Status Codes**:

- `200 OK` — Checkpoint cleared
- `404 Not Found` — Task or checkpoint not found

---

## Observational Memory

### POST /api/observations

Add an observation to a task.

**Request**:

```bash
curl -X POST http://localhost:3001/api/observations \
  -H "Content-Type: application/json" \
  -d '{
    "taskId": "US-42",
    "type": "decision",
    "content": "Chose React Query over Redux for simpler data fetching and better caching",
    "importance": 8
  }'
```

**Request Body**:

```typescript
{
  taskId: string; // Required: task ID
  type: 'decision' | 'blocker' | 'insight' | 'context'; // Required
  content: string; // Required: observation text (XSS-sanitized)
  importance: number; // Required: 1-10 (1-3: low, 4-7: medium, 8-10: high)
}
```

**Response**:

```json
{
  "success": true,
  "observation": {
    "id": "obs_abc123",
    "taskId": "US-42",
    "type": "decision",
    "content": "Chose React Query over Redux for simpler data fetching and better caching",
    "importance": 8,
    "createdAt": "2026-02-15T12:00:00Z",
    "createdBy": "veritas"
  }
}
```

**Status Codes**:

- `201 Created` — Observation added
- `400 Bad Request` — Invalid type or importance score
- `404 Not Found` — Task not found

**Notes**:

- Content is XSS-sanitized (strips script tags, dangerous attributes)
- Activity log entry created automatically

---

### GET /api/tasks/:id/observations

Get all observations for a task.

**Request**:

```bash
curl http://localhost:3001/api/tasks/US-42/observations
```

**Response**:

```json
{
  "success": true,
  "observations": [
    {
      "id": "obs_abc123",
      "taskId": "US-42",
      "type": "decision",
      "content": "Chose React Query over Redux",
      "importance": 8,
      "createdAt": "2026-02-15T12:00:00Z",
      "createdBy": "veritas"
    },
    {
      "id": "obs_def456",
      "taskId": "US-42",
      "type": "blocker",
      "content": "Waiting on API key from ops team",
      "importance": 6,
      "createdAt": "2026-02-15T13:00:00Z",
      "createdBy": "codex"
    }
  ]
}
```

**Status Codes**:

- `200 OK` — Success
- `404 Not Found` — Task not found

---

### GET /api/observations/search

Full-text search across all observations for all tasks.

**Request**:

```bash
curl "http://localhost:3001/api/observations/search?query=react+query&limit=10&offset=0"
```

**Query Parameters**:

- `query` (required): search terms (full-text search)
- `limit` (optional): max results per page (default: 50, max: 200)
- `offset` (optional): pagination offset (default: 0)

**Response**:

```json
{
  "success": true,
  "results": [
    {
      "id": "obs_abc123",
      "taskId": "US-42",
      "taskTitle": "Implement user authentication",
      "type": "decision",
      "content": "Chose React Query over Redux for simpler data fetching",
      "importance": 8,
      "createdAt": "2026-02-15T12:00:00Z",
      "createdBy": "veritas"
    }
  ],
  "total": 1,
  "limit": 10,
  "offset": 0
}
```

**Status Codes**:

- `200 OK` — Success
- `400 Bad Request` — Missing query or invalid limit/offset

**Notes**:

- Searches across all tasks
- Results include task title for context
- Max 200 results per page

---

### DELETE /api/observations/:id

Delete an observation.

**Request**:

```bash
curl -X DELETE http://localhost:3001/api/observations/obs_abc123
```

**Status Codes**:

- `200 OK` — Observation deleted
- `404 Not Found` — Observation not found

**Notes**:

- Activity log entry created automatically

---

## Agent Filter

### GET /api/tasks?agent=:name

Filter tasks by assigned agent name.

**Request**:

```bash
curl "http://localhost:3001/api/tasks?agent=codex"
```

**Query Parameters**:

- `agent` (optional): agent name (trimmed, max 100 chars)
- `status` (optional): filter by status (todo, in-progress, blocked, done)
- `limit` (optional): max results (default: 100)
- `offset` (optional): pagination offset (default: 0)

**Response**:

```json
{
  "success": true,
  "tasks": [
    {
      "id": "US-42",
      "title": "Implement user authentication",
      "status": "in-progress",
      "agents": ["codex"],
      "priority": "high",
      "type": "feature"
    },
    {
      "id": "US-45",
      "title": "Add input validation",
      "status": "todo",
      "agents": ["codex", "veritas"],
      "priority": "medium",
      "type": "code"
    }
  ],
  "total": 2
}
```

**Status Codes**:

- `200 OK` — Success

**Notes**:

- Agent name is case-insensitive and trimmed
- Works with existing pagination and filters
- Returns tasks where the agent is in the `agents[]` array

---

## WebSocket Events

All workflow state changes are broadcast via WebSocket for real-time UI updates.

**Connection**:

```javascript
const ws = new WebSocket('ws://localhost:3001/ws');

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('Event:', message.type, message.data);
};
```

### workflow:started

Emitted when a workflow run starts.

**Payload**:

```json
{
  "type": "workflow:started",
  "data": {
    "id": "run_20260209_abc123",
    "workflowId": "feature-dev",
    "status": "running",
    "startedAt": "2026-02-09T12:00:00Z",
    ...
  }
}
```

### workflow:step:started

Emitted when a step begins execution.

**Payload**:

```json
{
  "type": "workflow:step:started",
  "data": {
    "runId": "run_20260209_abc123",
    "stepId": "plan",
    "status": "running",
    "startedAt": "2026-02-09T12:00:00Z"
  }
}
```

### workflow:step:completed

Emitted when a step completes successfully.

**Payload**:

```json
{
  "type": "workflow:step:completed",
  "data": {
    "runId": "run_20260209_abc123",
    "stepId": "plan",
    "status": "completed",
    "completedAt": "2026-02-09T12:10:00Z",
    "duration": 600
  }
}
```

### workflow:step:failed

Emitted when a step fails.

**Payload**:

```json
{
  "type": "workflow:step:failed",
  "data": {
    "runId": "run_20260209_abc123",
    "stepId": "plan",
    "status": "failed",
    "error": "Acceptance criteria not met",
    "completedAt": "2026-02-09T12:10:00Z"
  }
}
```

### workflow:completed

Emitted when a workflow run completes (all steps succeeded).

**Payload**:

```json
{
  "type": "workflow:completed",
  "data": {
    "id": "run_20260209_abc123",
    "workflowId": "feature-dev",
    "status": "completed",
    "completedAt": "2026-02-09T12:45:00Z",
    "duration": 2700
  }
}
```

### workflow:failed

Emitted when a workflow run fails (step failed with no retry policy).

**Payload**:

```json
{
  "type": "workflow:failed",
  "data": {
    "id": "run_20260209_abc123",
    "workflowId": "feature-dev",
    "status": "failed",
    "error": "Step 'test' failed after 2 retries",
    "completedAt": "2026-02-09T12:30:00Z"
  }
}
```

### workflow:blocked

Emitted when a workflow run is blocked (waiting for human approval or gate).

**Payload**:

```json
{
  "type": "workflow:blocked",
  "data": {
    "id": "run_20260209_abc123",
    "workflowId": "feature-dev",
    "status": "blocked",
    "currentStep": "quality-gate",
    "error": "Quality gate failed — manual review required"
  }
}
```

### task:changed

Emitted when a task associated with a workflow run is updated (triggers counter/metrics refresh).

**Payload**:

```json
{
  "type": "task:changed",
  "data": {
    "taskId": "US-42",
    "status": "done",
    "workflowRunId": "run_20260209_abc123"
  }
}
```

### agent:status

Emitted when an agent's status changes (for multi-agent coordination).

**Payload**:

```json
{
  "type": "agent:status",
  "data": {
    "agent": "developer",
    "status": "working",
    "taskTitle": "Implement feature X",
    "workflowRunId": "run_20260209_abc123"
  }
}
```

---

## TypeScript Interfaces

### WorkflowDefinition

```typescript
export interface WorkflowDefinition {
  id: string;
  name: string;
  version: number;
  description: string;
  config?: WorkflowConfig;
  agents: WorkflowAgent[];
  steps: WorkflowStep[];
  variables?: Record<string, unknown>;
  schemas?: Record<string, unknown>;
}
```

### WorkflowConfig

```typescript
export interface WorkflowConfig {
  timeout?: number; // seconds
  fresh_session_default?: boolean;
  progress_file?: string;
  telemetry_tags?: string[];
  budget?: AgentBudgetPolicy;
}
```

### WorkflowAgent

```typescript
export interface WorkflowAgent {
  id: string;
  name: string;
  role: string; // maps to tool policy
  sandboxPresetId?: string; // maps to a sandbox policy preset
  budget?: AgentBudgetPolicy; // stricter workflow-agent budget
  model?: string; // default model for this agent
  description: string;
  tools?: string[]; // tool restrictions (overrides role policy)
}
```

### WorkflowStep

```typescript
export type StepType = 'agent' | 'loop' | 'gate' | 'parallel';

export interface WorkflowStep {
  id: string;
  name: string;
  agent?: string; // agent ID (required for agent/loop steps)
  type: StepType;
  fresh_session?: boolean; // legacy: use session config instead
  session?: StepSessionConfig;
  input?: string; // template for agent prompt
  output?: StepOutput;
  acceptance_criteria?: string[];
  on_fail?: FailurePolicy;
  timeout?: number;

  // Loop-specific config
  loop?: LoopConfig;

  // Gate-specific config
  condition?: string; // expression evaluating to boolean
  on_false?: EscalationPolicy;

  // Parallel-specific config
  parallel?: ParallelConfig;
}
```

### StepOutput

```typescript
export interface StepOutput {
  file: string; // filename in step-outputs/
  schema?: string; // schema ID for validation
}
```

### FailurePolicy

```typescript
export interface FailurePolicy {
  retry?: number;
  retry_delay_ms?: number; // delay between retries
  retry_step?: string; // retry a different step ID
  escalate_to?: 'human' | `agent:${string}` | 'skip';
  escalate_message?: string;
  on_exhausted?: EscalationPolicy;
}
```

### EscalationPolicy

```typescript
export interface EscalationPolicy {
  escalate_to: 'human' | `agent:${string}` | 'skip';
  escalate_message?: string;
}
```

### LoopConfig

```typescript
export interface LoopConfig {
  over: string; // expression returning array
  item_var?: string; // variable name for current item
  index_var?: string; // variable name for loop index
  completion: 'all_done' | 'any_done' | 'first_success';
  fresh_session_per_iteration?: boolean;
  verify_each?: boolean;
  verify_step?: string; // step ID to run after each iteration
  max_iterations?: number;
  continue_on_error?: boolean; // if true, failed iterations don't fail the loop
}
```

### GateStepConfig

```typescript
export interface GateStepConfig {
  condition: string; // expression evaluating to boolean
  on_false: EscalationPolicy;
}
```

### ParallelConfig

```typescript
export interface ParallelConfig {
  steps: ParallelSubStep[]; // sub-steps to execute in parallel
  completion: 'all' | 'any' | number; // wait for all, any, or N sub-steps
  fail_fast?: boolean; // if true, abort others when one fails
  timeout?: number; // max time to wait (seconds)
}

export interface ParallelSubStep {
  id: string;
  agent: string;
  input: string; // template for sub-step input
  output?: StepOutput;
  timeout?: number;
}
```

### WorkflowRun

```typescript
export type WorkflowRunStatus = 'pending' | 'running' | 'blocked' | 'completed' | 'failed';
export type WorkflowAdmissionState = 'waiting' | 'dispatching' | 'active' | 'terminal';

export interface WorkflowRun {
  id: string; // run_<timestamp>_<nanoid>
  workflowId: string;
  workflowVersion: number;
  taskId?: string; // optional task association
  admission?: WorkflowRootAdmissionBinding; // durable root reservation or queue identity
  status: WorkflowRunStatus;
  currentStep?: string; // current step ID
  context: Record<string, unknown>; // shared context across steps
  budget?: AgentBudgetState; // effective budget, usage, threshold events, traces
  startedAt: string;
  completedAt?: string;
  lastCheckpoint?: string; // last state persistence timestamp
  error?: string;
  steps: StepRun[];
}
```

### StepRun

```typescript
export type StepRunStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';

export interface StepRun {
  stepId: string;
  status: StepRunStatus;
  agent?: string;
  sessionKey?: string; // OpenClaw session key
  startedAt?: string;
  completedAt?: string;
  duration?: number; // seconds
  retries: number;
  output?: string; // path to output file
  error?: string;
  admission?: WorkflowStepAdmissionBinding; // latest executable attempt or queue decision

  // Loop-specific state
  loopState?: {
    totalIterations: number;
    currentIteration: number;
    completedIterations: number;
    failedIterations: number;
  };
}
```

When root or step capacity is temporarily unavailable, the corresponding
admission binding has `state: "waiting"` and a durable `queueEntryId`. The run
and step remain `pending`; provider execution is not marked active. A claimed
entry briefly uses `dispatching` while Veritas transfers durable ownership to
workflow recovery, then becomes `active` before provider execution.

### ToolPolicy

```typescript
export interface ToolPolicy {
  role: string;
  allowed: string[]; // tool names (use '*' for all)
  denied: string[]; // tool names (takes precedence)
  description: string;
}
```

### StepSessionConfig

```typescript
export interface StepSessionConfig {
  mode: 'fresh' | 'reuse'; // fresh = new session, reuse = continue existing
  context: 'minimal' | 'full' | 'custom'; // how much context to pass
  cleanup: 'delete' | 'keep'; // delete session after step or keep
  timeout: number; // session timeout in seconds
  includeOutputsFrom?: string[]; // step names for 'custom' context
}
```

### WorkflowACL

```typescript
export type WorkflowPermission = 'view' | 'create' | 'edit' | 'delete' | 'execute';

export interface WorkflowACL {
  workflowId: string;
  owner: string; // user ID or 'system'
  editors: string[]; // users who can edit
  viewers: string[]; // users who can view
  executors: string[]; // users who can trigger runs
  isPublic: boolean; // anyone can view/execute
}
```

### WorkflowAuditEvent

```typescript
export interface WorkflowAuditEvent {
  timestamp: string;
  userId: string;
  action: 'create' | 'edit' | 'delete' | 'run';
  workflowId: string;
  workflowVersion?: number;
  changes?: Array<{ field: string; oldValue: unknown; newValue: unknown }>;
  runId?: string;
}
```

---

## Error Responses

All errors follow this structure:

```json
{
  "error": {
    "message": "Human-readable error message",
    "code": "ERROR_CODE",
    "details": {}
  }
}
```

### Status Codes

| Code  | Name                  | Description                                              |
| ----- | --------------------- | -------------------------------------------------------- |
| `400` | Bad Request           | Validation error, missing required fields, invalid input |
| `401` | Unauthorized          | Missing or invalid authentication                        |
| `403` | Forbidden             | No permission to perform action                          |
| `404` | Not Found             | Resource not found                                       |
| `409` | Conflict              | Resource already exists (e.g., workflow ID collision)    |
| `500` | Internal Server Error | Unexpected server error                                  |

### Common Errors

#### Validation Error

```json
{
  "error": {
    "message": "Workflow must define at least one agent",
    "code": "VALIDATION_ERROR",
    "details": {
      "field": "agents"
    }
  }
}
```

#### Not Found

```json
{
  "error": {
    "message": "Workflow feature-dev not found",
    "code": "NOT_FOUND",
    "details": {
      "workflowId": "feature-dev"
    }
  }
}
```

#### Permission Denied

```json
{
  "error": {
    "message": "No edit permission for workflow feature-dev",
    "code": "PERMISSION_DENIED",
    "details": {
      "workflowId": "feature-dev",
      "requiredPermission": "edit",
      "userId": "user-123"
    }
  }
}
```

#### Conflict

```json
{
  "error": {
    "message": "Workflow hello-world already exists",
    "code": "CONFLICT",
    "details": {
      "workflowId": "hello-world"
    }
  }
}
```

---

**End of API Reference**
