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.
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 instructions 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”. Plan variables are the only values substituted with template syntax.
{ "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.
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.
Caller-supplied values for the plan's declared variables. Unknown variable names are rejected.
tunnel
string
No
Hostname of a live tunnel owned by this tenant. Every agent in the run starts at its test's entry URL rewritten onto this host, and is instructed to substitute this host for any other URL its prompt mentions.
The request is structurally valid but cannot be processed.
500
The server could not complete the request.
503
The requested feature is not configured on this server.
Response Fields (201)
Field
Type
Description
id
uuid
plan_id
uuid
tenant_id
uuid
project_id
uuid
status
string
stopping
boolean
graph_snapshot
object
variables
object
tunnel_url
uri
node_runs
object[]
node_runs[].id
uuid
node_runs[].node_id
uuid
node_runs[].agent_id
uuid
node_runs[].status
string
node_runs[].attempt
integer
node_runs[].outputs
object
node_runs[].started_at
date-time
node_runs[].finished_at
date-time
node_runs[].created_at
date-time
started_at
date-time
finished_at
date-time
created_at
date-time
POST/api/v1/test-plans/{id}/steps
Add a plan step
Parameters
Parameter
Type
In
Required
Description
id
uuid
path
Yes
Test plan ID.
Request Body (application/json)
Field
Type
Required
Description
label
string
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
Yes
Test to run for this step. Must belong to the plan's project.
profile_name
string
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
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).
parents
string[]
No
Sibling step labels this step depends on. Empty array for entry-point steps.
outputs
json
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.
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
Yes
Test to run for this step. Must belong to the plan's project.
profile_name
string
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
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).
parents
string[]
No
Sibling step labels this step depends on. Empty array for entry-point steps.
outputs
json
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.