---
title: "Declare a run's success criteria before its outcome is known"
method: POST
path: "/api/v1/checks/{id}/runs/{rid}/expectations"
tags: ["Checks"]
---

# Declare a run's success criteria before its outcome is known

`POST /api/v1/checks/{id}/runs/{rid}/expectations`

## Request

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

Lets an agent commit, at the start of a run, to the criteria by which
that run will be judged when it closes — instead of the run's own
success ping grading itself. The declared set is evaluated against
the run's ping body when the run closes, using the same evaluator as
per-check output assertions; a failed
expectation turns that close into a `fail` with cause `assertion`.

**Immutable: declare-once.** A second declaration for the same `rid`
is rejected with `409` and the first set is left unchanged — an agent
that could rewrite its criteria mid-run once it can see failure
coming would be self-grading again with extra steps.

Every assertion is validated before any write, using the same
evaluator this endpoint's response is later judged by
(the same validation everywhere): an uncompilable regexp, a `matches`
pattern over 1000 bytes, a missing required field for the given
`kind`, or a `json_path` containing `[`, `*` or `$` all fail the
request with `400` and **nothing is written**.

At most **20 assertions** are allowed per run, the same cap applied
to per-check output assertions.

Unlike per-check assertions, a declared run assertion has no `id` and
no `name` — nothing about it is ever referenced or edited after this
call, and it is not authored by a human choosing an alert label.

## Request body

- object
  - `assertions` object[], required: The run's complete set of expectations (1-20 entries). An empty array is rejected with 400 — declaring no criteria means never calling this endpoint, since an empty declaration would write nothing and leave a later call free to declare a full set after the outcome is already known, which is exactly the evasion declare-once immutability exists to prevent.
    - `kind` 'contains' | 'not_contains' | 'matches' | 'json_path', required: How the run's ping body will be inspected at close — same semantics as the `Assertion` schema's `kind`.
    - `value` string: Required for `contains`, `not_contains` and `matches`. For `json_path`, the expected value compared via `op`.
    - `path` string: Dotted path (e.g. `result.rows_processed`), `json_path` only.
    - `op` 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte': Comparison operator, `json_path` only.

## Response `201`

The declared set, as stored.

- object
  - `assertions` object[]
    - `kind` string
    - `value` string
    - `path` string
    - `op` string

## Other responses

- `400`: Invalid request — one of: - An empty `assertions` array. Declaring no criteria is expressed by not calling this endpoint at all, never by an empty array — see the `assertions` field description above. - More than 20 assertions in the request. - An assertion fails validation: missing `value`/`path`/`op` for the given `kind`, an uncompilable `matches` regexp, a `matches` pattern over 1000 bytes, an invalid `op`, or a `json_path` containing `[`, `*` or `$`. The 400 names the offending entry by its array position (e.g. `assertion "assertion[0]": ...`), since a declared run assertion has no `name` to report instead. - Malformed JSON body, or a missing `rid` path segment. Nothing is written when any entry fails validation.
- `401`: Missing or invalid API key.
- `404`: Check not found in this project, or `rid` has never been armed by a `/start` ping.
- `409`: The run has already closed, or expectations were already declared for this rid.
- `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)
