Test Plans
Chain several tests into a directed acyclic graph (DAG): each step runs one test under a named browser profile and declares the outputs it records. Every value recorded by a step's ancestors is provided to that step's agent automatically — prompts refer to them in plain language (e.g. "log in with the credentials from the register step"), no template syntax needed. The runner walks the graph in dependency order, runs independent steps in parallel, and shares one logged-in browser across all steps that name the same profile.
The mental model
New to test plans? Read the Test Plans guide first; it explains the concepts in plain language. This page is the API reference: the exact JSON, the addressing rules, and the behaviour you can rely on at run time.
A test plan is a directed acyclic graph (DAG) of steps. Each step runs exactly one test under a named browser profile and declares the outputs it records; every value recorded by a step's ancestors is handed to that step's agent automatically. “Acyclic” means the dependencies can never loop back on themselves. The runner needs a definite order, so a step can never (directly or indirectly) depend on itself.
When you start a run, Aiqaramba freezes the plan into a snapshot and walks the graph. A step is dispatched the moment every step it depends on has finished, so independent branches run in parallel and dependent steps run in order. If a step fails, its descendants are skipped by default (see on_parent_failure).
Addressing, IDs, and errors
Two ways to address a step. In URLs, a step is always its UUID (/test-plans/{id}/steps/{stepId}). Inside a request body, steps reference each other by label: the parents array lists parent labels, and the values an agent receives from its ancestors are namespaced by the producing step's label. Labels are mandatory, unique within a plan, and identifier-shaped (^[A-Za-z_][A-Za-z0-9_]*$, no hyphens).
Step UUIDs are stable across saves as long as the label is unchanged, so a GET → mutate → PUT round-trip preserves them. Renaming a label is treated as delete-old + insert-new, because every reference to the old label breaks anyway.
Validation errors come back as RFC 7807 application/problem+json with per-field violations under the violations extension. Any 422 in this section uses that shape. The validator runs before any data is saved, so a rejected plan never leaves a partial graph behind.
Browser profiles are actors, not steps
This is the single most important design rule, and the one people get wrong first. A profile_name (pattern ^[a-z0-9_-]+$) represents one actor, one person sitting at one browser, not one step.
Every step that names the same profile reuses the same logged-in browser session: the same cookies, local storage, and authenticated identity, carried over in dependency order. Steps with different profiles get fully isolated sessions and may run in parallel.
So the canonical shape is: one small login step per actor, with that actor's remaining steps as descendants on the same profile, each prompt starting from the already-logged-in state. (See the example plan further down, where both steps run as alice in one browser, logged in once.)
Why it matters: giving every step its own profile makes every step log in from scratch. On apps that allow only one active session per user, those parallel logins invalidate each other mid-run. The symptom is steps failing with 500s or redirects back to the login page that a retry cannot fix. Reserve distinct profiles for genuinely different actors (an admin and the teammate they invite, a buyer and a fulfilment user).
What happens when a step fails
Each step carries an on_parent_failure policy controlling what happens when a parent reaches a non-success terminal state:
skip(the default) propagates the failure: the step is skipped, and so are its descendants. This is usually what you want, because a checkout step is meaningless if signup never completed.runruns anyway once the parents are terminal. Use this for cleanup, teardown, or verification that does not actually depend on the parent succeeding.
Moving data between steps
There are two sources of data a step can use: plan-level variables (template-substituted into the prompt) and ancestor step outputs (handed to the agent automatically — no template syntax).
1. Plan-level variables
Declared on the plan as a top-level variables object: name → {description, type, default?}. A variable without a default is required, and the caller must supply it when starting a run via the variables body of POST /api/v1/test-plans/{id}/runs. Missing required variables are rejected with HTTP 422.
A test can directly reference a plan variable inside its prompt_template using {{ plan.variables.<name> }}. The runner resolves the reference to a plain string before the agent sees the prompt.
2. Step outputs
Each step declares the data its test will publish for downstream steps as name → {description, type?, format?}. The test is responsible for actually emitting these values during its run.
Every value recorded by a step's ancestors is provided to that step's agent automatically, namespaced by the producing step's label and carrying the declared description. Prompts refer to them in plain language — “log in with the credentials recorded by the register step”. Template syntax for step outputs ({{ steps… }}) is rejected by the validator.
Worked example
Plan variables:
{ "plan_name": {"type": "string", "default": "Acme Inc."} }Step signup declares outputs:
{ "user_id": {"description": "ID of the new user", "type": "string", "format": "uuid"} }The onboarding test's prompt_template (running as a step dependent on signup) injects the plan variable with template syntax and refers to the signup step's output in plain language:
Welcome to {{ plan.variables.plan_name }}! Verify the profile page shows the user id recorded by the signup step.See the Tests reference for the full prompt-template data model.
Writing prompts for steps
A step runs an ordinary test, but the test's prompt is read in the context of a plan. A few rules keep that context from biting you:
- Standalone test template variables are NOT rendered in plan runs. A test run on its own can use
{{.EntryURL}},{{.RunIndex}}, and{{.Vars.*}}. In a plan, those tokens are left untouched and the agent sees raw braces. Use literal URLs, or{{ plan.variables.<name> }}to inject values supplied at run time. - Descendant prompts assume the shared session set up by an earlier step on the same profile, but should carry a credential fallback in case of an unexpected logout (“when not signed in, log in as …”).
- End every step with an explicit verification naming an observable outcome, and keep plans to roughly eight steps. Split bigger pipelines into separate phases rather than one sprawling graph.
Common pitfalls
- A separate profile per step. The most common mistake; see Browser profiles are actors. Give each actor one profile and share it across that actor's steps; a fresh profile per step makes every step log in again.
- Test prompt references an output from a step that is not an ancestor. The validator enforces that steps only reference values from guaranteed-completed ancestors to prevent runtime race conditions.
- Required plan variable not supplied at run time.
POST /api/v1/test-plans/{id}/runsreturns HTTP 422 with the missing-variable name in the response.
Create a test plan
Creates a plan with the supplied steps + edges in one transactional save. The validator runs first and rejects structural issues (cycles, dangling references, malformed schemas) along with cross-entity issues (unknown test or role) as a 422 problem with field-level violations. Start from the canonical shape: one login step per actor, descendants sharing that actor's profile_name (see the example below — both steps run as "alice" in one browser, logged in once). Step labels are identifier-shaped (^[A-Za-z_][A-Za-z0-9_]*$ — no hyphens). Prompt rules for steps: outputs recorded by ancestor steps are provided to the agent automatically — refer to them in plain language ("the credentials from the register step"), never with template syntax ({{ steps.* }} is rejected by the validator); standalone test template variables ({{.EntryURL}}, {{.RunIndex}}, {{.Vars.*}}) are NOT rendered in plan runs — use literal URLs or {{ plan.variables.* }} instead; child prompts assume the shared session but should carry a credential fallback for unexpected logouts; end every step with an explicit Verify: naming an observable outcome; keep plans to roughly 8 steps and split bigger pipelines into phases.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
project_id | uuid | body | Yes | Project that owns the plan. Every referenced test must belong to the same project. |
name | string | body | Yes | Plan name (required, ≤120 chars). |
description | string | body | No | Optional human description (≤500 chars). |
variables | object | body | No | Plan-level variables the caller will supply at run time. Map of name → {description, type, default?}. Required variables are those without a default. |
steps | array | body | No | Initial graph. May be empty; use POST /test-plans/{id}/steps to add steps incrementally. |
Status Codes
| Code | Description |
|---|---|
201 | Plan created |
400 | Malformed JSON or missing project_id |
401 | Unauthorized |
422 | Validation failed (structural or cross-entity) |
Response Body
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Signup → onboarding → invite",
"description": "End-to-end coverage for the new-user flow.",
"variables": {
"plan_name": {"description": "Display name shown on the welcome screen", "type": "string", "default": "Acme Inc."}
},
"steps": [
{
"id": "11111111-1111-1111-1111-111111111111",
"label": "signup",
"test_id": "770e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": [],
"outputs": {
"user_id": {"description": "ID of the user created during signup", "type": "string", "format": "uuid"}
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
"label": "onboarding",
"test_id": "880e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": ["signup"],
"outputs": {}
}
],
"created_at": "2026-05-07T09:30:00Z",
"updated_at": "2026-05-07T09:30:00Z"
}/api/v1/test-plansList test plans for a project
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
project_id | uuid | query | Yes | Filter to plans in this project. Required. |
Status Codes
| Code | Description |
|---|---|
200 | OK |
400 | Missing or invalid project_id |
401 | Unauthorized |
Response Body
{
"test_plans": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Signup → onboarding → invite",
"description": "End-to-end coverage for the new-user flow.",
"variables": {
"plan_name": {"description": "Display name shown on the welcome screen", "type": "string", "default": "Acme Inc."}
},
"steps": [
{
"id": "11111111-1111-1111-1111-111111111111",
"label": "signup",
"test_id": "770e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": [],
"outputs": {
"user_id": {"description": "ID of the user created during signup", "type": "string", "format": "uuid"}
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
"label": "onboarding",
"test_id": "880e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": ["signup"],
"outputs": {}
}
],
"created_at": "2026-05-07T09:30:00Z",
"updated_at": "2026-05-07T09:30:00Z"
}
]
}/api/v1/test-plansGet a hydrated test plan
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
Status Codes
| Code | Description |
|---|---|
200 | OK |
401 | Unauthorized |
404 | Not found |
Response Body
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Signup → onboarding → invite",
"description": "End-to-end coverage for the new-user flow.",
"variables": {
"plan_name": {"description": "Display name shown on the welcome screen", "type": "string", "default": "Acme Inc."}
},
"steps": [
{
"id": "11111111-1111-1111-1111-111111111111",
"label": "signup",
"test_id": "770e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": [],
"outputs": {
"user_id": {"description": "ID of the user created during signup", "type": "string", "format": "uuid"}
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
"label": "onboarding",
"test_id": "880e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": ["signup"],
"outputs": {}
}
],
"created_at": "2026-05-07T09:30:00Z",
"updated_at": "2026-05-07T09:30:00Z"
}/api/v1/test-plans/{id}Replace a test plan
Full-document replace. The request body has the same shape as a GET response, so a typical workflow is GET → mutate → PUT. project_id and tenant_id on the existing row are preserved; the body's project_id (if any) is ignored. Step IDs you carry over from the GET response are preserved on save when the label is unchanged.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
Status Codes
| Code | Description |
|---|---|
200 | Plan updated |
401 | Unauthorized |
404 | Not found |
422 | Validation failed |
Response Body
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Signup → onboarding → invite",
"description": "End-to-end coverage for the new-user flow.",
"variables": {
"plan_name": {"description": "Display name shown on the welcome screen", "type": "string", "default": "Acme Inc."}
},
"steps": [
{
"id": "11111111-1111-1111-1111-111111111111",
"label": "signup",
"test_id": "770e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": [],
"outputs": {
"user_id": {"description": "ID of the user created during signup", "type": "string", "format": "uuid"}
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
"label": "onboarding",
"test_id": "880e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": ["signup"],
"outputs": {}
}
],
"created_at": "2026-05-07T09:30:00Z",
"updated_at": "2026-05-07T09:30:00Z"
}/api/v1/test-plans/{id}Delete a test plan
Cascades to nodes, edges, runs, node-runs, and run-profiles via FK. Historical runs survive deletion of the live plan because run rows carry their own graph_snapshot.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
Status Codes
| Code | Description |
|---|---|
204 | Deleted |
401 | Unauthorized |
404 | Not found |
/api/v1/test-plans/{id}Add a step to a plan
Convenience wrapper around PUT: loads the live plan, appends the new step, and saves. The new step's parents reference existing steps by label; the response is the freshly-saved plan with the new step's UUID assigned.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
label | string | body | Yes | Identifier-shaped label (^[A-Za-z_][A-Za-z0-9_]*$), unique within the plan. Descendant steps see this step's recorded outputs namespaced under this label, and prompts refer to the step by it. |
test_id | uuid | body | Yes | Test to run for this step. Must belong to the plan's project. |
profile_name | string | body | Yes | Browser identity for the step (^[a-z0-9_-]+$). One profile per ACTOR, shared across that actor's steps: steps sharing a profile_name reuse the same logged-in browser within a run (serialised, state carries over), so log in once in an early step and let descendants start from the logged-in state. Distinct profiles are separate sessions that may run in parallel — only use them for genuinely different actors. |
on_parent_failure | string | body | No | What to do when a parent step reaches a non-success terminal state. One of skip (default — propagate failure to descendants) or run (run regardless once parents are terminal). (default: skip) |
parents | string[] | body | No | Sibling step labels this step depends on. Empty array for entry-point steps. |
outputs | object | body | No | Map of output name → {description (required), type?, format?}. Names must match the label pattern. type ∈ {string, number, boolean, object}; format ∈ {uuid, url, email}. Values recorded here are automatically provided to every descendant step's agent. |
role_id | uuid | body | No | Optional role to attach to the dispatched agent. |
Status Codes
| Code | Description |
|---|---|
201 | Step added |
401 | Unauthorized |
404 | Plan not found |
422 | Validation failed |
Response Body
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Signup → onboarding → invite",
"description": "End-to-end coverage for the new-user flow.",
"variables": {
"plan_name": {"description": "Display name shown on the welcome screen", "type": "string", "default": "Acme Inc."}
},
"steps": [
{
"id": "11111111-1111-1111-1111-111111111111",
"label": "signup",
"test_id": "770e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": [],
"outputs": {
"user_id": {"description": "ID of the user created during signup", "type": "string", "format": "uuid"}
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
"label": "onboarding",
"test_id": "880e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": ["signup"],
"outputs": {}
}
],
"created_at": "2026-05-07T09:30:00Z",
"updated_at": "2026-05-07T09:30:00Z"
}/api/v1/test-plans/{id}/stepsUpdate a step in place
Replaces every field on the step with the supplied payload (no implicit merge). The step's previous parents are dropped and replaced with the parents array on the request. stepId is the step's UUID, which is stable across saves with unchanged labels.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
stepId | uuid | path | Yes | Step ID |
Status Codes
| Code | Description |
|---|---|
200 | Step updated |
401 | Unauthorized |
404 | Plan or step not found |
422 | Validation failed |
Response Body
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Signup → onboarding → invite",
"description": "End-to-end coverage for the new-user flow.",
"variables": {
"plan_name": {"description": "Display name shown on the welcome screen", "type": "string", "default": "Acme Inc."}
},
"steps": [
{
"id": "11111111-1111-1111-1111-111111111111",
"label": "signup",
"test_id": "770e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": [],
"outputs": {
"user_id": {"description": "ID of the user created during signup", "type": "string", "format": "uuid"}
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
"label": "onboarding",
"test_id": "880e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": ["signup"],
"outputs": {}
}
],
"created_at": "2026-05-07T09:30:00Z",
"updated_at": "2026-05-07T09:30:00Z"
}/api/v1/test-plans/{id}/steps/{stepId}Delete a step
Removes the step plus every edge touching it (inbound + outbound). Descendant steps lose access to its recorded outputs; the validator surfaces any resulting structural issues as a 422.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
stepId | uuid | path | Yes | Step ID |
Status Codes
| Code | Description |
|---|---|
200 | Step removed |
401 | Unauthorized |
404 | Plan or step not found |
422 | Removing the step left the plan invalid (e.g. a downstream step references it) |
Response Body
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Signup → onboarding → invite",
"description": "End-to-end coverage for the new-user flow.",
"variables": {
"plan_name": {"description": "Display name shown on the welcome screen", "type": "string", "default": "Acme Inc."}
},
"steps": [
{
"id": "11111111-1111-1111-1111-111111111111",
"label": "signup",
"test_id": "770e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": [],
"outputs": {
"user_id": {"description": "ID of the user created during signup", "type": "string", "format": "uuid"}
}
},
{
"id": "22222222-2222-2222-2222-222222222222",
"label": "onboarding",
"test_id": "880e8400-e29b-41d4-a716-446655440000",
"profile_name": "alice",
"on_parent_failure": "skip",
"parents": ["signup"],
"outputs": {}
}
],
"created_at": "2026-05-07T09:30:00Z",
"updated_at": "2026-05-07T09:30:00Z"
}/api/v1/test-plans/{id}/steps/{stepId}Start a run
Freezes the plan as a graph_snapshot, creates a run row, and enqueues the coordinator. variables in the body are merged with the plan's defaults; required variables (declared with no default) cause a 422 if not supplied.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
variables | object | body | No | Caller-supplied values for the plan's declared variables. Unknown variable names are rejected. |
Status Codes
| Code | Description |
|---|---|
201 | Run started |
401 | Unauthorized |
404 | Plan not found |
422 | Missing required variable or unknown variable supplied |
Response Body
{
"id": "99999999-9999-9999-9999-999999999999",
"plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"status": "running",
"stopping": false,
"graph_snapshot": { "plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "nodes": [/* frozen nodes */], "edges": [/* frozen edges */] },
"variables": {"plan_name": "Acme Inc."},
"node_runs": [
{
"id": "aaaaaaaa-1111-1111-1111-111111111111",
"node_id": "11111111-1111-1111-1111-111111111111",
"agent_id": "bbbbbbbb-1111-1111-1111-111111111111",
"status": "succeeded",
"attempt": 1,
"outputs": {"user_id": "d290f1ee-6c54-4b01-90e6-d701748f0851"},
"started_at": "2026-05-07T09:31:02Z",
"finished_at": "2026-05-07T09:34:11Z",
"created_at": "2026-05-07T09:31:00Z"
}
],
"started_at": "2026-05-07T09:31:00Z",
"created_at": "2026-05-07T09:31:00Z"
}/api/v1/test-plans/{id}/runsList runs for a plan
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Plan ID |
Status Codes
| Code | Description |
|---|---|
200 | OK |
401 | Unauthorized |
404 | Plan not found |
Response Body
{"runs": [{
"id": "99999999-9999-9999-9999-999999999999",
"plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"status": "running",
"stopping": false,
"graph_snapshot": { "plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "nodes": [/* frozen nodes */], "edges": [/* frozen edges */] },
"variables": {"plan_name": "Acme Inc."},
"node_runs": [
{
"id": "aaaaaaaa-1111-1111-1111-111111111111",
"node_id": "11111111-1111-1111-1111-111111111111",
"agent_id": "bbbbbbbb-1111-1111-1111-111111111111",
"status": "succeeded",
"attempt": 1,
"outputs": {"user_id": "d290f1ee-6c54-4b01-90e6-d701748f0851"},
"started_at": "2026-05-07T09:31:02Z",
"finished_at": "2026-05-07T09:34:11Z",
"created_at": "2026-05-07T09:31:00Z"
}
],
"started_at": "2026-05-07T09:31:00Z",
"created_at": "2026-05-07T09:31:00Z"
}]}/api/v1/test-plans/{id}/runsGet a run with node-runs inline
Returns the run plus the node_runs array so polling clients can render the whole view in one round-trip. graph_snapshot is the frozen plan definition captured at run start; do not interpret it as the current plan.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Run ID |
Status Codes
| Code | Description |
|---|---|
200 | OK |
401 | Unauthorized |
404 | Run not found |
Response Body
{
"id": "99999999-9999-9999-9999-999999999999",
"plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"status": "running",
"stopping": false,
"graph_snapshot": { "plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "nodes": [/* frozen nodes */], "edges": [/* frozen edges */] },
"variables": {"plan_name": "Acme Inc."},
"node_runs": [
{
"id": "aaaaaaaa-1111-1111-1111-111111111111",
"node_id": "11111111-1111-1111-1111-111111111111",
"agent_id": "bbbbbbbb-1111-1111-1111-111111111111",
"status": "succeeded",
"attempt": 1,
"outputs": {"user_id": "d290f1ee-6c54-4b01-90e6-d701748f0851"},
"started_at": "2026-05-07T09:31:02Z",
"finished_at": "2026-05-07T09:34:11Z",
"created_at": "2026-05-07T09:31:00Z"
}
],
"started_at": "2026-05-07T09:31:00Z",
"created_at": "2026-05-07T09:31:00Z"
}/api/v1/test-plan-runs/{id}Stop a run
Flips the run's stopping flag and wakes the coordinator. Cancellation of in-flight agents is asynchronous; the response is 202 Accepted and reflects the latest run state. Calling stop on an already-terminal run is a no-op.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
id | uuid | path | Yes | Run ID |
Status Codes
| Code | Description |
|---|---|
202 | Stop requested |
401 | Unauthorized |
404 | Run not found |
Response Body
{
"id": "99999999-9999-9999-9999-999999999999",
"plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "660e8400-e29b-41d4-a716-446655440000",
"status": "running",
"stopping": false,
"graph_snapshot": { "plan_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "nodes": [/* frozen nodes */], "edges": [/* frozen edges */] },
"variables": {"plan_name": "Acme Inc."},
"node_runs": [
{
"id": "aaaaaaaa-1111-1111-1111-111111111111",
"node_id": "11111111-1111-1111-1111-111111111111",
"agent_id": "bbbbbbbb-1111-1111-1111-111111111111",
"status": "succeeded",
"attempt": 1,
"outputs": {"user_id": "d290f1ee-6c54-4b01-90e6-d701748f0851"},
"started_at": "2026-05-07T09:31:02Z",
"finished_at": "2026-05-07T09:34:11Z",
"created_at": "2026-05-07T09:31:00Z"
}
],
"started_at": "2026-05-07T09:31:00Z",
"created_at": "2026-05-07T09:31:00Z"
}/api/v1/test-plan-runs/{id}/stop