Skip to content

Submit a batch of results to a run

POST
/v1/runs/{id}/results
curl --request POST \
--url https://beta-api.plune.ai/v1/runs/example/results \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "results": [ { "resultKey": "e2e/checkout.spec.ts::Checkout — empty cart::chromium", "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1", "rawStatus": "passed", "source": "playwright", "execution": { "startedAt": "2026-09-16T09:12:04.000Z", "finishedAt": "2026-09-16T09:12:05.310Z", "durationMs": 1310, "retry": 0, "worker": "chromium-1" }, "params": { "project": "chromium" } }, { "resultKey": "e2e/checkout.spec.ts::Checkout — coupon applied::chromium", "testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1", "rawStatus": "failed", "source": "playwright", "execution": { "startedAt": "2026-09-16T09:12:05.400Z", "finishedAt": "2026-09-16T09:12:09.021Z", "durationMs": 3621, "retry": 1, "worker": "chromium-1" }, "errorContext": "Error: expect(locator).toHaveText(\"−10 %\")\n Expected: \"−10 %\"\n Received: \"0 %\"\n at e2e/checkout.spec.ts:41:31", "params": { "project": "firefox" }, "failure": { "headline": "Error: expect(locator).toHaveText(\"−10 %\")", "steps": [ "Pay", "Apply the coupon" ], "location": { "file": "e2e/checkout.spec.ts", "line": 41, "column": 31 }, "artifacts": [ { "name": "screenshot", "contentType": "image/png" }, { "name": "error-context", "contentType": "text/markdown" }, { "name": "trace", "contentType": "application/zip" } ], "ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826" } } ] }'

Idempotent by resultKey, which is unique within the run. A repeated batch answers duplicate for every element and changes nothing; an element whose key is already held with DIFFERENT content answers conflict and names the result that holds it — a different failure included. Both are 200 with a per-element verdict — one bad element must not cost the other 499.

A failed attempt may carry failure (FailureDetailSubmission). A wrong value in it — a line that is not positive, a file named by a path of the machine, a link that is not http or https — refuses the WHOLE batch with 400, naming the element and the field, and nothing of it is stored. Size never refuses: the error text and every string of failure are stored without terminal colour codes, with credentials replaced by [redacted:<kind>] markers, and only then shortened (FailureDetail). The route takes up to 10485760 bytes, like POST /v1/runs, so a batch of long failures arrives whole.

id
required
string
Media type application/json
object
results
required
Array<object>
>= 1 items <= 500 items
object
resultKey
required

Names the result WITHIN its run — the reporter chooses it, and sending it twice is how a retried upload stays free.

string
testCaseId
required
string
status

Optional. When sent it is stored as it is and the project’s status map is not consulted. Leave it out and send rawStatus with source: the project’s map, then the source’s defaults, decide (D4).

string
Allowed values: passed failed broken blocked skipped
rawStatus

The runner’s own word, kept verbatim; status is what it maps to.

string
source

Which runner produced this — decides the mapping table used for rawStatus.

string
expectedStatus

Defaults to passed. A failure that was expected is outcome: expected, not a regression.

string
Allowed values: passed failed broken blocked skipped
execution
object
startedAt
required
string format: date-time
finishedAt
required
string format: date-time
durationMs
required
integer
retry

Which attempt this was. Anything above 0 is what makes a pass flaky.

integer
worker
string
errorContext

Never rejected for being large. Stored without terminal colour codes and with every credential replaced by a [redacted:<kind>] marker, then cut to 256 KB, head and tail.

string
params

Part of the signature: the same case with different params is a different test.

object
key
additional properties
any
attachments

Reserved for per-file links, and never returned. The files a failed attempt kept travel in failure.artifacts.

Array<object>
<= 50 items
object
key
additional properties
any
assertions
Array<object>
object
key
additional properties
any
steps

A person’s marks per step of the case (D34) — source: manual. A reporter has no steps to mark.

Array<object>
<= 200 items
object
index
required

The step’s position in the case’s steps, from 0.

integer
status
required
string
Allowed values: passed failed skipped
failure

What a reporter may say about a failed attempt, beside its error text. Every field is optional: a reporter sends the part it knows, and the run page shows the part that is there. No length or count is refused — the server sanitizes every string, then shortens (see FailureDetail). A wrong value is refused: a line that is not a positive integer, a file named by a path of the machine, a link that is not http or https.

object
headline

The first line of the error the reporter chose — on a test timeout, the error of the action that was waiting. Absent or empty, and the page takes the first non-empty line of the text instead.

string
steps

The declared steps (test.step) from the outermost to the one that failed. Absent when the test declares none.

Array<string>
location

Where the test failed: the first stack frame in the test file itself, or where the error was thrown when there is none.

object
file
required

From the repository root in posix form, the way a case’s specRef names it. A path of the machine is refused: absolute, with a drive or a scheme, from a home (~), or climbing out with ...

string
>= 1 characters /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/
line
required
integer
>= 1
column
integer
>= 1
artifacts

The files the runner kept for this attempt, by name — a name, not a link: the files live in the CI run ciUrl points to.

Array<object>
object
name
required

As the runner named it — screenshot, trace, error-context.

string
>= 1 characters
contentType
string
>= 1 characters
ciUrl

The CI run holding the artifacts. Absent outside CI: the artifacts are then named without a link.

string format: uri
/^[Hh][Tt][Tt][Pp][Ss]?:///
Examples

A reporter’s batch — a pass, and a failure with its detail

{
"results": [
{
"resultKey": "e2e/checkout.spec.ts::Checkout — empty cart::chromium",
"testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1",
"rawStatus": "passed",
"source": "playwright",
"execution": {
"startedAt": "2026-09-16T09:12:04.000Z",
"finishedAt": "2026-09-16T09:12:05.310Z",
"durationMs": 1310,
"retry": 0,
"worker": "chromium-1"
},
"params": {
"project": "chromium"
}
},
{
"resultKey": "e2e/checkout.spec.ts::Checkout — coupon applied::chromium",
"testCaseId": "6f1c2b3a-0000-4000-8000-0000000000c1",
"rawStatus": "failed",
"source": "playwright",
"execution": {
"startedAt": "2026-09-16T09:12:05.400Z",
"finishedAt": "2026-09-16T09:12:09.021Z",
"durationMs": 3621,
"retry": 1,
"worker": "chromium-1"
},
"errorContext": "Error: expect(locator).toHaveText(\"−10 %\")\n Expected: \"−10 %\"\n Received: \"0 %\"\n at e2e/checkout.spec.ts:41:31",
"params": {
"project": "firefox"
},
"failure": {
"headline": "Error: expect(locator).toHaveText(\"−10 %\")",
"steps": [
"Pay",
"Apply the coupon"
],
"location": {
"file": "e2e/checkout.spec.ts",
"line": 41,
"column": 31
},
"artifacts": [
{
"name": "screenshot",
"contentType": "image/png"
},
{
"name": "error-context",
"contentType": "text/markdown"
},
{
"name": "trace",
"contentType": "application/zip"
}
],
"ciUrl": "https://github.com/plune-ai/plune/actions/runs/35003177826"
}
}
]
}

Per-element verdicts and counts

Media type application/json
object
items
required

One verdict per submitted element, in the order they were sent.

Array<object>
object
index
required

Position in the submitted batch.

integer
resultKey
required
string
status
required
string
Allowed values: accepted duplicate conflict rejected
id

Present when accepted — the result just created.

string
existingId

Present on duplicate and conflict — the result already holding the key.

string
reason

Present when rejected.

string
counts
required
object
accepted
required
integer
duplicate
required
integer
conflict
required
integer
rejected
required
integer
Example
{
"items": [
{
"index": 0,
"resultKey": "e2e/checkout.spec.ts::Checkout — empty cart::chromium",
"status": "accepted",
"id": "6f1c2b3a-0000-4000-8000-0000000000e1"
},
{
"index": 1,
"resultKey": "e2e/checkout.spec.ts::Checkout — coupon applied::chromium",
"status": "accepted",
"id": "6f1c2b3a-0000-4000-8000-0000000000e2"
}
],
"counts": {
"accepted": 2,
"duplicate": 0,
"conflict": 0,
"rejected": 0
}
}

Body is not a batch of submittable results — the whole batch is refused and nothing of it is stored

Media type application/json
object
error
required
string
Examples

The line of the place is not a positive integer

{
"error": "validation failed — results.1.failure.location.line: Too small: expected number to be >0"
}

No / invalid token or session

Not found or not yours

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

Over 10485760 bytes — refused before anything reads the body

Media type application/json
object
error
required
string
Example
{
"error": "request body too large — this route accepts at most 10485760 bytes"
}

Rate limited