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.
The five states
Section titled “The five states”| State | Meaning | Who puts a case there |
|---|---|---|
draft | Written, not yet in play. Every case a person creates starts here. | the person creating it |
in_review | Proposed by a machine and approved from the review queue without trusting the source — waiting for a person to confirm it. | the queue, on approval |
active | In play: results attach, stability is counted. | a person; automation, for a draft it created; the queue, for a discovery from a trusted source |
detached | An automated case whose test is no longer in the code. Kept, with its history. | automation (a token) |
deprecated | Retired. 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.
Who may move a case where
Section titled “Who may move a case where”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 → | draft | in_review | active | detached | deprecated |
|---|---|---|---|---|---|
draft | = | token | person, token | — | person |
in_review | — | = | person | — | person |
active | — | token | = | token | person |
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, detachedMoving a case to the state it is already in is allowed and does nothing — no audit entry, no change
to stateChangedAt.
From the case page
Section titled “From the case page”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 is | You see |
|---|---|
draft | Activate, Retire |
in_review | Confirm, Retire |
active | Retire |
detached | Return to work, Retire |
deprecated | Return 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.
Through the API
Section titled “Through the API”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.