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.
  • run runs 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}/runs returns HTTP 422 with the missing-variable name in the response.
POST /api/v1/test-plans

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

ParameterTypeInRequiredDescription
project_iduuidbodyYesProject that owns the plan. Every referenced test must belong to the same project.
namestringbodyYesPlan name (required, ≤120 chars).
descriptionstringbodyNoOptional human description (≤500 chars).
variablesobjectbodyNoPlan-level variables the caller will supply at run time. Map of name → {description, type, default?}. Required variables are those without a default.
stepsarraybodyNoInitial graph. May be empty; use POST /test-plans/{id}/steps to add steps incrementally.

Status Codes

CodeDescription
201Plan created
400Malformed JSON or missing project_id
401Unauthorized
422Validation 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"
}
POST /api/v1/test-plans
cURL
Response
GET /api/v1/test-plans

List test plans for a project

Parameters

ParameterTypeInRequiredDescription
project_iduuidqueryYesFilter to plans in this project. Required.

Status Codes

CodeDescription
200OK
400Missing or invalid project_id
401Unauthorized

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"
}
  ]
}
GET /api/v1/test-plans
cURL
Response
GET /api/v1/test-plans/{id}

Get a hydrated test plan

Parameters

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID

Status Codes

CodeDescription
200OK
401Unauthorized
404Not 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"
}
GET /api/v1/test-plans/{id}
cURL
Response
PUT /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

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID

Status Codes

CodeDescription
200Plan updated
401Unauthorized
404Not found
422Validation 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"
}
PUT /api/v1/test-plans/{id}
cURL
Response
DELETE /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

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID

Status Codes

CodeDescription
204Deleted
401Unauthorized
404Not found
DELETE /api/v1/test-plans/{id}
cURL
Response
POST /api/v1/test-plans/{id}/steps

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

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID
labelstringbodyYesIdentifier-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_iduuidbodyYesTest to run for this step. Must belong to the plan's project.
profile_namestringbodyYesBrowser 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_failurestringbodyNoWhat 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)
parentsstring[]bodyNoSibling step labels this step depends on. Empty array for entry-point steps.
outputsobjectbodyNoMap 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_iduuidbodyNoOptional role to attach to the dispatched agent.

Status Codes

CodeDescription
201Step added
401Unauthorized
404Plan not found
422Validation 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"
}
POST /api/v1/test-plans/{id}/steps
cURL
Response
PATCH /api/v1/test-plans/{id}/steps/{stepId}

Update 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

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID
stepIduuidpathYesStep ID

Status Codes

CodeDescription
200Step updated
401Unauthorized
404Plan or step not found
422Validation 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"
}
PATCH /api/v1/test-plans/{id}/steps/{stepId}
cURL
Response
DELETE /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

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID
stepIduuidpathYesStep ID

Status Codes

CodeDescription
200Step removed
401Unauthorized
404Plan or step not found
422Removing 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"
}
DELETE /api/v1/test-plans/{id}/steps/{stepId}
cURL
Response
POST /api/v1/test-plans/{id}/runs

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

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID
variablesobjectbodyNoCaller-supplied values for the plan's declared variables. Unknown variable names are rejected.

Status Codes

CodeDescription
201Run started
401Unauthorized
404Plan not found
422Missing 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"
}
POST /api/v1/test-plans/{id}/runs
cURL
Response
GET /api/v1/test-plans/{id}/runs

List runs for a plan

Parameters

ParameterTypeInRequiredDescription
iduuidpathYesPlan ID

Status Codes

CodeDescription
200OK
401Unauthorized
404Plan 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"
}]}
GET /api/v1/test-plans/{id}/runs
cURL
Response
GET /api/v1/test-plan-runs/{id}

Get 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

ParameterTypeInRequiredDescription
iduuidpathYesRun ID

Status Codes

CodeDescription
200OK
401Unauthorized
404Run 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"
}
GET /api/v1/test-plan-runs/{id}
cURL
Response
POST /api/v1/test-plan-runs/{id}/stop

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

ParameterTypeInRequiredDescription
iduuidpathYesRun ID

Status Codes

CodeDescription
202Stop requested
401Unauthorized
404Run 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"
}
POST /api/v1/test-plan-runs/{id}/stop
cURL
Response