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 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.
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.
Endpoints
| Method | Endpoint | Purpose |
|---|
| GET | /api/v1/runs | List runs with KPI tiles and pagination |
| GET | /api/v1/runs/{id} | Get one run by trace id |
List 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
{
"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
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
{
"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.