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}/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 journey or role) as a 422 problem with field-level violations.

Parameters

ParameterTypeInRequiredDescription
project_iduuidbodyYesProject that owns the plan. Every referenced journey 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",
      "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"
}
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",
      "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"
}
  ]
}
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",
      "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"
}
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",
      "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"
}
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. Used as the reference key in {{ steps.<label>... }} bindings.
journey_iduuidbodyYesJourney to run for this step. Must belong to the plan's project.
profile_namestringbodyYesBrowser identity for the step (^[a-z0-9_-]+$). Steps sharing a profile_name reuse the same browser within a run.
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}.
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",
      "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"
}
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",
      "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"
}
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). 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

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",
      "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"
}
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