# API Reference

The workflow API exposes run state, step history, durable events, and SSE subscriptions. When the background executor is enabled (`workflow.executor_enabled: true`), the API also supports enqueuing new runs.

## REST Endpoints

### `POST /api/workflow-runs`

Enqueue a workflow run for background execution. Requires the executor to be enabled.

```bash
$ curl -X POST http://localhost:8080/api/workflow-runs \
  -H "Content-Type: application/json" \
  -d '{"workflow_id": "issue_delivery", "target_ref": "my-target"}'
```

Request body:

| Field | Type | Required | Description |
|---|---|---|---|
| `workflow_id` | string | Yes | Workflow definition ID (max 128 chars) |
| `target_kind` | string | No | Target kind (default: `"generic"`, max 64 chars) |
| `target_ref` | string | Yes | Target reference (max 512 chars) |
| `inputs` | object | No | Workflow inputs (max 64KB serialized) |
| `params` | object | No | Param overrides as key-value strings (#958) |
| `space_id` | string | No | Space to scope the run to |

Responses:

| Status | Description |
|---|---|
| `201` | Run enqueued — returns `{"run_id": "..."}` |
| `404` | Workflow definition not found |
| `422` | Invalid definition, inputs too large, or validation failed |
| `503` | Executor not enabled |

The enqueued run starts in `pending` status. The background executor claims and dispatches it automatically.

### `GET /api/workflows`

List available workflow definitions from built-in and example directories. The `source` field is `"built_in"` or `"example"`.

```bash
$ curl http://localhost:8080/api/workflows
```

Example response:

```json
[
  {
    "id": "issue_delivery",
    "source": "example",
    "path": "/path/to/examples/workflows/issue_delivery.yaml"
  }
]
```

### `GET /api/workflow-runs`

List workflow runs.

```bash
$ curl "http://localhost:8080/api/workflow-runs?status=paused&limit=10"
```

Query parameters:

| Parameter | Description |
|---|---|
| `status` | Filter by run status |
| `workflow_id` | Filter by workflow id |
| `limit` | Page size, 1-100 |
| `offset` | Pagination offset |
| `db` | Optional shared database name when `db_manager` is present |

Example response:

```json
[
  {
    "id": "uuid",
    "workflow_id": "issue_delivery",
    "workflow_version": "0.1.0",
    "status": "paused",
    "target_kind": "issue",
    "target_ref": "123",
    "current_step_id": "fast_checks",
    "attempt_count": 4,
    "stop_reason": "stale_heartbeat_reclaimed",
    "created_at": "2026-03-15T10:00:00+00:00",
    "updated_at": "2026-03-15T10:04:00+00:00",
    "started_at": "2026-03-15T10:00:05+00:00",
    "completed_at": null,
    "heartbeat_at": null
  }
]
```

Possible `stop_reason` values for paused/failed runs:

| Value | Description |
|---|---|
| `process_interrupted` | Runner process exited unexpectedly |
| `process_interrupted_during_compensation` | Process exited during compensation rollback |
| `stale_heartbeat_reclaimed` | Heartbeat went stale; lock reclaimed automatically |
| `stale_heartbeat_reclaimed_during_compensation` | Heartbeat went stale during compensation rollback; lock reclaimed |

### `GET /api/workflow-runs/{run_id}`

Return run detail plus step history and any pending approval record.

```bash
$ curl http://localhost:8080/api/workflow-runs/<run_id>
```

Example response:

```json
{
  "id": "uuid",
  "workflow_id": "issue_delivery",
  "workflow_version": "0.1.0",
  "status": "running",
  "target_kind": "issue",
  "target_ref": "123",
  "current_step_id": "start_work",
  "attempt_count": 2,
  "stop_reason": null,
  "result_summary": null,
  "params": {"lint_command": "eslint src/"},
  "steps": [
    {
      "id": "uuid",
      "step_id": "gate_issue_current",
      "step_type": "gate",
      "runner_type": null,
      "status": "completed",
      "result_status": "success",
      "result_summary": "Gate 'issue_is_current' passed",
      "duration_ms": 6,
      "branch_id": null,
      "is_compensation": 0
    }
  ],
  "pending_approval": null,
  "recovery_actions": []
}
```

- `recovery_actions` — list of concrete recovery action strings for non-success runs. Empty for `completed`, `running`, `pending`, and `claimed` runs. For `waiting_for_approval` runs, includes approve/deny commands. For `waiting_for_input`, includes the respond command. For `failed`, `blocked`, `paused`, and other non-success states, delegates to the diagnosis engine for targeted next actions (#1153).

Run-level fields added by this release:

- `params` — resolved param values (defaults merged with overrides), `null` if no params declared (#958)

Step-level fields added by this release:

- `branch_id` — branch name for steps inside a `parallel` step, `null` for top-level steps (#976)
- `is_compensation` — `1` for compensation rollback steps, `0` for normal steps (#978)
- `on_failure_handled` — `true` in `result_artifacts` when an `on_failure` branch ran and succeeded (#1127)
- Failure branch steps use qualified IDs: `{parent_step_id}/_fail/{branch_step_id}` (#1127)
- `result_outputs` — resolved structured outputs declared via `outputs` on the step definition, `null` if none declared (#1131)

Event payload additions:

- `step_retry` events include `retry_reason`: `exception`, `failed_status`, or `result_condition` (#1129)
- `failure_branch_started`, `failure_branch_completed`, `failure_branch_failed` events for `on_failure` branches (#1127)

When the run status is `waiting_for_approval`, `pending_approval` contains the unresolved approval request (tool name, arguments, risk tier). It is `null` otherwise.

### `GET /api/workflow-runs/{run_id}/events`

Return the durable event log for one run.

```bash
$ curl http://localhost:8080/api/workflow-runs/<run_id>/events
```

Example response:

```json
[
  {
    "id": 1,
    "run_id": "uuid",
    "step_id": null,
    "event_type": "run_started",
    "payload": {
      "workflow_id": "issue_delivery",
      "target": "issue:123"
    },
    "created_at": "2026-03-15T10:00:05+00:00"
  },
  {
    "id": 2,
    "run_id": "uuid",
    "step_id": "start_work",
    "event_type": "step_started",
    "payload": {
      "step_type": "runner"
    },
    "created_at": "2026-03-15T10:00:07+00:00"
  }
]
```

The events endpoint supports filtering by event type prefix via the `event_type` query parameter.

| Parameter | Description |
|---|---|
| `event_type` | Filter events by type prefix. `transcript` returns all transcript events. `run` returns run-level events. Any prefix is supported — matching uses `event_type LIKE '{prefix}%'` |

Example — retrieve only transcript events:

```bash
$ curl "http://localhost:8080/api/workflow-runs/<run_id>/events?event_type=transcript"
```

### `GET /api/workflow-runs/{run_id}/steps/{step_id}/transcript`

Return transcript events for a specific step, ordered by creation time.

```bash
$ curl http://localhost:8080/api/workflow-runs/<run_id>/steps/<step_id>/transcript
```

This endpoint queries `list_transcript_events(db, run_id, step_id)` directly. Returns `404` if the run is not found.

Example response:

```json
[
  {
    "id": 42,
    "run_id": "uuid",
    "step_id": "start_work",
    "event_type": "transcript_assistant",
    "payload": {
      "content": "I'll start by reading the issue and understanding the requirements.",
      "tokens": 150,
      "model": "gpt-4o"
    },
    "created_at": "2026-03-15T10:00:09+00:00"
  },
  {
    "id": 43,
    "run_id": "uuid",
    "step_id": "start_work",
    "event_type": "transcript_tool_call",
    "payload": {
      "tool_name": "bash",
      "arguments": "{\"command\": \"gh issue view 123\"}"
    },
    "created_at": "2026-03-15T10:00:11+00:00"
  },
  {
    "id": 44,
    "run_id": "uuid",
    "step_id": "start_work",
    "event_type": "transcript_tool_result",
    "payload": {
      "tool_name": "bash",
      "status": "success",
      "output": "Issue #123: Add widget parser..."
    },
    "created_at": "2026-03-15T10:00:12+00:00"
  }
]
```

Errors:
- `404` — Run not found

### `PATCH /api/workflow-runs/{run_id}/repair`

Repair a field on a paused/waiting workflow run or one of its completed steps.

**Request body:**

```json
{
  "field_path": "inputs",
  "value": {"issue_number": 99}
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `field_path` | string | Yes | `"inputs"` for run fields, `"step.<step_id>.<field>"` for step fields |
| `value` | any | Yes | New value for the field |

The `actor` field in the audit event is resolved from the server's identity configuration — it cannot be supplied by the caller.

**Repairable fields:**
- Run: `inputs`
- Step: `result_artifacts`, `result_summary`

### Failure Triage Artifacts (#1151)

Failed steps may include a `failure_triage` object in `result_artifacts` with normalized fields extracted from the step's output:

```json
{
  "failure_triage": {
    "exit_code": 1,
    "first_failing_test": "tests/unit/test_foo.py::TestBar::test_baz",
    "first_assertion": "AssertionError: assert 1 == 2",
    "first_traceback_file": "src/anteroom/services/foo.py",
    "first_traceback_line": 45,
    "failure_count": 3,
    "failure_signature": "a1b2c3d4e5f6g7h8"
  }
}
```

All fields are optional — only those extractable from the output are included. The `failure_signature` is a stable 16-character SHA-256 prefix for deduplication across retries.

### Loop Step Artifacts (#1125, #1130, #1152)

Loop steps include a `loop_end_reason` key in `result_artifacts`:

| Value | Meaning |
|---|---|
| `until_met` | Loop exited because the `until` condition was satisfied |
| `until_not_met` | Loop exhausted all rounds without meeting `until` (with `fail_if_not_met: true`) |
| `break_when` | Loop exited because the `break_when` condition matched |
| `no_progress` | Loop stopped because the same failure signature repeated for `stop_after_unchanged` consecutive rounds |
| `all_succeeded` | Loop exited because all nested steps succeeded (legacy behavior, no `until` set) |
| `exhausted` | Loop exhausted all rounds (no `until`, or `fail_if_not_met: false`) |

**Response:** Updated run or step dict.

**Errors:**
- `404` — Run not found
- `422` — Invalid field, run not in repairable status (must be paused, waiting_for_approval, waiting_for_input, or failed), or step not completed

### Error Responses

Missing runs return:

```json
{"detail": "Run not found"}
```

with HTTP `404`.

## SSE Endpoint

Workflow event streaming uses the existing SSE endpoint:

```bash
$ curl -N "http://localhost:8080/api/events?workflow_run_id=<run_id>"
```

### Query Parameters

| Parameter | Description |
|---|---|
| `workflow_run_id` | Subscribe to `workflow:{run_id}` |
| `db` | Database name, default `personal` |
| `client_id` | Reserved client identifier |

`conversation_id` continues to serve chat/conversation SSE. Workflow SSE uses `workflow_run_id`, not a `conversation_id` hack.

### Example Stream

```text
event: connected
data: {}

event: workflow_run_started
data: {"run_id":"uuid","event_type":"run_started","workflow_id":"issue_delivery","target":"issue:123"}

event: workflow_step_started
data: {"run_id":"uuid","event_type":"step_started","step_type":"runner"}

event: workflow_step_finished
data: {"run_id":"uuid","event_type":"step_finished","result_status":"success","duration_ms":143}

event: workflow_step_continued
data: {"run_id":"uuid","event_type":"step_continued","step_id":"lint_check","result_status":"failed","summary":"exit code 1","continue_on":["failed"]}
```

The `step_continued` event is emitted when a step with `continue_on: [failed]` fails but the workflow proceeds. It is distinct from `step_failed`, which indicates the run aborted.

### `POST /api/workflow-runs/{run_id}/cancel`

Request cancellation of a workflow run. Works for any non-terminal status — running runs are cancelled cooperatively via DB poll, paused/waiting/blocked runs are cancelled immediately.

```bash
$ curl -X POST http://localhost:8080/api/workflow-runs/<run_id>/cancel
```

Response: `{"run_id": "...", "status": "cancelled"}` (or `"cancel_requested"` for running runs)

Errors:
- `404` — Run not found
- `409` — Run is in a terminal state (completed, failed, cancelled, compensated, compensation_failed)

### `POST /api/workflow-runs/{run_id}/approve`

Approve a pending tool approval request on a `waiting_for_approval` run.

```bash
$ curl -X POST http://localhost:8080/api/workflow-runs/<run_id>/approve \
  -H 'Content-Type: application/json' \
  -d '{"resolved_by": "operator"}'
```

Request body (optional): `{"resolved_by": "operator"}` (max 256 chars)

Response: `{"run_id": "...", "approval_id": "...", "status": "approved"}`

Errors:
- `404` — Run not found or no pending approval request

### `POST /api/workflow-runs/{run_id}/deny`

Deny a pending tool approval request. Moves the run to `paused` with stop reason.

```bash
$ curl -X POST http://localhost:8080/api/workflow-runs/<run_id>/deny \
  -H 'Content-Type: application/json' \
  -d '{"resolved_by": "operator"}'
```

Request body (optional): `{"resolved_by": "operator"}` (max 256 chars)

Response: `{"run_id": "...", "approval_id": "...", "status": "denied"}`

Errors:
- `404` — Run not found or no pending approval request
- `409` — Approval request already resolved

### `POST /api/workflow-runs/{run_id}/respond`

Resolve a `human_gate` step decision on a `waiting_for_input` run.

```bash
$ curl -X POST http://localhost:8080/api/workflow-runs/<run_id>/respond \
  -H 'Content-Type: application/json' \
  -d '{"selected_option": "approve"}'
```

| Field | Type | Required | Description |
|---|---|---|---|
| `selected_option` | string | Yes | Decision option (max 256 chars, defined in the workflow YAML) |
| `resolved_by` | string | No | Operator identifier (max 256 chars, default: "operator") |

Response: `{"run_id": "...", "decision_id": "...", "selected_option": "approve"}`

Errors:
- `404` — Run not found or no pending human decision
- `409` — Decision already resolved
- `422` — Missing or invalid `selected_option`

### `POST /api/workflow-runs/{run_id}/resume`

Resume a paused, waiting, blocked, or compensating run from its last checkpoint. The run continues from the last completed step, not from the beginning. For blocked runs, the blocking gate step is re-evaluated.

```bash
$ curl -X POST http://localhost:8080/api/workflow-runs/<run_id>/resume
```

No request body. The workflow definition is loaded from the stored snapshot.

Response: `{"run_id": "...", "status": "resuming", "message": "Run resuming from last checkpoint"}`

Errors:
- `404` — Run not found
- `409` — Run is not in a resumable state (must be paused, waiting_for_approval, waiting_for_input, compensating, failed, or blocked)
- `422` — Cannot resolve workflow definition for resume

## Current API Scope

What the workflow API does today:

- list definitions
- list runs
- show run detail
- show durable event history
- stream live events
- resume workflows
- cancel workflows
- approve/deny tool requests
- respond to human gates

## Related Monitoring Model

Workflow SSE is fed by the same event bus used elsewhere in Anteroom:

- CLI workflow runs publish events
- events are persisted to `change_log`
- the web app poller reads those events
- SSE subscribers receive the same transitions

That is what gives you cross-process visibility for CLI-started runs.

## See Also

- [Workflow Monitoring](monitoring.md)
- [Workflow Commands](commands.md)
- [API Index](../api/index.md)
