veritas-kanban/docs/API-WORKFLOWS.md

2339 lines
50 KiB
Markdown

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