Skip to content

The lifecycle of a case

A case is always in exactly one of five states, and every move between them is either a person’s decision or automation’s report — never both, and never silent. Each move is written to the audit log as “was → became”, with the reason when one was given.

StateMeaningWho puts a case there
draftWritten, not yet in play. Every case a person creates starts here.the person creating it
in_reviewProposed by a machine and approved from the review queue without trusting the source — waiting for a person to confirm it.the queue, on approval
activeIn play: results attach, stability is counted.a person; automation, for a draft it created; the queue, for a discovery from a trusted source
detachedAn automated case whose test is no longer in the code. Kept, with its history. Set when a run that declares itself complete (configuration.full: true — PLUNE_FULL_RUN=1 on the reporter or plune run import, since @plune-ai/cli 0.12.0) finishes with every expected test reported and this case’s source did not report it; the next result brings it back to active by itself.automation (a token)
deprecatedRetired. Taken out of play on purpose, with a reason if one was given. Nothing is deleted.a person only

state is the field; status is an older, three-word view of the same thing (draft, active, archived) that every response still carries for clients written before the five states existed. Read state — it can say more.

The table is enforced on every write, and it tells a session (a person on the dashboard) and a bearer token (automation) apart:

From ↓ · to →draftin_reviewactivedetacheddeprecated
draft=tokenperson, token—person
in_review—=person—person
active—token=tokenperson
detached——person, token=person
deprecated——person—=

Two things the table says on purpose. Only a person retires a case or brings one back: a script that could retire tests would retire the ones that inconvenience it. And a case at in_review waits for a person — approving a proposal put it there, confirming it is a second, separate yes.

A move the table forbids answers 400, and the message names what is allowed from there:

an agent cannot move a case from active to deprecated — allowed from active: in_review, active, detached

Moving a case to the state it is already in is allowed and does nothing — no audit entry, no change to stateChangedAt.

Open a case and the ⋯ menu offers exactly the moves the table allows a person from where the case stands — and nothing else:

The case isYou see
draftActivate, Retire
in_reviewConfirm, Retire
activeRetire
detachedReturn to work, Retire
deprecatedReturn to work

Each opens the same was → becomes dialog as “Move to…”, with an optional Why. What you write becomes the case’s stateReason and shows beside the state badge, with the date of the move, for as long as the case stays in that state. A blank Why sends nothing — an absent reason is not an empty line. If the server refuses the move, its words appear inside the dialog and the case is untouched.

A retired case keeps its suite, its position, its keys and its results, and “Return to work” gives it back exactly as it was. The reason of the retirement leaves the page then, but not the history: every state_change entry in the audit log carries the reason it was made with.

The list shows cases of every state by default, each with its badge; state=deprecated narrows it to the retired ones, and state=active to the ones in play.

Retiring keeps a case in play as history; deleting takes it out of the list, the tree and the counts. Its results stay in the runs they belong to — a run happened, and the evidence of it does not disappear with the case. Deleting a suite takes every suite and case beneath it in one go, and the dialog says how many before you confirm.

Nothing deleted is gone at once. Settings → Bin lists everything deleted in the last six months — suites (one row per deletion, with what went along), cases, runs — with the date each one is removed for good. Restore puts it back where it was: a suite comes back with everything that went with it; a case that went with its suite comes back with the suite, not on its own (the row says so if you try). After six months a nightly sweep removes it permanently.

The same list is GET /v1/bin, and the way back is the entity’s own restore — POST /v1/test-cases/{id}/restore, POST /v1/suites/{id}/restore, POST /v1/runs/{id}/restore — for the account that owns the project. A stranger’s request, like an unknown id, is answered 404.

GET /v1/test-cases/{id} answers with allowedStates — the states the caller may move the case to from here, per the table above and per who the caller is. The list endpoint does not carry it.

{
"id": "6f1c…f1",
"title": "Checkout keeps the total right @smoke",
"state": "active",
"stateChangedAt": "2026-09-14T10:12:41.000Z",
"allowedStates": ["deprecated"]
}

A move is a PATCH with state and, optionally, stateReason:

PATCH /v1/test-cases/6f1c…f1
{ "state": "deprecated", "stateReason": "flaky since the checkout redesign; covered by the new flow" }

stateReason must not be empty when sent. The record keeps only the last reason — a later move without one clears it from the case — while the audit log keeps every one. A status in the same body is accepted from older clients and mapped to a state; state wins if both are present.