Skip to content

Ingest a Cairn run

POST
/v1/ingest/cairn
curl --request POST \
--url https://beta-api.plune.ai/v1/ingest/cairn \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "schemaVersion": 1, "run": { "runId": "cairn-2026-08-30-0001", "mode": "api", "target": "https://shop.example/api" }, "cases": [ { "stableId": "9f2a4c1e", "title": "health check answers 200", "execution": "auto", "steps": [ "GET /health" ], "expected": "HTTP 200", "verdict": "passed", "technique": "happy-path" }, { "stableId": "3b7d0e55", "title": "order with a negative quantity is refused", "execution": "auto", "steps": [ "POST /orders" ], "expected": "HTTP 422" } ] }'

Where plune ingest <dir> sends a Cairn run after reading it off disk (ADR-CI-01) — the client is @plune-ai/cli, and the dashboard never calls this surface. Generated cases NEVER become test cases here — they are queued as proposals for a human (ADR-CI-03), so ADR 0010 holds. Idempotent per project on run.runId: a repeat writes nothing and replays the same counters.

Media type application/json
object
schemaVersion
required
integer
Allowed values: 1
run
required
object
runId
required

Idempotency key — the same run ingested twice changes nothing.

string
mode
required
string
Allowed values: explore design api
target
required

Page URL or API base URL that was tested.

string
cost
object
tokens
required
integer
cases
required
integer
cases
required
Array<object>
object
stableId
required

Cairn’s content-derived identity — the dedup key.

string
title
required
string
<= 300 characters
execution
required

Automatable, or a person runs it.

string
Allowed values: auto manual
steps
required
Array<string>
>= 1 items <= 20 items
expected
required
string
<= 2000 characters
verdict

ABSENT means the run never executed this case (a design run writes cases and runs nothing) — not “no result yet”. Only an executed case can become a Result.

string
Allowed values: passed failed
technique
string
<= 32 characters
priority
string
source
object
kind
required
string
Allowed values: url openapi-operation requirement
value
required
string
>= 1 characters <= 100 characters
rationale

Why the agent thinks this case is worth having. Read by opening an item: at 400 characters it does not fit a queue row.

string
>= 1 characters <= 400 characters
externalKeys

Identifiers this test already has elsewhere, so the queue can say which existing case a proposal looks like. An unknown kind refuses the WHOLE ingest and names the accepted ones: a partial acceptance would leave the agent believing it had sent something matchable, and the only symptom would be an entry with no comparison — indistinguishable from a case that genuinely is new.

Array<object>
<= 32 items

An identifier another tool already uses for this test, so a report can find its case without anyone typing a Plune id. The order of kind is load-bearing: it is the ranking that decides which key wins when several match different cases.

object
kind
required
string
Allowed values: playwright-id path-title allure-history cairn-stable eval-id qase testrail
value
required
string
<= 1024 characters
Example
{
"schemaVersion": 1,
"run": {
"runId": "cairn-2026-08-30-0001",
"mode": "api",
"target": "https://shop.example/api"
},
"cases": [
{
"stableId": "9f2a4c1e",
"title": "health check answers 200",
"execution": "auto",
"steps": [
"GET /health"
],
"expected": "HTTP 200",
"verdict": "passed",
"technique": "happy-path"
},
{
"stableId": "3b7d0e55",
"title": "order with a negative quantity is refused",
"execution": "auto",
"steps": [
"POST /orders"
],
"expected": "HTTP 422"
}
]
}

Already recorded — the same answer as the first time

Media type application/json

The three counters always add up to the number of cases sent, so a caller can see nothing was dropped.

object
runId
required

The run row this ingest recorded.

string format: uuid
linked
required

Verdicts attached to cases the project already has.

integer
proposed
required

Cases queued for a human to approve — none were created.

integer
skipped
required

Already queued, already rejected, repeated, or never executed.

integer
Example generated
{
"runId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"linked": 1,
"proposed": 1,
"skipped": 1
}

Recorded

Media type application/json

The three counters always add up to the number of cases sent, so a caller can see nothing was dropped.

object
runId
required

The run row this ingest recorded.

string format: uuid
linked
required

Verdicts attached to cases the project already has.

integer
proposed
required

Cases queued for a human to approve — none were created.

integer
skipped
required

Already queued, already rejected, repeated, or never executed.

integer
Example generated
{
"runId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"linked": 1,
"proposed": 1,
"skipped": 1
}

Unreadable body, or a payload version this server does not know

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

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.

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

Seconds until the window rolls over