Step Actions Reference
Each step in a QA plan performs one of two action types: http_request or browser. The AI agent selects and configures these actions when building QA plans via MCP tools. This page documents the full configuration schema for both.
http_request
Section titled “http_request”The http_request action sends an HTTP request and validates the response.
HttpRequestConfig
Section titled “HttpRequestConfig”| Field | Type | Required | Description |
|---|---|---|---|
method | string | Yes | HTTP method: GET, POST, PUT, PATCH, DELETE, etc. |
url | string | Yes | The request URL. Supports template variables (e.g., {{base_url}}/api/users). |
headers | Record<string, string> | No | Request headers as key-value pairs. Values support template variables. The runner sends headers exactly as written and does not auto-inject Content-Type — see Header handling. |
body | RequestBody | No | The request body. Use the discriminated form below to send any Content-Type. Legacy shorthand (plain object → JSON; string → text) is still accepted. |
auth | HttpAuth | No | Authentication helper. The runner builds the matching Authorization header — see Authentication. |
timeout | number | No | Request timeout in milliseconds. |
poll | PollConfig | No | Poll the endpoint until a condition is met. |
response_body | "auto" | "text" | "binary" | No | How to handle the response body. auto (default) decides from Content-Type. text forces UTF-8 decoding; binary forces binary handling. |
max_response_body_size | number | No | Maximum bytes to read from the response (default 52428800 = 50 MB). When exceeded, the response is truncated and body_truncated is set. |
Header handling
Section titled “Header handling”The runner sends every header exactly as written in the plan, and does not auto-inject Content-Type even when body is structured. This intentional design enables several QA cases that are otherwise impossible to express:
- Negative tests — deliberately mismatching
Content-Typeand body to verify the server rejects with400/415. - Vendor-specific MIME types —
application/vnd.api+json,application/ld+json,application/problem+json,application/cloudevents+json, etc. - Charset variants — e.g.
application/json; charset=utf-8. - Omitted
Content-Type— verify the server’s default behavior when the header is missing.
For multipart bodies, you must declare both the boundary field on the body and a matching Content-Type: multipart/form-data; boundary=<same> header — they are not auto-synchronized.
The one exception to “headers as written” is the auth field: when set, the runner generates an Authorization header for you. If you ALSO write an explicit Authorization in headers, both Authorization headers are sent on the wire — the runner does not deduplicate. This keeps conflicts visible and lets negative tests express auth misconfiguration deliberately.
Authentication
Section titled “Authentication”The auth field declares the request’s authentication intent in a structured form. The runner expands template variables in the credentials, builds the matching Authorization header, and the existing masking rules redact the header before it is recorded as an artifact. Compared to hand-rolling Authorization in headers, this keeps secrets readable in the plan and avoids the need to base64-encode credentials yourself.
type | Fields | Result |
|---|---|---|
basic | username, password | Authorization: Basic <base64(username:password)> (RFC 7617) |
bearer | token | Authorization: Bearer <token> (RFC 6750) |
All credential fields support {{variable}} template expansion so they can live in the environment file as secrets.
Basic auth:
{ "method": "GET", "url": "{{api_base_url}}/admin", "auth": { "type": "basic", "username": "{{admin_user}}", "password": "{{admin_password}}" }}Bearer token:
{ "method": "GET", "url": "{{api_base_url}}/me", "auth": { "type": "bearer", "token": "{{access_token}}" }}When auth and an explicit Authorization header are both present, both are sent on the wire (see the note in Header handling).
Request Body Formats
Section titled “Request Body Formats”The body field is a discriminated union on type. Pick the form that matches what you actually want on the wire.
type | Use for | Required fields |
|---|---|---|
json | Plain JSON (most REST APIs). The value is JSON.stringify-ed. | value (any JSON value) |
form | application/x-www-form-urlencoded form posts (login forms, OAuth token exchange). | fields: Record<string, string> |
multipart | multipart/form-data for file uploads or HTML form-style submissions. | fields?, files?, boundary? |
text | Raw text body — XML/SOAP, plain text webhooks, etc. | value: string |
binary | Raw bytes — image/PDF PUTs, application/octet-stream uploads. | one of path or content_base64 |
graphql | GraphQL query envelope (sugar over JSON). | query, optional variables, operationName |
Legacy shorthand (backwards compatible)
Section titled “Legacy shorthand (backwards compatible)”Plans written before the discriminated form are still accepted: a plain object becomes { type: "json", value: <object> }, and a plain string becomes { type: "text", value: <string> }. New plans should prefer the explicit form.
Examples
Section titled “Examples”JSON POST:
{ "method": "POST", "url": "{{api_base_url}}/users", "headers": { "Content-Type": "application/json" }, "body": { "type": "json", "value": { "name": "Alice", "email": "alice@example.com" } }}Form-urlencoded:
{ "method": "POST", "url": "{{api_base_url}}/login", "headers": { "Content-Type": "application/x-www-form-urlencoded" }, "body": { "type": "form", "fields": { "username": "{{user}}", "password": "{{password}}" } }}Multipart file upload:
{ "method": "POST", "url": "{{api_base_url}}/upload", "headers": { "Content-Type": "multipart/form-data; boundary=----aqua-b" }, "body": { "type": "multipart", "boundary": "----aqua-b", "fields": { "title": "report" }, "files": [ { "name": "file", "path": "./fixtures/sample.pdf", "filename": "sample.pdf", "content_type": "application/pdf" } ] }}Each file entry must supply exactly one of path (local file relative to cwd), content (inline UTF-8 text), or content_base64 (inline binary).
Raw text (XML / SOAP):
{ "method": "POST", "url": "{{api_base_url}}/soap", "headers": { "Content-Type": "application/xml" }, "body": { "type": "text", "value": "<envelope>...</envelope>" }}Raw binary (image PUT):
{ "method": "PUT", "url": "{{api_base_url}}/avatar", "headers": { "Content-Type": "image/png" }, "body": { "type": "binary", "path": "./fixtures/avatar.png" }}A binary body must supply exactly one of path or content_base64.
GraphQL:
{ "method": "POST", "url": "{{api_base_url}}/graphql", "headers": { "Content-Type": "application/json" }, "body": { "type": "graphql", "query": "query Q($id: ID!) { user(id: $id) { name } }", "variables": { "id": "u_1" } }}Response body handling
Section titled “Response body handling”The HTTP driver decides whether the response is text or binary based on the response Content-Type header. text/*, application/json, application/xml, application/*+json, and application/*+xml are treated as text; everything else is binary.
For binary responses:
- The body is not decoded as UTF-8. The raw bytes are kept and saved as a separate
http_response_bodyartifact (downloadable from the web UI; images and PDFs are previewable). json_pathassertions,body_containsassertions, andextractalways fail / yield nothing — useheader,body_size, orbody_hashassertions instead.- The response is read with streaming, capped at
max_response_body_size. When exceeded, the body is truncated and the assertions still run against the partial bytes.
A SHA-256 hash and byte count are computed on every response (text or binary), so body_size and body_hash assertions work uniformly.
PollConfig
Section titled “PollConfig”Some API endpoints require polling — repeating a request until a condition is met. When poll is specified, the request is repeated at a fixed interval until the until condition is satisfied or the timeout is reached.
| Field | Type | Required | Description |
|---|---|---|---|
until | object | Yes | The condition to check. Must specify one of the sub-fields below. |
until.status_code | number | No | Poll until the response status code matches this value. |
until.json_path | object | No | Poll until a JSON path in the response body matches. Contains path (string) and expected (any). |
interval_ms | number | Yes | Milliseconds to wait between each poll request. |
timeout_ms | number | Yes | Maximum total time in milliseconds before the poll is considered failed. |
Polling example — wait for an async job to complete:
{ "action": "http_request", "config": { "method": "GET", "url": "{{api_base_url}}/jobs/{{job_id}}" }, "poll": { "until": { "json_path": "$.status", "equals": "completed" }, "interval_ms": 2000, "timeout_ms": 60000 }}Extract
Section titled “Extract”The extract field captures values from the HTTP response for use in subsequent steps. Each extraction uses a JSONPath expression to locate the value in the response body. Extracted values become template variables accessible via {{variable_name}}.
{ "extract": { "user_id": "$.data.id", "user_name": "$.data.name" }}After this step executes, {{user_id}} and {{user_name}} are available in all subsequent steps.
Example
Section titled “Example”The following example creates a user, extracts the returned ID, and then retrieves the user to verify it was created correctly.
Step 1 — Create user:
{ "step_key": "create_user", "action": "http_request", "config": { "method": "POST", "url": "{{api_base_url}}/users", "headers": { "Content-Type": "application/json", "Authorization": "Bearer {{auth_token}}" }, "body": "{\"name\": \"Test User\", \"email\": \"test@example.com\"}" }, "assertions": [ { "type": "status_code", "expected": 201 }, { "type": "json_path", "expression": "$.data.name", "equals": "Test User" } ], "extract": { "created_user_id": "$.data.id" }}Step 2 — Retrieve user:
{ "step_key": "get_user", "action": "http_request", "depends_on": ["create_user"], "config": { "method": "GET", "url": "{{api_base_url}}/users/{{created_user_id}}", "headers": { "Authorization": "Bearer {{auth_token}}" } }, "assertions": [ { "type": "status_code", "expected": 200 }, { "type": "json_path", "expression": "$.data.email", "equals": "test@example.com" } ]}Step Condition
Section titled “Step Condition”The condition field on a step controls whether the step executes based on a variable’s value. If the condition is not met, the step is skipped (same behavior as depends_on or requires skips).
Condition checks are evaluated after depends_on checks. The variables referenced in conditions typically come from extract in earlier steps.
ConditionConfig
Section titled “ConditionConfig”| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | The condition type: "variable_equals" or "variable_not_equals". |
variable | string | Yes | The name of the variable to check. |
value | string | Yes | The value to compare against. |
variable_equals
Section titled “variable_equals”Execute the step only if the variable equals the specified value. If the variable is undefined, the condition is not met and the step is skipped.
{ "condition": { "type": "variable_equals", "variable": "user_role", "value": "admin" }}variable_not_equals
Section titled “variable_not_equals”Execute the step only if the variable does NOT equal the specified value. If the variable is undefined, the condition is met and the step runs.
{ "condition": { "type": "variable_not_equals", "variable": "payment_method", "value": "free" }}browser
Section titled “browser”The browser action drives a headless browser using Playwright.
BrowserConfig
Section titled “BrowserConfig”| Field | Type | Required | Description |
|---|---|---|---|
steps | BrowserStep[] | Yes | An ordered array of browser actions to execute. |
timeout_ms | number | No | Maximum time in milliseconds for the entire browser action sequence. |
Browser Step Types
Section titled “Browser Step Types”Each entry in the steps array is an object with a type field and type-specific parameters.
Navigation
Section titled “Navigation”| Type | Parameter | Description |
|---|---|---|
goto | string (URL) | Navigate to a URL. Supports template variables. |
Mouse Actions
Section titled “Mouse Actions”| Type | Parameter | Description |
|---|---|---|
click | string (CSS selector) | Click an element. |
double_click | string (CSS selector) | Double-click an element. |
hover | string (CSS selector) | Hover over an element. |
Form Input
Section titled “Form Input”| Type | Parameter | Description |
|---|---|---|
type | { selector: string, text: string } | Type text into an input field. Both fields support template variables. |
select_option | { selector: string, value: string } | Select an option in a <select> element by value. |
check | string (CSS selector) | Check a checkbox. |
uncheck | string (CSS selector) | Uncheck a checkbox. |
press | { selector: string, key: string } | Press a keyboard key while focused on an element (e.g., Enter, Tab). |
focus | string (CSS selector) | Focus an element. |
upload_file | { selector: string, path: string } | Upload a file to a file input element. |
Waiting
Section titled “Waiting”| Type | Parameter | Description |
|---|---|---|
wait_for_selector | string (CSS selector) | Wait until an element matching the selector appears in the DOM. |
wait_for_url | string (URL substring) | Wait until the page URL contains the given substring. |
Capture
Section titled “Capture”| Type | Parameter | Description |
|---|---|---|
screenshot | string (name) | Take a screenshot and save it with the given name. |
Headers
Section titled “Headers”| Type | Parameter | Description |
|---|---|---|
set_header | Record<string, string> | Set custom HTTP headers for subsequent browser requests. |
Frames
Section titled “Frames”| Type | Parameter | Description |
|---|---|---|
switch_to_frame | string (CSS selector) | Switch the execution context to an iframe identified by the selector. |
switch_to_main_frame | true | Switch back to the main frame. |
Browser Context and State
Section titled “Browser Context and State”Within a Scenario
Section titled “Within a Scenario”The browser context is shared across all steps within a single scenario. This means:
- Login sessions persist across steps.
- Cookies and localStorage are preserved.
- Navigation history is maintained.
Across Scenarios
Section titled “Across Scenarios”The browser’s storageState (cookies and localStorage) is carried over from one scenario to the next. This allows you to perform a login in an early scenario and have that session available in all subsequent scenarios without repeating the authentication flow.
Example
Section titled “Example”The following example tests a login flow and verifies the user lands on the dashboard.
Step 1 — Login:
{ "step_key": "login", "action": "browser", "config": { "steps": [ { "type": "goto", "url": "{{web_base_url}}/login" }, { "type": "type", "selector": "#email", "text": "{{test_email}}" }, { "type": "type", "selector": "#password", "text": "{{test_password}}" }, { "type": "click", "selector": "button[type='submit']" }, { "type": "wait_for_url", "url": "/dashboard" } ], "timeout_ms": 15000 }, "assertions": [ { "type": "url_contains", "expected": "/dashboard" }, { "type": "element_visible", "selector": "[data-testid='welcome-message']" } ]}Step 2 — Verify dashboard content:
{ "step_key": "verify_dashboard", "action": "browser", "depends_on": ["login"], "config": { "steps": [ { "action": "wait_for_selector", "selector": "[data-testid='user-name']" } ] }, "assertions": [ { "type": "element_text", "selector": "[data-testid='user-name']", "expected": "Test User" }, { "type": "element_visible", "selector": "[data-testid='recent-activity']" } ]}