Start a run, or ingest a finished eval run
const url = 'https://beta-api.plune.ai/v1/runs';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"schemaVersion":2,"kind":"automated","externalKey":"gh-35003177826","title":"plune · 2026-09-16 09:12 · ci","environment":"staging","labels":["plune","vitest","ci"],"configuration":{"expected":[{"testCaseId":"6f1c2b3a-0000-4000-8000-0000000000c1"},{"externalKey":{"kind":"path-title","value":"e2e/checkout.spec.ts > Checkout — coupon applied"}}],"full":true},"meta":{"sha":"3e31591","branch":"master","ciUrl":"https://github.com/plune-ai/plune/actions/runs/35003177826","runner":"playwright"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://beta-api.plune.ai/v1/runs \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "schemaVersion": 2, "kind": "automated", "externalKey": "gh-35003177826", "title": "plune · 2026-09-16 09:12 · ci", "environment": "staging", "labels": [ "plune", "vitest", "ci" ], "configuration": { "expected": [ { "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1" }, { "externalKey": { "kind": "path-title", "value": "e2e/checkout.spec.ts > Checkout — coupon applied" } } ], "full": true }, "meta": { "sha": "3e31591", "branch": "master", "ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826", "runner": "playwright" } }'One route, two bodies, told apart by schemaVersion. 2 — RunStart: the reporter (@plune-ai/playwright, plune run start, plune run import) opens a run or joins the one that already carries this externalKey, then streams results to POST /v1/runs/{id}/results and closes it with POST /v1/runs/{id}/events. 1 — RunResult: plune sync uploads a finished eval run in one piece, results included. The dashboard never calls this surface — it reads what has already arrived. That boundary is ADR 0010: Cairn writes files and knows nothing of the platform, the CLI uploads them, and one published package validates both sides of the contract. meta.ciUrl must be an http or https address — any other refuses the start and opens no run; a shard that joins changes nothing of meta. A run counts against the daily quota (a rolling 24 hours, one count however the run arrived: a start, plune sync, a Cairn ingest or a plan launch). A call that would open a run over it answers 429; a shard that joins a run already carrying its externalKey opens none, spends nothing and is never refused for it.
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”Create a run, or join the one already carrying this key. Sixteen shards sending this get one run.
object
What shards meet on — typically git sha + CI run id. Unique within the project.
What the run expects to happen. Without it, a crashed shard is indistinguishable from a green run.
object
object
object
object
The caller states that expected is everything its source has — the whole suite, not one file or a --grep. When such a run finishes with an empty notRun, every active case that this source (the source of the run’s results) reported before and did not report in this run moves to detached, with the run named as the reason. Manual cases, other sources, in_review and deprecated cases are never touched; a later result on a detached case brings it back to active by itself. Set it only for the run that covers the whole suite (PLUNE_FULL_RUN=1 in the CLI).
Who is to run which case (B8-team T4): testCaseId → userId. Filled at launch by assignment: prefer-assignee from each expected case’s assigneeId at that moment — a snapshot, so a case that changes hands later does not rewrite a run under way. Cases nobody holds are absent. May also be sent as is.
object
object
The commit the run was made from — run.sha in GET /v1/runs/{id}/summary.
The branch it was made on — run.branch in GET /v1/runs/{id}/summary.
The CI run that produced the report — the reporter reads the runner’s own config.metadata.ci.buildHref. Sanitized before it is stored (a credential in a query parameter becomes a marker); set by the shard that opens the run and never changed by one that joins it. The run page links it from GET /v1/runs/{id}/summary.
The tool that reported the run — playwright, or the report format plune run import read — not the machine of the CI; run.runner in GET /v1/runs/{id}/summary.
The plan the run was launched from (D33), when it was — what GET /v1/runs?planId= filters by.
What to call this run in a list. The first shard to declare one wins; a shard that arrives later fills a field left empty rather than overwriting.
Where it ran — staging, prod, a preview. Filterable on GET /v1/runs, so two CI matrices read as two histories.
How the run is marked — smoke, nightly, a team name. An empty array is how every mark is removed.
How configuration.assignees is filled (B8-team T4): none — as sent; prefer-assignee — from each expected case’s assignee now, an empty map when nobody holds any. Not stored: the outcome is. Only the shard that creates the run is heard.
CLI run output (plune run --format json). The server validates the full contract.
object
object
object
object
object
object
object
object
object
Examples
schemaVersion 2 — open (or join) a run before its results arrive
{ "schemaVersion": 2, "kind": "automated", "externalKey": "gh-35003177826", "title": "plune · 2026-09-16 09:12 · ci", "environment": "staging", "labels": [ "plune", "vitest", "ci" ], "configuration": { "expected": [ { "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1" }, { "externalKey": { "kind": "path-title", "value": "e2e/checkout.spec.ts > Checkout — coupon applied" } } ], "full": true }, "meta": { "sha": "3e31591", "branch": "master", "ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826", "runner": "playwright" }}schemaVersion 2 — a person’s run, handed out by each case’s assignee (B8-team T4)
{ "schemaVersion": 2, "kind": "manual", "title": "Release regression · 2026-09-18", "configuration": { "expected": [ { "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1" } ] }, "assignment": "prefer-assignee"}schemaVersion 1 — a finished eval run, in one piece
{ "schemaVersion": 1, "plune_version": "0.6.0", "started_at": "2026-08-30T09:12:04.000Z", "finished_at": "2026-08-30T09:12:41.522Z", "config_hash": "a3f1c9", "summary": { "total": 12, "passed": 11, "failed": 1, "errored": 0, "cost_usd": 0.0417, "duration_ms": 37522 }, "evals": []}Responses
Section titled “ Responses ”Joined — a run already carried this externalKey; joined: true, and the shard reports into it
What POST /v1/runs answers to a RunStart: the run, and whether this call created it or found it.
object
object
The value rerun is reached only by joining a terminated run again — it is never requested.
What the run expects to happen. Without it, a crashed shard is indistinguishable from a green run.
object
object
object
object
The caller states that expected is everything its source has — the whole suite, not one file or a --grep. When such a run finishes with an empty notRun, every active case that this source (the source of the run’s results) reported before and did not report in this run moves to detached, with the run named as the reason. Manual cases, other sources, in_review and deprecated cases are never touched; a later result on a detached case brings it back to active by itself. Set it only for the run that covers the whole suite (PLUNE_FULL_RUN=1 in the CLI).
Who is to run which case (B8-team T4): testCaseId → userId. Filled at launch by assignment: prefer-assignee from each expected case’s assigneeId at that moment — a snapshot, so a case that changes hands later does not rewrite a run under way. Cases nobody holds are absent. May also be sent as is.
object
object
The commit the run was made from — run.sha in GET /v1/runs/{id}/summary.
The branch it was made on — run.branch in GET /v1/runs/{id}/summary.
The CI run that produced the report — the reporter reads the runner’s own config.metadata.ci.buildHref. Sanitized before it is stored (a credential in a query parameter becomes a marker); set by the shard that opens the run and never changed by one that joins it. The run page links it from GET /v1/runs/{id}/summary.
The tool that reported the run — playwright, or the report format plune run import read — not the machine of the CI; run.runner in GET /v1/runs/{id}/summary.
The plan the run was launched from (D33), when it was — what GET /v1/runs?planId= filters by.
What to call this run in a list. The first shard to declare one wins; a shard that arrives later fills a field left empty rather than overwriting.
Where it ran — staging, prod, a preview. Filterable on GET /v1/runs, so two CI matrices read as two histories.
How the run is marked — smoke, nightly, a team name. An empty array is how every mark is removed.
What a run amounted to, frozen at finish. Distinct from RunSummary, which is the list-item shape.
object
One entry per status that occurred; an absent key means zero.
object
Expected and never reported — what keeps a dead shard from looking green.
Who launched it (B8-team T3): the userId of the session, or of the owner of the token that started or synced it — the first shard to create the run; a joiner is not recorded. null for a run from before names were kept. Resolve it to a name through GET /v1/members.
false for the shard that created the run (201), true for every one that joined it (200).
Example
{ "run": { "id": "6f1c2b3a-0000-4000-8000-0000000000d1", "projectId": "u-shop::default", "kind": "automated", "externalKey": "gh-35003177826", "statusEvent": "launched", "configuration": { "full": true, "expected": [ { "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1" }, { "externalKey": { "kind": "path-title", "value": "e2e/checkout.spec.ts > Checkout — coupon applied" } } ] }, "meta": { "sha": "3e31591", "branch": "master", "ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826", "runner": "playwright" }, "startedAt": "2026-09-16T09:12:04.151Z", "finishedAt": null, "terminateReason": null, "summary": null, "title": "plune · 2026-09-16 09:12 · ci", "environment": "staging", "labels": [ "plune", "vitest", "ci" ], "createdBy": "u-shop" }, "joined": true}Created — the run this shard opened (RunStarted, schemaVersion 2) or the eval run as recorded (RunCreated, schemaVersion 1)
What POST /v1/runs answers to a RunStart: the run, and whether this call created it or found it.
object
object
The value rerun is reached only by joining a terminated run again — it is never requested.
What the run expects to happen. Without it, a crashed shard is indistinguishable from a green run.
object
object
object
object
The caller states that expected is everything its source has — the whole suite, not one file or a --grep. When such a run finishes with an empty notRun, every active case that this source (the source of the run’s results) reported before and did not report in this run moves to detached, with the run named as the reason. Manual cases, other sources, in_review and deprecated cases are never touched; a later result on a detached case brings it back to active by itself. Set it only for the run that covers the whole suite (PLUNE_FULL_RUN=1 in the CLI).
Who is to run which case (B8-team T4): testCaseId → userId. Filled at launch by assignment: prefer-assignee from each expected case’s assigneeId at that moment — a snapshot, so a case that changes hands later does not rewrite a run under way. Cases nobody holds are absent. May also be sent as is.
object
object
The commit the run was made from — run.sha in GET /v1/runs/{id}/summary.
The branch it was made on — run.branch in GET /v1/runs/{id}/summary.
The CI run that produced the report — the reporter reads the runner’s own config.metadata.ci.buildHref. Sanitized before it is stored (a credential in a query parameter becomes a marker); set by the shard that opens the run and never changed by one that joins it. The run page links it from GET /v1/runs/{id}/summary.
The tool that reported the run — playwright, or the report format plune run import read — not the machine of the CI; run.runner in GET /v1/runs/{id}/summary.
The plan the run was launched from (D33), when it was — what GET /v1/runs?planId= filters by.
What to call this run in a list. The first shard to declare one wins; a shard that arrives later fills a field left empty rather than overwriting.
Where it ran — staging, prod, a preview. Filterable on GET /v1/runs, so two CI matrices read as two histories.
How the run is marked — smoke, nightly, a team name. An empty array is how every mark is removed.
What a run amounted to, frozen at finish. Distinct from RunSummary, which is the list-item shape.
object
One entry per status that occurred; an absent key means zero.
object
Expected and never reported — what keeps a dead shard from looking green.
Who launched it (B8-team T3): the userId of the session, or of the owner of the token that started or synced it — the first shard to create the run; a joiner is not recorded. null for a run from before names were kept. Resolve it to a name through GET /v1/members.
false for the shard that created the run (201), true for every one that joined it (200).
The stored run, plus how many of its evals found a test case to attach to (ADR 0010). An unmatched eval is counted, never turned into a new case.
object
Evals whose id matched a case — one Result each.
Evals no case claims. Set evalId on a case to link.
Example
{ "run": { "id": "6f1c2b3a-0000-4000-8000-0000000000d1", "projectId": "u-shop::default", "kind": "automated", "externalKey": "gh-35003177826", "statusEvent": "launched", "configuration": { "full": true, "expected": [ { "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1" }, { "externalKey": { "kind": "path-title", "value": "e2e/checkout.spec.ts > Checkout — coupon applied" } } ] }, "meta": { "sha": "3e31591", "branch": "master", "ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826", "runner": "playwright" }, "startedAt": "2026-09-16T09:12:04.151Z", "finishedAt": null, "terminateReason": null, "summary": null, "title": "plune · 2026-09-16 09:12 · ci", "environment": "staging", "labels": [ "plune", "vitest", "ci" ], "createdBy": "u-shop" }, "joined": false}Invalid body — field-level detail in error
object
Examples
The link of the run is not a web address
{ "error": "validation failed — meta.ciUrl: must be an http or https address"}No / invalid token or session
Rate-limited: either the per-route throttle or a tenant quota. Retry-After carries the seconds until the window rolls over.
object
Example generated
{ "error": "example"}Headers
Section titled “Headers ”Seconds until the window rolls over