# Runs API

Runs record each occurrence of a job and every attempt to deliver it. Use these endpoints to build dashboards, export history, or debug failures.

## The run object

```json
{
  "id": "run_01J9ZM1A…",
  "job_id": "job_01J9ZK3Q…",
  "scheduled_at": "2026-11-10T03:30:00Z",
  "trigger": "schedule",
  "status": "succeeded",
  "attempts": 2,
  "claimed_at": "2026-11-10T03:30:00.004Z",
  "started_at": "2026-11-10T03:30:00.006Z",
  "finished_at": "2026-11-10T03:30:21.410Z",
  "next_attempt_at": null,
  "start_lag_ms": 6,
  "e2e_ms": 21410,
  "sla_breached": false,
  "last_error": null
}
```

| Field | Values |
|---|---|
| `trigger` | `schedule`, `manual`, `retry_manual` |
| `status` | `running`, `deferred`, `succeeded`, `failed`, `skipped`, `cancelled` |

See [Jobs, runs and attempts](../concepts/jobs-runs-attempts.md) for what each state and timestamp means.

## List runs of a job

`GET /api/v1/jobs/{id}/runs?status=failed&from=2026-11-01T00:00:00Z&to=2026-11-10T00:00:00Z&limit=50`

Newest first, with run ID breaking ties at the same scheduled time. Pass `next_cursor` as `cursor` to continue a page (default 50, maximum 200). Filter by `status`, `trigger`, `sla_breached=true`, and a `from`/`to` range on `scheduled_at`.

## List runs across the workspace

`GET /api/v1/runs` with the same filters plus `job_id` and `tag`.

## Get a run

`GET /api/v1/runs/{id}` returns the run plus its attempts:

```json
{
  "id": "run_01J9ZM1A…",
  "status": "succeeded",
  "attempts": [
    {
      "n": 0,
      "started_at": "2026-11-10T03:30:00.006Z",
      "finished_at": "2026-11-10T03:30:01.010Z",
      "duration_ms": 1004,
      "outcome": "retryable",
      "status_code": 503,
      "error": "HTTP 503",
      "request_summary": "POST https://api.example.com/reports (231 bytes)",
      "response_body": "upstream unavailable"
    },
    {
      "n": 1,
      "started_at": "2026-11-10T03:30:11.300Z",
      "finished_at": "2026-11-10T03:30:21.410Z",
      "duration_ms": 110,
      "outcome": "success",
      "status_code": 200,
      "error": null,
      "request_summary": "POST https://api.example.com/reports (231 bytes)",
      "response_body": null
    }
  ]
}
```

`response_body` is `null` when it wasn't stored (successful attempts by default) or has aged out. See [Data retention](../platform/data-retention.md).

## Retry a failed run

`POST /api/v1/runs/{id}/retry` → `202` with the new run. Only runs with status `failed` can be retried; the new run has `trigger = retry_manual`, the same `scheduled_at`, and a fresh set of attempts. Repeated requests for the same occurrence coalesce into one manual retry; an exhausted manual retry returns `409 conflict`.