> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Runs

> Agent execution runs reconstructed from the Crew Journal stream.

A "run" is one agent execution — orchestrator picks up an assignment, exec's the CLI, the run ends with one of `COMPLETED` / `FAILED` / `CANCELLED` / `TIMEOUT`. Since PR #234 the legacy `agent_runs` table is gone; runs are reconstructed from the [Crew Journal](/guides/crew-journal) by grouping `journal_entries` on `trace_id` (which equals the run id). The HTTP shape is preserved — frontend consumers don't see a contract change.

Every endpoint is workspace-scoped via the session context.

<Note>
  Runs are **read-only** — there is no write endpoint. A run comes into existence when the orchestrator emits `run.started` and concludes when a terminal `run.{completed|failed|cancelled|timeout}` lands on the same `trace_id`.
</Note>

## Endpoints

| Method | Endpoint                                 | Purpose                                 |
| ------ | ---------------------------------------- | --------------------------------------- |
| GET    | [`/api/v1/runs`](#list-runs)             | List runs with KPI tiles and pagination |
| GET    | [`/api/v1/runs/{id}`](#get-a-single-run) | Get one run by trace id                 |

***

## List runs

```
GET /api/v1/runs
```

Returns a paginated list of runs with KPI tiles (`stats`) and pagination metadata. Backed by `journal.ListRuns` (CTE-grouped over `journal_entries` keyed on `trace_id`) plus `journal.RunStats` for the tiles.

**Query parameters:**

| Param      | Type    | Description                                                                                        |
| ---------- | ------- | -------------------------------------------------------------------------------------------------- |
| `status`   | string  | Filter to one of `RUNNING`, `COMPLETED`, `FAILED`, `CANCELLED`, `TIMEOUT`. Invalid values → `400`. |
| `agent_id` | string  | Filter by agent.                                                                                   |
| `trigger`  | string  | Filter by `payload.trigger_type` on the `run.started` entry (e.g. `USER`, `WEBHOOK`, `CRON`).      |
| `tag`      | string  | Match a single tag inside `payload.metadata.tags` array on the `run.started` entry.                |
| `page`     | integer | 1-based page index, default `1`. Out-of-range values clamp to `1`.                                 |
| `limit`    | integer | Page size, 1-100, default `50`. Out-of-range values fall back to the default.                      |

**Response:** `200 OK`

```json theme={null}
{
  "data": [
    {
      "id": "run_a1b2c3",
      "workspace_id": "ws_123",
      "agent_id": "agt_viktor",
      "agent_name": "Viktor",
      "agent_slug": "viktor",
      "crew_name": "Backend",
      "chat_id": "chat_xyz",
      "triggered_by": "user_42",
      "trigger_type": "USER",
      "status": "COMPLETED",
      "started_at": "2026-04-30T10:00:00Z",
      "finished_at": "2026-04-30T10:02:00Z",
      "error_message": null,
      "exit_code": 0,
      "metadata": {"tags": ["urgent", "compliance"]},
      "created_at": "2026-04-30T10:00:00Z"
    }
  ],
  "stats": {
    "running": 1,
    "today": 12,
    "failed": 2
  },
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 12,
    "total_pages": 1
  }
}
```

| Field                              | Type     | Description                                                                                   |
| ---------------------------------- | -------- | --------------------------------------------------------------------------------------------- |
| `data[].id`                        | string   | The run's `trace_id`.                                                                         |
| `data[].status`                    | string   | Computed from the terminal entry's type. `RUNNING` when no terminal has landed.               |
| `data[].started_at`                | string?  | RFC3339 from `run.started`. Null only on a corrupt row that lost its starter.                 |
| `data[].finished_at`               | string?  | RFC3339 from the terminal entry. Null while still running.                                    |
| `data[].error_message`             | string?  | Pulled from `terminal.payload.error_message`. Null on success.                                |
| `data[].exit_code`                 | integer? | Pulled from `terminal.payload.exit_code`. Null while running.                                 |
| `data[].metadata`                  | object?  | Pulled from `started.payload.metadata`. Free-shape per orchestrator.                          |
| `data[].agent_name` / `agent_slug` | string?  | Enriched from the `agents` table by the handler (one extra workspace-scoped SELECT per page). |
| `data[].crew_name`                 | string?  | Enriched via the `agents.crew_id` → `crews.name` join.                                        |
| `stats.running`                    | integer  | Distinct trace\_ids with `run.started` and no terminal in the workspace.                      |
| `stats.today`                      | integer  | Distinct trace\_ids with `run.started` `ts >= start-of-today UTC`.                            |
| `stats.failed`                     | integer  | Distinct trace\_ids with `run.failed` or `run.timeout` today.                                 |

**Errors:**

| Status | Condition                                                                                                                                                                        |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | Invalid `status` value (must be one of the five enum values), or no `workspace_id` resolved for the session (the `RequireWorkspace` middleware rejects before the handler runs). |
| 401    | Not authenticated.                                                                                                                                                               |
| 403    | Authenticated, but not a member of the resolved workspace.                                                                                                                       |
| 500    | DB error.                                                                                                                                                                        |

***

## Get a single run

```
GET /api/v1/runs/{id}
```

Returns a single run by trace id. The response shape is identical to one element of the `data[]` array from `GET /api/v1/runs` -- the handler reuses the same enrichment so dashboard detail views can share the row renderer.

**Path parameters:**

| Param | Description           |
| ----- | --------------------- |
| `id`  | The run's `trace_id`. |

**Response:** `200 OK`

```json theme={null}
{
  "id": "run_a1b2c3",
  "workspace_id": "ws_123",
  "agent_id": "agt_viktor",
  "agent_name": "Viktor",
  "agent_slug": "viktor",
  "crew_name": "Backend",
  "chat_id": "chat_xyz",
  "triggered_by": "user_42",
  "trigger_type": "USER",
  "status": "COMPLETED",
  "started_at": "2026-04-30T10:00:00Z",
  "finished_at": "2026-04-30T10:02:00Z",
  "error_message": null,
  "exit_code": 0,
  "metadata": {"tags": ["urgent", "compliance"]},
  "created_at": "2026-04-30T10:00:00Z"
}
```

The handler does not have a direct `journal.GetRunByID` primitive yet -- it scans up to 10 pages of `journal.ListRuns` (1000 rows) looking for the trace id. The typical lookup is page 1 (recent runs); the deeper scan keeps older runs resolvable without a contract change.

| Status | Condition                                                                                                                                                          |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | Empty `id` path segment, or no `workspace_id` resolved for the session (the `RequireWorkspace` middleware rejects before the handler runs).                        |
| `401`  | Not authenticated.                                                                                                                                                 |
| `403`  | Authenticated, but not a member of the resolved workspace.                                                                                                         |
| `404`  | No run with that id in the caller's workspace (cross-workspace probes are masked as 404 -- the journal store filter never matches a foreign workspace's trace id). |
| `500`  | Journal page query failed, or enrichment returned an empty slice for a found row (data integrity issue).                                                           |

***

## Tenancy

* `workspace_id` is taken from the session context — never from a query parameter.
* Cross-tenant trace IDs are filtered out at the journal store (`journal.ListRuns` requires a non-empty `WorkspaceID`).
* Enrichment lookup (`agents` + `crews`) is workspace-scoped, so an agent ID collision across workspaces (test fixtures, restored backups) cannot attach foreign names to a row.

## Related

* [Crew Journal guide](/guides/crew-journal) — full event-type catalog and the `run.*` lifecycle.
* [Journal API](/api-reference/journal) — the underlying read surface, including FTS search and SSE live tail with `trace_id` filtering for one run's spans.
* [`crewship journal --trace-id <run-id>`](/cli/journal) — list every span (LLM call, exec, network egress) belonging to one run.
