Live checks through the API¶
The concept behind live checks is explained in Live checks.
On this page you give an agent version live checks through the API, read what a stopped turn tells
your application, and read the results and pass rates back. A live check is a rule every real turn
of the agent is held to: on the caller's question (input) or on the model's answer (output),
deciding on the turn (block) or scoring a sample afterwards (background).
Your key needs the agents capability to write the checks. Reading one turn's results needs
threads, as reading the turn does; reading an agent's pass rates needs evaluations. You do the
same with a mouse in Live checks in the console.
Add checks to an agent version¶
Live checks are part of an agent version, written with the rest of it on POST /agents or
POST /agents/{agent_id}/versions:
curl -s -X POST "$CORE_URL/agents/agt_5Nj2.../versions" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"model": "<a model id>",
"instructions": "Answer questions about orders in two sentences.",
"blocked_reply": "I cannot help with that. Never share a password in this conversation.",
"recover_attempts": 2,
"live_checks": [
{"name": "no-passwords", "stage": "input", "kind": "not_contains",
"expected": "password"},
{"name": "no-iban", "stage": "output", "kind": "matches_pattern",
"pattern": "NL\\d{2}[A-Z]{4}\\d{10}", "max_match": 18, "recover": true,
"description": "Never state a bank account number."},
{"name": "polite", "stage": "output", "kind": "polite_tone",
"mode": "background", "sample": 0.2,
"alert": {"failure_rate": 0.3, "over_last": 50}}
]}'
This version refuses a question that mentions a password with the standard reply, never lets an answer carry a Dutch IBAN (and asks the model again when one does), and has a judge score the tone of one turn in five, raising a notification when 30% of the last 50 scores fail.
A version is the whole definition: a version you write without live_checks has no checks, and
calls that name no version run it at once: a version written through the API is published. Save it as a draft in the console
first when you want to measure it with an evaluation before your callers get it
(Drafts).
The version's fields¶
| Field | What it does |
|---|---|
live_checks |
at most 16 checks; names are unique within the list |
blocked_reply |
the standard reply, at most 1000 characters, a turn answers with when a blocking input check fails. Required as soon as one input check blocks |
recover_attempts |
how often an answer a check with recover failed is asked for again, 1 to 3; 2 when you leave it out |
A check's fields¶
| Field | What it does |
|---|---|
name |
lowercase letters, digits, - and _, starting with a letter or digit, at most 64 characters. Results, pass rates and notifications name the check by it, and the caller of a turn it stops sees it in X-Core-Check, so put nothing secret in it |
description |
optional, at most 300 characters: your own sentence for the rule. When an answer is asked for again, this is what the model is told it broke; without one, Core tells it the expected text of a judged check, or the check's name |
stage |
input (the caller's text, before any model call) or output (the answer) |
kind |
which rule, see the kinds |
mode |
block (default): decide on the turn and stop it on a failure. background: score the answer after the turn, on a sample |
expected |
the text the rule compares against, see the kinds |
pattern, max_match |
matches_pattern only: the regular expression, and the longest match it can have, 1 to 200 characters |
threshold |
judged kinds only: the score from 0 to 1 the check passes at; 0.7 when you leave it out |
judge_model |
judged kinds only: the model that judges this check; without one, the model the turn runs on |
recover |
true asks the model again when its answer fails this check; a blocking check on the output only |
on_error |
blocking judged checks only: "block" (when you leave it out) stops the turn when the judge cannot decide; "pass" lets the turn go on as if the check had passed, the result still errored |
sample |
background checks only: the share of turns judged, 0 to 1; 0.1 when you leave it out |
alert |
optional: {"failure_rate": …, "over_last": …}, see Alerts |
The kinds¶
kind |
Fails when | expected |
Stage | Background |
|---|---|---|---|---|
contains_text |
the text does not contain expected, ignoring case |
required | both | no |
not_contains |
the text contains expected, ignoring case |
required | both | no |
matches_pattern |
the text holds a match of pattern |
not used | both | no |
must_use_tool |
the turn did not call the tool named in expected, or no tool at all when expected is empty |
optional | output | no |
correct_format |
a judge finds the answer does not match the format expected describes |
required | both | yes |
polite_tone |
a judge finds the tone not professional, helpful and polite | not used | both | yes |
no_hallucination |
a judge finds something made up | not used | both | yes |
llm_judge |
a judge finds the text does not meet the rule expected states |
required | both | yes |
These are the kinds an evaluation uses, so a rule means the same in a
suite and on a live turn. For a judged kind, expected is the rule, such as "the question is
about our products", not an answer you expect. response_time is refused 422 as not yet supported.
A pattern needs a bound. pattern is RE2 syntax, at most 500 characters, without
backreferences or lookaround. It is refused when its longest match could be longer than
max_match, as with .* or \w+, or when it can match nothing at all. The bound is what keeps a
streamed answer only a few characters behind the model: Core holds back max_match characters
before it sends them. NL\d{2}[A-Z]{4}\d{10} matches exactly 18 characters, so max_match: 18
fits.
A judge model has to be a model of your organisation's catalogue, never a router. When the
agent's own model is a router, every judged check names a judge_model, because a router picks
a different model per turn. An agent without a model of its own, called with a router as the
model, needs one too: a judged check without a judge_model then cannot decide (errored), and a
blocking one stops the turn with check_errored, unless it says "on_error": "pass". The judge's tokens count as model_tokens on the key that ran the
turn.
What your application receives¶
A turn a check stopped keeps the vendor's shape, and the X-Core-Check header says which check
stopped it:
X-Core-Check: stage=input, check="no-passwords", code=input_check_failed
The header is a structured dictionary (RFC 8941) with stage, check and code. It names the
first blocking check that did not pass, and is absent on every turn no check stopped, a recovered
one included. It never carries a pattern, expected text, threshold, score or reason.
code |
What happened | The response |
|---|---|---|
input_check_failed |
a blocking input check failed; no model was called | completed: one assistant message whose only content part is a refusal holding blocked_reply |
output_check_failed |
a blocking output check failed and the answer was not asked for again | incomplete, incomplete_details.reason: "content_filter" |
recovery_exhausted |
the answer was asked for again and its last attempt still failed | incomplete, content_filter |
check_errored |
a blocking check could not decide, so the turn was stopped; never a check with "on_error": "pass" |
refused or withheld, by the check's stage |
Read the refusal part as you read a model's own refusal: show it to the person. On
POST /messages a refused question and a withheld answer are both stop_reason: "refusal",
with the same header.
A streamed turn. An input check runs before the stream starts, so its header arrives on a
streamed turn too. An output check decides after the headers were sent: the stream ends with
response.incomplete and content_filter, and
GET /responses/{id}/checks names the check. When the agent's blocking output checks
are all patterns, the answer streams a few characters behind the model and stops where a match
starts; with any other blocking output check, the answer arrives in one piece once it has passed.
A recovered turn. When a check with recover fails the answer, Core asks the model again
without showing it the rejected answer, and the response holds only the answer that passed. Each
attempt is a model call billed on your key and included in usage; the turn's deadline and your
usage cap are checked before each one. Recovery is not attempted on a streamed turn whose answer was
already reaching you: there the answer is cut.
A turn Core queues itself. A workflow step or a task started in the background is refused when
an input check fails, since no one waits for the standard reply: the workflow step ends
policy_refused, and the task is not started. A request you send with background: true is
answered with the standard reply at once.
Read the results of one turn¶
curl -s "$CORE_URL/responses/resp_01M3.../checks" \
-H "Authorization: Bearer $CORE_KEY"
{
"items": [
{"object": "response.check", "id": "rck_…", "response_id": "resp_01M3…",
"name": "no-passwords", "stage": "input", "kind": "not_contains", "mode": "block",
"attempt": 1, "outcome": "failed", "score": null,
"reason": "The answer contains text it had to avoid.", "judge_model": null,
"passed_on_error": false, "created_at": "2026-10-16T09:14:03Z"}
],
"next_cursor": null
}
Every result of every check on the turn, in the order they were decided, paged by cursor
(Cursor pagination). outcome is passed,
failed or errored; passed_on_error is true on an errored result its check's
"on_error": "pass" let through. attempt counts the answers: 1 is the first, 2 the first answer asked for
again. reason is a fixed sentence for a rule and for a check that could not decide, and the
judge's own sentence for a judged check; it never repeats the text the check read. The fixed
sentences are the ones an evaluation uses, so they speak of "the answer" on an input check too. A background
check's result appears once the judge has scored it, shortly after the turn.
You read the results of a turn wherever you can read the turn itself with GET /responses/{id};
a turn that was not stored answers 404.
Read an agent's pass rates¶
curl -s "$CORE_URL/agents/agt_5Nj2.../checks/summary?since=2026-10-09T00:00:00Z" \
-H "Authorization: Bearer $CORE_KEY"
{
"since": "2026-10-09T00:00:00Z",
"items": [
{"name": "no-iban", "passed": 412, "failed": 3, "errored": 0},
{"name": "no-passwords", "passed": 409, "failed": 6, "errored": 0},
{"name": "polite", "passed": 79, "failed": 2, "errored": 1}
]
}
Per check name, how many results of the agent's turns passed, failed or could not decide since
since: seven days back when you leave it out, at most ninety. The counts cover every version of
the agent, and never carry a reason or any text.
Alerts¶
A check with alert raises a notification when too many of its latest results did not pass:
"alert": {"failure_rate": 0.3, "over_last": 50}
Once the check has at least over_last results (1 to 1000), and the share of them that failed or
could not decide is at or above failure_rate (above 0, at most 1), the organisation's
administrators get one Live check failing notification by mail or Slack. It names the agent,
the check and the count, never a question or an answer, and comes at most once a day per check.
The results of every version of the agent count. An organisation that switched the kind
check.failing off is not notified; see Notifications.
Events¶
Every result that did not pass, in either mode, is a response.check_failed event on your
event feed and webhooks, with the check's name, stage, kind and
outcome and the turn's response_id. Use it to follow failures as they happen rather than
reading pass rates.
Errors¶
| Status | Cause |
|---|---|
422 on live_checks |
more than 16 checks, two with the same name, a field the kind does not use or a field it needs left out, an input stage for must_use_tool, a background check that is not a judged check on the output, recover on anything but a blocking output check, "on_error": "pass" on anything but a blocking judged check, sample on a blocking check, a judge model that is a router or not in your catalogue, or a judged check without judge_model on an agent whose model is a router |
422 on live_checks |
a pattern that RE2 refuses, that can match nothing, or whose longest match could exceed max_match |
422 on live_checks |
kind: "response_time", which is not yet supported on a live check |
422 on blocked_reply |
an input check blocks and the version has no standard reply |
422 on recover_attempts |
a number outside 1 to 3 |
422 on since |
more than ninety days back |
404 |
no such response or agent, or a turn that was not stored — or threads (one turn's results) or evaluations (pass rates) is not effective for your key |