Eval runs
Start a run of an agent's eval cases with a key, stop it, and read its status and verdicts. What a run costs, how a retry stays safe with an Idempotency-Key, and how to poll it.
A run plays an agent's eval cases against it, one case at a time. For each case the Subcore console plays the customer, sends each agent turn to the agent's deployment on its eval door, and judges the conversation at the end. These routes start the same runs the console's Run button starts, and read every run of the agent, whoever started it.
| Route | What it does | Success |
|---|---|---|
POST /agents/{slug}/evals/runs | Start a run of a suite, or of one case | 201, or 200 on a replay, { "replayed", "run" } |
POST /agents/{slug}/evals/runs/{runId}/stop | Ask it to stop | 200 { "run" } |
GET /agents/{slug}/evals/runs | The agent's runs, newest first | 200 { "items", "nextCursor" } |
GET /agents/{slug}/evals/runs/{runId} | One run's status | 200 { "run" } |
GET /agents/{slug}/evals/runs/{runId}/results | Its status and its verdicts, a page at a time | 200 { "run", "items", "nextCursor" } |
GET /agents/{slug}/evals/runs/{runId}/result | One verdict, with its details | 200 { "result" } |
Every route needs the
manage-agents permission, counts
against the agent's rate budget, and answers
404 NOT_FOUND for an agent that is not the organization's or is
retired. Starting and
stopping a run also need the
run-evals permission. The MCP server's
eval tools call the same handlers.
What a run costs
A run is not free, and it is not quick:
- It spends the agent's deployment. Every agent turn is a request to it, with the agent's real model, on your deployment's model key.
- It spends Subcore's models. The simulated customer and the judge run on Subcore's side.
- It takes minutes. Cases run one at a time, each up to its
maxTurnsagent turns, and a deployment answers at most two eval turns at once, across every run on it. A suite of 20 cases is several minutes.
One run per agent goes at a time. A second start answers 409 RUN_IN_PROGRESS
until the first one ends. Tools return what each case says they return, and
only the tools your project lists may run for real, so a run does not reach
your systems unless you let it. See evals.
Start a run
POST /agents/{slug}/evals/runsThe body holds exactly one of:
| Field | Runs |
|---|---|
suite | Every live case in that suite. Archived cases are left out |
caseId | That one case |
curl -X POST -H "Authorization: Bearer $SUBCORE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: seed-2026-10-30-reschedules" \
"https://api.subcore.ai/agents/front-desk/evals/runs" \
-d '{ "suite": "reschedules" }'{ "replayed": false, "run": { "runId": "run_4f0c…", "status": "running", "total": 12, … } }It answers 201 once the run has started, with the cases fixed as they were: an
edit, an archive or a delete after this point applies to the next run. Both
fields, neither, or another field is 400 INVALID_BODY, and a body over 4 KiB
is 413 PAYLOAD_TOO_LARGE.
Before it starts anything, the console checks the run can go. It refuses:
| Status | code | When |
|---|---|---|
| 404 | CASE_NOT_FOUND | caseId names no case of this agent |
| 409 | CASE_ARCHIVED | caseId names an archived case. Unarchive it first |
| 409 | NO_CASES | The suite has no live case |
| 409 | TOO_MANY_CASES | The suite has more than 50 live cases. count says how many |
| 409 | EVAL_LIVE_NOT_ALLOWED | A case answers a tool live that the deployment does not let run live on the case's channel, including a case on a channel the agent does not run. cases lists each, [{ "caseId", "tools" }] |
| 409 | NO_DEPLOYMENT | The agent names no deployment |
| 409 | EVALS_UNAVAILABLE | Its deployment does not advertise the eval door, which comes with kit 0.44.0, or has no approved address |
| 409 | RUN_IN_PROGRESS | Another run of this agent is going. runId names it, to poll or stop. It is null when that run ended in between: retry |
Retrying safely: the Idempotency-Key
A start that timed out may have started a run. Send an Idempotency-Key
header, any 1 to 255 characters you choose, and retry with the same key:
- A key that already started a run, with the same agent and body from the same
API key, answers
200withreplayed: trueand that run, whatever its state now: still running, complete, or since edited around. Nothing new starts. - A key that started something else, another agent, another body or through
another API key, answers
409 IDEMPOTENCY_KEY_REUSED. Keys share one space across your organization, so make them specific, such as a date and a suite. - A start refused for any reason in the table above stores nothing, so the same key works once the cause is fixed, such as a busy agent being free again.
- A key outside 1 to 255 characters is
400 INVALID_IDEMPOTENCY_KEY.
Two rarer answers come after the run was written:
| Status | code | Meaning |
|---|---|---|
| 503 | RUN_NOT_STARTED | The run was written and then could not start. It is kept as a failed run, runId names it, and the key is released, so a retry with the same key starts a new run |
| 503 | CONSOLE_UNAVAILABLE | Subcore could not finish the request. With retryable: true its answer could not be read, so a run may have started: retry with the same key, which returns it if it did. With false nothing was started, and retrying changes nothing until Subcore is fixed |
A key lives as long as the run it started. Without a key, a retry after a timeout can start a second run once the first ends.
Starts have a budget of their own, beside the agents API's: 10 a minute for
each agent, per key, shared by this route and the MCP server's
start_eval_run. It is taken once a request
has passed its own checks, so a malformed body costs nothing, and a replay
counts as a start. Over it is 429 RATE_LIMITED with
scope: "eval-run-start" and Retry-After.
Stop a run
POST /agents/{slug}/evals/runs/{runId}/stopcurl -X POST -H "Authorization: Bearer $SUBCORE_API_KEY" \
"https://api.subcore.ai/agents/front-desk/evals/runs/run_4f0c…/stop"It answers 200 with { "run" }, its stopRequestedAt set. The run finishes
the case in flight, so its status stays running for up to one more case,
then completes with endReason: "stopped". Asking again answers the same, also
once it has completed that way.
| Status | code | When |
|---|---|---|
| 404 | RUN_NOT_FOUND | No run with this id on this agent |
| 409 | RUN_FINISHED | It completed before a stop was asked |
| 409 | RUN_NOT_STOPPABLE | It was reported by a local bun run evals, which nothing here controls |
The run object
{
"runId": "run_4f0c…",
"suite": "reschedules",
"source": "api",
"status": "running",
"endReason": null,
"stopRequestedAt": null,
"total": 12,
"resultsWritten": 5,
"passed": null,
"failed": null,
"currentCase": { "caseId": "reschedule-weekend", "sid": "sms-eval-…" },
"startedAt": "2026-10-30T09:20:01.554Z",
"lastActiveAt": "2026-10-30T09:23:40.012Z",
"finishedAt": null,
"deploymentIdentity": { "deploymentIds": ["…"], "configDigest": "…" }
}| Field | Description |
|---|---|
runId | Its id. A run started here or in the console is run_ and a UUID. A local run's is whatever its harness reported |
suite | The suite it ran |
source | api when a key started it, console when someone pressed Run, telemetry for a local run that reported in. Never null. A run whose starting key has since been deleted reads console |
status | running or complete |
endReason | null for a run that finished its cases, stopped, or failed |
stopRequestedAt | When a stop was asked, or null. A run that is running with this set is stopping |
total | The cases fixed at the start. It stays the same on a run that ended early, so total minus resultsWritten is the cases it never ran. A case on a channel the agent does not run is one of them: the run writes no result for it, so it is in neither passed nor failed. Two exceptions: a run whose start failed after its insert reads 0, and a telemetry run's is what its harness reported, which counts only the cases it ran |
resultsWritten | How many verdicts it has written so far. Exact when you read the run or its results. On a list page, a running run's is exact, and a complete run's is its passed plus failed (its total when the harness reported no tallies) |
passed, failed | null until it completes: the tallies are written at the end |
currentCase | The case in flight and its conversation's sid, or null |
startedAt, finishedAt | When it started, and when it completed or null |
lastActiveAt | The runner's last sign of life, before and after each agent turn |
deploymentIdentity | The builds that answered its turns, and a digest of the agent's configuration at the start. More than one build means it spanned a redeploy. When the run skipped a case because the deployment does not run its channel, it also holds channels, the channels the deployment said it runs. null on a local run |
A run fails, with endReason: "failed", when the agent's configuration or the
deployment it is reached through changes during it, or when the runner itself
fails. A case cut off that way records why in its reasoning.
List the runs
GET /agents/{slug}/evals/runs| Parameter | Description |
|---|---|
status | running or complete |
cursor | nextCursor from the previous page |
limit | 1 to 100, default 25 |
Every run of the agent, newest first: those started here, in the console, and local runs that reported in.
Read a run
GET /agents/{slug}/evals/runs/{runId}{ "run": Run }, or 404 RUN_NOT_FOUND. runId may be any 1 to 255
characters, as a local run's id can be.
Read the results
GET /agents/{slug}/evals/runs/{runId}/results{
"run": { "runId": "run_4f0c…", "status": "complete", "passed": 10, "failed": 2, … },
"items": [
{
"caseId": "reschedule-delivery",
"passed": false,
"reasoning": "Gated: expected tool calls — lookup_customer was never called",
"sid": "sms-eval-…",
"mode": null,
"createdAt": "2026-10-30T09:21:12.004Z"
}
],
"nextCursor": null
}The run comes with every page, so one call polls its status and reads its verdicts. Results are oldest first, 25 a page by default and at most 100, and without their details.
| Field | Description |
|---|---|
caseId | The case it judged |
passed | The verdict |
reasoning | Why. A case that could not run says so here, such as "Not run: …", and one that failed its expected tool calls says which. A case on a channel the agent does not run has no result at all: it counts in total only |
sid | Its conversation. Read it with GET /telemetry/sessions/{sid}, with a key that carries telemetry |
mode, createdAt | The case's mode, where a local run reported one, and when the verdict was written |
Polling
Poll GET …/runs/{runId}/results, from the first page each time, until the
run's status is complete, then page through with nextCursor until it is
null to read every verdict. Every 10 seconds is plenty: each case is a whole
conversation.
A run whose runner died without cleaning up reads running until the console
notices, which happens on the agent's next run start or Evaluations page load:
at once when its workflow is known to be gone, otherwise once the run is a day
old and three hours quiet. Watch lastActiveAt. A start that answers
RUN_IN_PROGRESS for a run you believe dead checks first, so try it again
later.
Read one result
GET /agents/{slug}/evals/runs/{runId}/result?caseId={caseId}{ "result" }: one verdict with details. For a run the console ran, details
holds the case as it ran, the models that played the customer and judged, the
agent's tool calls in order, and the judge's checklist. For a local run, it is
whatever the harness reported. The case id is a query parameter because a local
run's can be any text, up to 255 characters. Without it, or past that, the
answer is 400 VALIDATION_ERROR, and a run with no result for it is
404 CASE_NOT_FOUND.
Errors
The codes above, plus the agents API's own on
errors. 403 FORBIDDEN on a start or stop means the key
lacks the run-evals permission.
Eval cases
Write an agent's eval cases with a key, the same cases the console's Evaluations page edits. The case shape, its rules, suites, and how to create, replace, rename, archive and delete one.
Errors
One error envelope, every code the agents API and the config read can answer with, and the refusals from your deployment these pages name.