Events

One run's full detail

One run's complete timeline, declared expectations judged against its outcome, output excerpt and, for a CI run, provider metadata — the trace-level detail GET /api/v1/runs and GET /api/v1/checks/{id}/runs deliberately omit.

events is the single time axis: every ping this run recorded (start, log, note, blocked, success, fail, cancel), interleaved with its step progress markers, plus one synthetic incident_opened event when an incident correlates with this rid. Ordered by at ascending; two events that tie on timestamp keep a stable source order (a run's own fail ping always precedes the incident_opened event it triggered, never the reverse). A step is listed once, as its step event, never twice.

events is capped at 200 entries. A run with more pings than that (a verbose CI job streaming per-line log pings is the realistic case) has the middle of its timeline dropped, oldest kept first; events_truncated is true whenever this happened. The run's own terminal ping (success/fail/cancel) is fetched independently of this cap and is always the last entry in events when one exists, even if it would otherwise have fallen past position 200 — so outcome, started_at, ended_at, duration_ms, output_excerpt and ci are never wrong because of this cap; only the interior of a very chatty run's displayed timeline is what gets thinned.

outcome, started_at, ended_at, duration_ms, title and exit_code are computed by the identical rule GET /api/v1/runs uses (see that endpoint's description), from this run's own real pings rather than a summary row — the two endpoints can never disagree about a run they both serve.

A run with spans but no pings (traced: true, see Run.traced) answers 200 with an empty events, its spans, and the outcome its spans give. A rid with neither pings nor spans is 404.

assertions reports every expectation declared via POST .../expectations, each judged against the run's terminal ping body using the same evaluator that judges the run when it closes. verdict is not_evaluated when the run has no terminal ping yet (there is no body to judge against), else pass or fail; failure carries the evaluator's one-line reason on fail and is null otherwise.

ci merges the ci_meta of every ping on this run, field by field, a later non-empty value winning, so the terminal ping's failing stage and duration sit beside the start ping's commit and run URL. null for an agent/heartbeat run with no CI provider attached.

newer_rid and older_rid are this MONITOR's runs either side of this one in time: the run that started just after it, and the one just before. Both are rids on the check named in the path, so a caller builds the neighbour's URL by swapping the rid segment. Either is null at that end of the monitor's own history (within the 90-day ping window), which is a fact about the monitor rather than about what a client happens to have listed, so a run reached by a direct link knows its neighbours without fetching a list first. Runs of OTHER monitors are never named here, however a list happens to be filtered.

spans is this run's OTLP spans, when any exporter sent them, in tree order: parents before children, siblings ordered by start time, and an orphan (a span whose parent_span_id matches no span in this same run) appended after every real root's subtree. Capped at 2,000 (spans_truncated); the separate per-run storage cap surfaces as spans_dropped, null when the run has no span stats at all. span_count, tokens_in, tokens_out and cost_usd (inherited from Run) are null the same way when the run was never traced.

Spans themselves are retained for 30 days; span_count, tokens_in, tokens_out and cost_usd survive for 90. A run between 31 and 90 days old therefore reports those four stats normally while spans comes back an empty array — retention, not a bug.

title, output_excerpt, events[].body, events[].label, steps[].name and every string field on spans[] are free text the run itself supplied (a ping's body_excerpt, a step's name, a run's title, an OTLP exporter's span content); render them as text, never interpret them as markup.

get/api/v1/checks/{id}/runs/{rid}

Request

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

Response

The run's full detail.

check_idstring uuid required
check_namestring required
projectstring 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_spansboolean required

True while spans for this run arrived in the last 2 minutes; the run's end and cost may still grow.

in_hook_sessionboolean 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_testboolean required

True for the set-up's test span. Listed, never counted.

failure_causestring 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_errorstring 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.

tracedboolean 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.

ridstring 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.

titlestring 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_atstring 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_atstring 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_msinteger 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_countinteger required

Number of progress markers recorded on this run's timeline (see RunStep), capped at 100 per run.

exit_codeinteger nullable required

Exit code carried by the terminal ping, when one exists and reported one.

span_countinteger 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_ininteger nullable required

Sum of gen_ai.usage.input_tokens across this run's GenAI spans. Null when the run was never traced.

tokens_outinteger nullable required

Sum of gen_ai.usage.output_tokens across this run's GenAI spans. Null when the run was never traced.

cost_usdstring 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_sourcestring 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_idstring uuid nullable required

The run's agent, by the rule RunListItem.agent_id reports it for the same run: the monitor's agent, or else the agent the run's dominant trace source resolves to (by slug, then name, then an adoption). Null, with agent_name, when neither gives one.

agent_namestring nullable required
source_namestring required

The run's dominant trace source (the OpenTelemetry service.name most of its spans carried), the same value as RunListItem.source_name. Empty string when the run carried no span or none named one. Third-party text.

output_excerptstring required

The terminal ping's body_excerpt, or an empty string for a run with no terminal ping yet. Third-party text: render it as text, never interpret it as markup.

events_truncatedboolean required

true when this run had more than 200 pings and events therefore does not show every one of them (oldest kept, middle dropped). The run's terminal ping is unaffected by this — it is always present and always the last entry in events when one exists — so outcome, duration_ms, output_excerpt and ci stay correct even when this is true.

spans_truncatedboolean required

true when this run had more than 2,000 recorded spans and spans therefore does not show every one of them (this endpoint's own display cap — see spans_dropped for the separate, more common storage cap). Always false once a run's spans have aged out at the 30-day retention cutoff described on spans above, since an empty spans[] from retention is not "too many", it is "none left to show".

spans_droppedinteger nullable required

Spans not stored because this run had already reached its 2,000-span storage cap. Null (not 0) when the run has no span stats at all — distinct from a traced run that dropped none.

newer_ridstring nullable

The rid of THIS monitor's next run in time, the one that started just after this one, or null when this is the monitor's newest run inside the 90-day ping window. It is an rid on the check named in this path, so the neighbour's URL is this one with the rid segment swapped. Never a run of another monitor.

older_ridstring nullable

The rid of THIS monitor's previous run in time, or null when this is the oldest run inside the 90-day ping window. Same scoping as newer_rid.

Example response

{
  "check_id": "550e8400-e29b-41d4-a716-446655440000",
  "check_name": "Nightly backup",
  "project": "reddit-bot",
  "failure_cause": "upstream",
  "upstream_error": "server_error",
  "rid": "9876543210",
  "title": "nightly backup",
  "outcome": "failed",
  "started_at": "2026-09-12T08:00:00Z",
  "ended_at": "2026-09-12T08:14:00Z",
  "duration_ms": 840000,
  "step_count": 3,
  "exit_code": 1,
  "incident": {
    "id": 4821,
    "cause": "fail",
    "opened_at": "2026-09-12T08:14:00Z",
    "closed_at": "2026-09-12T08:45:00Z"
  },
  "span_count": 14,
  "tokens_in": 1820,
  "tokens_out": 340,
  "cost_usd": "0.021400",
  "cost_source": "estimated",
  "agent_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "agent_name": "triage-bot",
  "source_name": "triage-bot",
  "events": [
    {
      "kind": "fail",
      "at": "2026-09-12T08:14:00Z",
      "label": "fail",
      "body": "exit 1: tests failed",
      "step_seq": 2
    }
  ],
  "steps": [
    {
      "seq": 2,
      "name": "db migrate",
      "at": "2026-07-20T10:04:00Z"
    }
  ],
  "assertions": [
    {
      "kind": "contains",
      "value": "ok",
      "verdict": "pass"
    }
  ],
  "output_excerpt": "exit 1: tests failed",
  "ci": {
    "run_url": "https://github.com/acme/api/actions/runs/128734",
    "commit_sha": "a1b2c3d4e5f6",
    "branch": "main",
    "actor": "octocat",
    "failing_stage": "test",
    "duration_s": 128734
  },
  "spans": [
    {
      "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
      "span_id": "00f067aa0ba902b7",
      "parent_span_id": "00f067aa0ba90111",
      "name": "chat.completions",
      "kind": "client",
      "status": "ok",
      "started_at": "2026-09-17T10:00:00.120Z",
      "ended_at": "2026-09-17T10:00:01.480Z",
      "duration_ms": 1360,
      "gen_ai": {
        "system": "anthropic",
        "model": "claude-sonnet-4-5",
        "tokens_in": 512,
        "tokens_out": 128,
        "cost_usd": "0.003400",
        "cost_source": "estimated"
      },
      "attributes": {
        "http.method": "POST",
        "http.status_code": 200
      },
      "source_name": "triage-bot",
      "peer_kind": "model",
      "peer_name": "anthropic claude-sonnet-4-5"
    }
  ],
  "newer_rid": "run-2026-08-23-002",
  "older_rid": "run-2026-08-22-014"
}

Changes

No recorded changes to this operation across all 2 revisions of this API.