Test Plans
Test plans string multiple user journeys into a directed acyclic graph (DAG). Each step runs a journey under a named browser profile, declares the outputs it records, and binds inputs from earlier steps' outputs or plan-level variables.
Implementation Details
Test plans string multiple user journeys into a directed acyclic graph (DAG). Each node (step) runs a journey under a named browser identity, declares the outputs it records, and binds inputs from earlier steps' outputs or plan-level variables. When you start a run, Aiqaramba walks the graph, dispatching agents as their dependencies clear.
Addressing convention: URLs always reference resources by UUID, including individual steps. Within a request payload, the parents array references sibling steps by label — labels are mandatory, unique within a plan, and the runtime template language ({{ steps.<label>.outputs.<name> }}) keys by label too.
ID Stability: Step UUIDs are stable across saves with unchanged labels. Renaming a label is treated as delete-old + insert-new because every binding referencing the old label invalidates anyway.
Validation failures: Errors return RFC 7807 application/problem+json with structured per-field violations under the violations extension. Check any 422 status code in this section for the specific response shape.
Variables and outputs — the data flow
A test plan moves data between steps using a single unified model. Plan variables are injected directly into journey templates, alongside outputs produced by other steps.
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 journey can directly reference a plan variable inside its prompt_template using {{ plan.variables.<name> }}.
2. Step outputs
Each step declares the data its journey will publish for downstream steps as name → {description, type?, format?}. The journey is responsible for actually emitting these values during its run.
A journey can directly reference an ancestor step's output using {{ steps.<label>.outputs.<name> }}.
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 journey's prompt_template (running as a step dependent on signup) reads both values:
Welcome to {{ plan.variables.plan_name }}! Your user id is {{ steps.signup.outputs.user_id }}.See the User Journeys reference for the full prompt-template data model.
Common pitfalls
- Journey 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 journey or role) as a 422 problem with field-level violations.
Parameters
| Parameter | Type | In | Required | Description |
|---|---|---|---|---|
project_id | uuid | body | Yes | Project that owns the plan. Every referenced journey 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",
"journey_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",
"journey_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",
"journey_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",
"journey_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",
"journey_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",
"journey_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",
"journey_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",
"journey_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. Used as the reference key in {{ steps.<label>... }} bindings. |
journey_id | uuid | body | Yes | Journey 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_-]+$). Steps sharing a profile_name reuse the same browser within a run. |
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}. |
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",
"journey_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",
"journey_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",
"journey_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",
"journey_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). May leave the rest of the graph valid or invalid depending on whether other steps still reference its outputs; the validator surfaces any resulting 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",
"journey_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",
"journey_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