---
title: "List runs across every check in the project"
method: GET
path: "/api/v1/runs"
tags: ["Events"]
---

# List runs across every check in the project

`GET /api/v1/runs`

## Request

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

Returns runs project-wide, one row per run rather than per ping,
newest-started first by default (see `sort`): the console's Runs page and any automation
that wants a single feed across every monitor instead of walking
`GET /api/v1/checks/{id}/runs` per check.

A run is any (check, `rid`) pair that has at least one ping carrying
that `rid`; `started_at` is that run's earliest ping. `outcome` is
computed by the same rule the single-run/incident endpoints use: a
terminal ping (`success`, `fail`, or `cancel`) on the run decides
(`succeeded` / `failed` / `cancelled`); with no terminal ping, an
incident opened while this run was the check's armed run means
`failed` — this covers a `silence`/`stalled`/`overrun` incident that
aborts a run without a fail ping of its own; a last-seen `blocked`
ping means `blocked`; with none of those, a run whose start is older
than its monitor's `max_runtime_s` (24 hours when unset), plus, for
the monitor's current run, the time that run spent blocked, is
`unfinished`: it started and never ended, which is not a failure and
pages no one, and it is derived when read, so an old orphaned start
is named this way with no change to stored data; otherwise
`running`. Two entries with the same
`outcome` are therefore comparable across every monitor in the
project, regardless of what kind of job produced them.

A run can also exist only as OTLP spans, with no ping at all: a
trace sent with no `lastping.run_id` (one run per trace, `rid`
`trace-` followed by the hex trace id), or an explicit
`lastping.run_id` that never pinged. Such a run is listed with
`traced: true`, counted in `counts`, and graded from its spans (see
`Run.traced`); it is in the window when its latest span is. A run
with both pings and spans is listed once, from its pings. The
set-up's test span is listed with `is_test: true` and the title
`LastPing test span`, and is left out of `counts`.

**Pagination** is keyset: on `(started_at, check_id, rid)` descending
by default, and under another `sort` on that sort's value with the same
tie-break, so a run written while a caller is mid-page can never shift
another run into or out of the page it already fetched. `next_cursor`
is an opaque token; pass it back as `cursor` verbatim, with the same
`sort` and `dir`, to fetch the next page, and stop when `next_cursor`
is `null`. A `cursor` this API did not issue (or one that has been
altered), or one issued for a different `sort` or `dir`, is rejected
with 400 `INVALID_CURSOR` rather than silently starting over.

**`?sort=`** orders the list. `cost`, `tokens` and `spans` order by the
run's total cost, its tokens in and out together, or its span count,
and list only runs that hold spans: a run with no spans has none of the
three. Under them a run is in the window when its latest span is at or
after `since`, and `counts` describes the runs they list. A run whose
cost is unknown (`cost_usd` null) is last in both directions. Ties are
broken by `check_id`, then `rid`, both descending, so every run has
exactly one place in the order.

**`?since=`** bounds the scan and defaults to the last 7 days;
it is clamped to 90 days back regardless of what is asked for,
because pings are pruned at 90 days and an earlier bound finds
nothing further anyway. It selects runs, never reshapes them: a run
with any ping at or after `since` is listed with its real
`started_at`, duration and outcome, even when it started earlier. **`?until=`** (RFC 3339) bounds
`started_at` from above.

Every filter — `monitor`, `outcome` (all six values, including the
incident-without-a-terminal-ping case described above), `since`,
`until` and `cursor` — is applied before paging, so the page size
is exact: a page shorter than `?limit=` means there are no more
matching runs, and `next_cursor` is `null` on exactly that page.

**`counts`** reports how many runs the requested window holds by
outcome, alongside the page itself. It is not narrowed by `?outcome=`
or by the cursor, so one request answers both "which runs am I looking
at" and "how many of each does this window hold". See the `RunCounts`
schema, including why it is absent rather than zeroed on a page that
matched nothing. Additive: every field this response carried before is
unchanged.

**`?project=`** keeps the runs whose Claude Code hook start carried
that project label (`Run.project`), and narrows `counts` too.

**Traced filters** (the console's Traced view): `traced`, `agent`,
`dependency`, `operation`, `model`, `has_error`, `min_duration_ms`,
`min_cost_usd`, `trace_id` and `q`. `traced=true` keeps only runs
that hold spans (a non-null `span_count`). Each narrows the window the way
`monitor` does, so `counts` describes the filtered runs. The span
filters (`dependency`, `operation`, `model`) match spans that started
inside the window. Each row also carries the run's `agent_id`,
`agent_name`, `source_name` and `multi_trace` (see `RunListItem`).

Trace-level fields (individual ping bodies, step timelines, assertion
verdicts) are not part of this list response; fetch
`GET /api/v1/checks/{id}/runs/{rid}` for one run's full detail.

## Query parameters

- `monitor` string, uuid
- `outcome` 'succeeded' | 'failed' | 'cancelled' | 'blocked' | 'running' | 'unfinished'
- `since` string, date-time
- `until` string, date-time
- `project` string
- `agent` string
- `dependency` string
- `operation` string
- `model` string
- `has_error` boolean
- `traced` boolean
- `min_duration_ms` integer
- `min_cost_usd` string
- `trace_id` string
- `q` string
- `sort` 'started' | 'cost' | 'tokens' | 'spans'
- `dir` 'desc' | 'asc'
- `limit` integer
- `cursor` string

## Response `200`

A page of runs (may be empty), in the order `sort` and `dir` name (newest-started first by default).

- RunPage: The body of `GET /api/v1/runs` — one keyset page of runs.
  - `runs` RunListItem[], required
    - `check_id` string, uuid, required
    - `check_name` string, required
    - `project` string, required: Project label the Claude Code hook sent with the run's start (the basename of the working directory); empty when none. Third-party text: render it as text, never interpret it as markup.
    - `receiving_spans` boolean, required: True while spans for this run arrived in the last 2 minutes; the run's end and cost may still grow.
    - `in_hook_session` boolean, required: True for a run the agent reported itself that began while a Claude Code hook run was open on the same monitor: usually the same turn reported twice. Counted like any run.
    - `is_test` boolean, required: True for the set-up's test span. Listed, never counted.
    - `failure_cause` string, required: Why a failed run failed, when the reason is known. Its values today are an empty string (every other run), `assertion` and `upstream`. `assertion`: the run sent a success, but its output failed an output assertion (one configured on the monitor, or an expectation the run declared), so the run is `failed` and opens an incident with cause `assertion` like any failure. `upstream`: the run ended on an API error that is the model provider's rather than the agent's (`server_error`, `overloaded` or `rate_limit`, named in `upstream_error`). Such a run is `failed`, but it pages only once 3 runs in a row (or the monitor's `failure_threshold`, when that is higher) have ended that way, as an incident with cause `upstream`. A plain string rather than an enum: treat a value you do not recognise as an empty string.
    - `upstream_error` string, required: The upstream API error type the run ended on (`server_error`, `overloaded` or `rate_limit`), set only when `failure_cause` is `upstream`. An empty string for every other run, including one that ended on an API error that is not the provider's (for example `authentication_failed`, which is the user's to fix and is an ordinary failure). Lowercase letters and underscores only.
    - `traced` boolean, required: True for a run that exists only as OTLP spans, with no ping: a trace that arrived with no `lastping.run_id` (its `rid` is `trace-` followed by the hex trace id, one run per trace), or an explicit `lastping.run_id` that never pinged. Its `outcome` comes from its spans: `failed` when a root span reported an error status (an error on a child span alone does not fail the run), `succeeded` once the root span has closed, `running` before that, and `unfinished` once its first span is older than the monitor's maximum runtime with the root still open. `started_at` is its first span's start, `ended_at` its root span's end. Such a run never opens an incident and never changes the monitor's status. False for every run that has pings, including one that also carries spans.
    - `rid` string, required: The run id every ping of this run shares — caller-chosen, raw job input. Third-party text: render it as text, never interpret it as markup.
    - `title` string, required: The run's title, or an empty string when none was set. The set-up's test span (`is_test`) is titled `LastPing test span`. Third-party text: render it as text, never interpret it as markup.
    - `outcome` 'succeeded' | 'failed' | 'cancelled' | 'blocked' | 'running' | 'unfinished', required
    - `started_at` string, date-time, required: The run's earliest ping, or its first span for a spans-only run. A run is in a window when any of its pings is; its `started_at` is still its real start when that is earlier than `since`.
    - `ended_at` string, date-time, nullable, required: Null while the run has no terminal ping (`running`, `unfinished` or `blocked`, or `failed` by incident with no terminal ping of its own). For a finished run with spans it follows the latest span: the later of the terminal ping and the run's last span end, so spans a background task sends after the run ended move it (see `receiving_spans`). The outcome and alerting never read it.
    - `duration_ms` integer, nullable, required: State-machine-measured wall-clock duration, present when a terminal ping paired with this run's start. Null otherwise. For a finished run with spans it runs to `ended_at`, the latest span, when that is later; alerting reads the ping's own measured duration, never this figure.
    - `step_count` integer, required: Number of progress markers recorded on this run's timeline (see `RunStep`), capped at 100 per run.
    - `exit_code` integer, nullable, required: Exit code carried by the terminal ping, when one exists and reported one.
    - `incident` RunIncidentRef, required: The incident that opened while this run was the check's armed run (the run id is recorded when the incident opens).
      - `id` integer, required
      - `cause` string, required
      - `opened_at` string, date-time, required
      - `closed_at` string, date-time, nullable, required: Null while the incident is still open.
    - `span_count` integer, nullable, required: Number of OTLP spans recorded for this run. Null when the run was never traced — distinct from `0`, which cannot occur (a run's span totals start with its first span). This and the other three stats fields below survive for 90 days, longer than the individual spans they summarise (30 days) — see `RunDetail.spans`'s description. A run between 31 and 90 days old therefore still reports a nonzero `span_count` here even though `GET .../runs/{rid}` shows an empty `spans[]` for it.
    - `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 (e.g. `"0.003400"`), never a JSON number, so summing many small costs never loses precision to float rounding. 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.
    - `agent_id` string, uuid, nullable, required
    - `agent_name` string, nullable, required
    - `source_name` string, required
    - `multi_trace` boolean, nullable, required
  - `next_cursor` string, nullable, required: Opaque cursor for the next page; pass back as `?cursor=`. Null when this is the last page.
  - `counts` RunCounts: How many runs the REQUESTED WINDOW holds, by outcome: the `?since=`/`?until=` window narrowed by `?monitor=`, and deliberately NOT narrowed by `?outcome=` or by the cursor. So a caller filtering to `?outcome=failed` still learns how many succeeded runs the same window holds, and a caller on page 4 gets the same numbers it got on page 1. Computed in the same read that builds the page, so the numbers and the rows can never disagree. **Absent, never zeroed, when the page carries no run at all.** The totals ride the returned rows, so a request that matched nothing has nothing to carry them, and an object full of zeros would claim an empty window for a filter that merely matched none of a full one. Treat an absent `counts` as unknown.
    - `total` integer, required: Every run in the window, whatever its outcome. This is the number to print, rather than the sum of the six below: a seventh outcome added later would be counted here and in none of them.
    - `succeeded` integer, required
    - `failed` integer, required
    - `cancelled` integer, required
    - `blocked` integer, required
    - `running` integer, required
    - `unfinished` integer, required: Runs that started and never ended: no terminal ping, no incident, not blocked, and a start older than the monitor's `max_runtime_s` (24 hours when unset), plus, for the monitor's current run, the time that run spent blocked. Never counted in `failed`.

## Other responses

- `400`: An invalid `monitor`, `outcome`, `since`, `until`, `sort`, `dir`, `cursor` or Traced filter (`INVALID_FILTER`, `INVALID_SORT` or `INVALID_CURSOR`).
- `401`: Missing or invalid API key.
- `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.

## Changes

- **2026-10-19** `bbc6eda91c46`: 2 info
  - added the new optional `query` request parameter `dir`
  - added the new optional `query` request parameter `sort`

[Operation history](https://skmtc.dev/lastping/apis/lastping-management-api/changes/api/v1/runs/get.md)

---

[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)
