---
title: "List run history for a check (CI/CD runs and agent/heartbeat runs)"
method: GET
path: "/api/v1/checks/{id}/runs"
tags: ["CI"]
---

# List run history for a check (CI/CD runs and agent/heartbeat runs)

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

## Request

- Base URL: `https://app.lastping.dev`
- URL: `https://app.lastping.dev/api/v1/checks/{id}/runs`
- Auth: API key in header `X-Api-Key`

Returns the last N runs for the check, newest first — both CI/CD runs
(pings that carry structured `ci_meta`, received from a CI/CD provider
webhook) and agent/heartbeat runs (pings that carry only a `rid` set at
`/start`, with no `ci_meta`). A ping with neither `ci_meta` nor a `rid`
is not a run and is excluded.

CI runs include the full failure context decoded from `ci_meta`:
`run_url`, `commit_sha`, `branch`, `actor`, `failing_stage`, `duration_s`,
and `outcome`. These fields are CI-only — they are simply absent
(omitted, not null or empty string) on an agent/heartbeat run. When a
matching incident (opened within ±10 minutes of the ping) exists,
`incident_cause`, `incident_detail`, and `incident_resolved` are
populated on either kind — enabling unattended failure triage without
following links to the CI provider.

`duration_ms` is separate from `duration_s` and is NOT CI-only: it is
the wall-clock duration LastPing itself measured from the run's
`/start` -> success timing, present on any run (CI or agent/heartbeat)
whose success ping paired with its start. Use it to answer "how long
does this job normally take?" for a non-CI monitor, where `duration_s`
is never populated.

Every entry also carries the run id (`rid`) and, when the run reported
progress markers to `/<uuid>/step?rid=…`, the `steps` it reached. A run
that stalled therefore names the step it wedged on without a second
call. Steps are matched on `rid`, so they appear only when the job or
agent posts `/step` with the same run id it started with.

Always returns a JSON array (never null).

## Query parameters

- `limit` integer

## Response `200`

Array of runs — CI runs and agent/heartbeat runs (may be empty).

- CIRun[]
  - `id` string, uuid, required: Ping UUID that represents this CI run.
  - `kind` string, required: Ping kind. One of `success`, `start`, `fail`, `log`, `cancel`, `blocked`, `note` (see the `Ping` schema); CI/CD webhooks typically post `success` or `fail`.
  - `received_at` string, date-time, required: RFC 3339 UTC timestamp when the ping was received.
  - `rid` string: Run id this ping carried (`?rid=`), omitted when the ping carried none. It is the key `steps` are matched on.
  - `title` string: Title for this run, stored on the run's `/start` ping — distinct from `body_excerpt`. Matched on `rid` the same way `steps` is — both pings of a run (its start and its terminal ping) carry it. Set by whichever call reaches it FIRST with a non-empty title: the body of the run's own `/start` call, or a later `POST .../title?rid=<run-id>` sent any time while the run is still open — same request shape as `/blocked` and `/note`, but (unlike them) it records no ping of its own: it only ever sets `title` on the run's existing `/start` ping, so it never appears as a `kind` in the `Ping` schema. Only the first non-empty write counts; neither a later `/start` body nor a later `/title` call can change a title once one is set. Omitted when nothing has set a title yet, which is the common case: every existing monitor's runs are untitled until a caller starts setting one.
  - `steps` RunStep[]: Progress markers the run reported to `/<uuid>/step?rid=…`, in the order they happened. Omitted entirely when the run reported none — an absent `steps` means the run was not instrumented with steps, not that it reported zero. Steps are matched on `rid`, so a CI run only carries them when the job posted `/step` with the same run id the CI webhook reports. The two pings of one run (its `start` and its terminal ping) share a rid and therefore both carry that run's timeline. At most 100 steps are stored per run.
    - `seq` integer: 1-based position of this step within its run.
    - `name` string: Step label from `?step=`, trimmed and truncated to 200 characters. Defaults to `step` when the parameter is absent or blank.
    - `at` string, date-time: RFC 3339 UTC timestamp when the step ping was received.
  - `run_url` string: CI-only. Direct URL to the CI/CD run page. Omitted when unavailable, and always omitted on an agent/heartbeat run.
  - `commit_sha` string: CI-only. Full or abbreviated commit SHA that triggered the run. Omitted when unavailable, and always omitted on an agent/heartbeat run.
  - `branch` string: CI-only. Branch name that triggered the run. Omitted when unavailable, and always omitted on an agent/heartbeat run.
  - `actor` string: CI-only. Username or identity that triggered the run. Omitted when unavailable, and always omitted on an agent/heartbeat run.
  - `failing_stage` string: CI-only. Name of the failing step or job (omitted when unavailable, and always omitted on an agent/heartbeat run). Availability differs by provider: * **GitLab CI** — always populated on a failed pipeline, from the stage of the first failed entry in the hook's `builds[]`. * **GitHub Actions** — requires the repository webhook to subscribe to **Workflow jobs** (`workflow_job`) in addition to **Workflow runs**. Only `workflow_job` carries per-step conclusions; with `workflow_run` alone this field is always omitted. * **Jenkins** — never populated. The Notification Plugin payload reports only the build's overall result and carries no per-stage or per-step detail.
  - `duration_s` integer: CI-only. Run duration in seconds. Omitted when unavailable or zero, and always omitted on an agent/heartbeat run.
  - `duration_ms` integer: Wall-clock duration in milliseconds, measured by LastPing from the run's `/start` -> success timing (`pings.duration_ms`). Present on ANY completed run — CI or agent/heartbeat — whose success ping paired with a preceding start on the same `rid`; omitted otherwise. Distinct from `duration_s`: that field is CI-only and self-reported by the provider from `ci_meta`, this one is computed by LastPing for every run kind. The two can each be present without the other and must not be treated as confirming each other.
  - `outcome` string: CI-only. Normalised run outcome string from the CI provider (e.g. `failure`, `success`). Always omitted on an agent/heartbeat run.
  - `incident_cause` string: Cause from the correlated incident (e.g. `silence`, `fail`, `overrun`, `never_started`, `runaway`, `stalled`, `blocked`, `upstream`) if one was opened within ±10 minutes of this run. Empty when no incident correlates.
  - `incident_detail` string: Log excerpt attached to the correlated incident via the `/ci/{id}/detail` endpoint. Empty when no detail has been posted or no incident correlates.
  - `incident_resolved` boolean, required: True when the correlated incident has been closed (resolved). False when the incident is still open or when no incident correlates.
  - `span_count` integer, nullable, required: Number of OTLP spans recorded for this run. Null when the run was never traced.
  - `tokens_in` integer, nullable, required: Sum of `gen_ai.usage.input_tokens` across this run's GenAI spans. Null when the run was never traced.
  - `tokens_out` integer, nullable, required: Sum of `gen_ai.usage.output_tokens` across this run's GenAI spans. Null when the run was never traced.
  - `cost_usd` string, nullable, required: Summed cost of this run's GenAI spans, as decimal text, never a JSON number. Each span's cost is the one its SDK reported (`gen_ai.usage.cost`) or, when it reported none, LastPing's estimate from the model's published price and the span's token counts (`cost_source` says which). Null when the run was never traced, or when no span has either: no reported cost and no known price for its model.
  - `cost_source` string, required: What `cost_usd` is made of. A plain string, not an enum, so a new value never breaks a client: `client` when every priced call's cost was reported by the client (the exporter's own figure, or Claude Code's per-call cost), `estimated` when LastPing priced every call at API list prices, `mixed` when the sum holds both, empty when `cost_usd` is null.

## Other responses

- `400`: id is not a valid UUID.
- `401`: Missing or invalid API key.
- `404`: Check not found in this project.
- `429`: Too many requests. The per-principal fixed-window rate limit has been exceeded. Retry after the number of seconds indicated by the `Retry-After` response header. The problem body's `code` is `RATE_LIMITED`. It names this limiter specifically, so a client can tell it apart from the application-level throttles that also answer 429 with their own codes and their own, much longer, `Retry-After` values (`TEST_PING_RATE_LIMITED`, `TOO_MANY_ATTEMPTS`). Back off globally on `RATE_LIMITED`; back off on the one operation for the others. The principal is the API key for a key-authenticated request and the USER for a browser session, so every tab one person has open shares one budget.

---

[API](https://skmtc.dev/lastping/apis/lastping-management-api.md) · [All operations](https://skmtc.dev/lastping/apis/lastping-management-api/llms.txt) · [OpenAPI document](https://skmtc.dev/lastping/apis/lastping-management-api/revisions/bbc6eda91c46?raw)
