Skip to content

Run cases API

GET /api/v1/runs/{runUlid}/cases returns every case that belongs to an open or closed test run. Two fields were added as additive, backward-compatible extensions — existing integrations that ignore unknown keys are unaffected.

GET /api/v1/runs/{runUlid}/cases

Requires viewer+ on the run’s project. Returns 404 when the run is outside the caller’s organization.

Each item in the cases array includes the usual run-case fields plus:

FieldTypeDescription
caseLifecycleStatusobject | nullThe source case’s current (live, not snapshotted) lifecycle status. null when the status field is unset or the source case has been deleted.
customFieldValues[].optionSystemKeystring | nullThe option’s own system key (e.g. status_active, status_draft). null for custom (user-defined) options and for non-select field types.

When non-null, the object has the same shape as a CustomFieldValueEntry:

FieldTypeDescription
optionUlidstringULID of the selected lifecycle option
optionNamestringDisplay name of the option, resolved for the request locale (see below)
optionColorstring | nullHex color, if any
optionIconstring | nullIcon identifier, if any
optionSystemKeystring | nullSystem key — status_active, status_draft, status_deprecated, or null for custom options

Request locale. optionName resolves according to the request’s Accept-Language header: an organization that has translated its status option names into Spanish sees the Spanish name when it sends Accept-Language: es. An absent or unsupported locale resolves to the English name, so integrators that never send the header see byte-identical responses to before this behavior existed.

Test case content (title, steps, description) is snapshotted when the case is added to a run — edits to the source case do not affect any existing run. caseLifecycleStatus is deliberately not snapshotted: it is resolved live at query time via a join to the source case, so it always reflects the case’s current status in the repository. If you need the snapshotted state, use the priority field (which is part of the snapshot).

Terminal window
curl -sS \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://app.probara.net/api/v1/runs/01J...RUN/cases"
{
"cases": [
{
"runCaseUlid": "01J...RC",
"caseUlid": "01J...CASE",
"caseNumber": 7,
"titleSnapshot": "Checkout flow",
"status": "untested",
"caseLifecycleStatus": {
"optionUlid": "01J...OPT",
"optionName": "Draft",
"optionColor": "#F59E0B",
"optionIcon": "circle-dashed",
"optionSystemKey": "status_draft"
},
"customFieldValues": [
{
"fieldUlid": "01J...FLD",
"fieldName": "Priority",
"optionUlid": "01J...PRI",
"optionName": "High",
"optionColor": "#EF4444",
"optionIcon": null,
"optionSystemKey": null
}
]
}
]
}

When the source case has no lifecycle status set, or the case was deleted:

{
"caseLifecycleStatus": null
}

When a customFieldValues entry belongs to a system option (lifecycle status field):

{
"optionSystemKey": "status_active"
}

When it belongs to a custom option (user-defined):

{
"optionSystemKey": null
}
PATCH /api/v1/runs/{runUlid}/cases/{runCaseUlid}

Sets assigneeUlid / status / elapsedMs. A status change marks every step and appends a result row; the response’s resultUlid carries the appended attempt (null for an assignee/elapsed-only patch that changes no status). Requires member+ on the run’s project; viewer receives 403 forbidden.

Allowed on an open run or a closed, non-aborted run when the project’s allowResultsInClosedRuns test-run setting resolves to true (the code default). An organization or project that sets it to false restores the older, stricter behavior: a closed run rejects the mark with 409 conflict. An aborted run always rejects it, under every setting value.

When the project’s assigneeResultLock setting is on and the case has an assignee, only that assignee — or a caller exempt via projects.manage or the project’s current owner — may mark it. Every other caller is refused, including an API token: there is no machine-actor exemption for this lock.

{
"error": {
"code": "run_case_assignee_locked",
"message": "Only the assigned tester may record a result for this case"
}
}

When the project’s timeTracking setting is "required" and the request sets a terminal status with no elapsedMs, the mark is rejected and nothing is persisted:

{
"error": {
"code": "validation_failed",
"message": "elapsedMs is required",
"details": { "code": "elapsed_ms_required", "path": ["elapsedMs"] }
}
}

Under "off" or "optional", a supplied elapsedMs is still accepted and persisted — the setting only governs whether the web client’s time-tracking UI is shown and whether a mark with no elapsedMs is rejected, never whether a supplied value is dropped.

Bulk mark and bulk submit-result are exempt from this rejection — neither POST /runs/{runUlid}/cases/bulk-mark nor POST /runs/{runUlid}/cases/bulk-submit-result carries a per-case elapsedMs field the server could require, so timeTracking: "required" never blocks either bulk endpoint.

See Test-run settings for the full settings vocabulary and resolution chain.

PATCH /api/v1/runs/{runUlid}/cases

Reconciles a run’s case selection to a supplied desired full selection, in a single atomic transaction. Unlike POST /runs/{runUlid}/cases (add-only — appends the given case ULIDs and never removes anything), this endpoint treats absence from the desired set as a removal. Both endpoints remain available and unchanged relative to each other.

Requires member+ (the same execute gate as running the tests); viewer receives 403. Allowed on an open run or a closed, non-aborted run (completed, or auto-closed once every case had a result); rejects only an aborted run with 409, mutating nothing.

{
"caseUlids": ["01J...A", "01J...B", "01J...D"],
"caseAssignees": [{ "caseUlid": "01J...D", "assigneeUlid": "01J...USER" }]
}
FieldTypeDescription
caseUlidsstring[]The full desired selection (source case ULIDs). An empty array is valid — it removes every case from the run.
caseAssigneesarray (optional)Per-case assignee for the desired selection. A case ULID absent from this array means that case is (or becomes) unassigned — there is no explicit “clear” value.

Given the current selection and the request above, the server diffs and applies, atomically:

  • Adds each case in caseUlids not currently on the run — snapshotting its title and steps fresh, status: "untested", with the supplied assignee (if any) applied inline.
  • Removes each current case absent from caseUlids — cascading its step snapshots and result attachments, same as the single-case DELETE /runs/{runUlid}/cases/{runCaseUlid} endpoint.
  • Retains each case present in both, unchanged (titleSnapshot, status, assigneeUlid, elapsedMs).
  • Reassigns any retained case whose supplied assignee differs from its current one.

The response is the reconciled run summary (RunResponse), the same shape every other run mutation returns — it does not include the run-cases array; call GET /runs/{runUlid}/cases for the refreshed list.

Each supplied assigneeUlid must resolve to a member of the run’s organization. A cross-tenant or unknown assignee ULID returns 404 not_found — matching the existing create (POST /projects/{projectId}/runs) and per-case patch (PATCH /runs/{runUlid}/cases/{runCaseUlid}) behavior for the same field, not a 422 validation_failed. No case is added, removed, or reassigned when this happens.

Emits exactly one event per case that actually changed: run.case_added for each added case, run.case_removed for each removed case, run.case_assigned for each case whose assignee changed (including a clear, to: null) — see Audit events API for the shared metadata shapes. A case unchanged in both membership and assignee emits nothing. A newly added case’s inline assignee is carried on run.case_added and does not additionally emit run.case_assigned.

Terminal window
curl -sS -X PATCH \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"caseUlids": ["01J...A", "01J...D"]}' \
"https://app.probara.net/api/v1/runs/01J...RUN/cases"

Each of the three endpoints below caps the caseUlids array in its request body. These are abuse ceilings, not product ceilings — the server processes any accepted size without a bound-parameter failure. Submitting more than the limit returns 422 validation_failed naming the caseUlids field, and mutates nothing.

EndpointFieldLimit
POST /api/v1/runs/{runUlid}/cases (add cases)caseUlids500
PATCH /api/v1/runs/{runUlid}/cases (reconcile)caseUlids5000
POST /api/v1/projects/{projectId}/runs (create run)caseUlids5000