Script API reference
What you can call from inside a request script, in both supported languages, and what QAClan hands you to work with.
Most checks need no code — see assertions and extractor rules below for the no-code equivalents of the two most common jobs. This page is for the cases where logic is genuinely required.
Where scripts run
A request can carry two scripts. Each is written in JavaScript or Python, chosen per script — a pre-script in Python and a post-script in JavaScript on the same request is fine. JavaScript is the default.
| Script | Runs | Typically used to |
|---|---|---|
| Pre-request | After variables resolve, before the request is sent | Compute a signature, stamp a timestamp, set a header or query parameter |
| Post-request | After the response arrives, after extractor rules have run | Pull a value out for the next request, assert something an operator cannot express |
The order of a request
{{variables}}are resolved from the environment and collection.- Pre-request extractor rules run.
- The pre-request script runs. Its header and parameter changes are merged in.
- The body is built and the request is sent.
- Post-response extractor rules run.
- The post-request script runs, so it can override anything an extractor rule just set.
- Assertions are evaluated, with any script assertions appended to them.
A request passes when every assertion passes. When it has no assertions at all, it passes on a status below 400.
A pre-request script has no response to read
The response object exists in a pre-request script but is empty, because nothing has been sent yet. Reading it there produces nothing useful, and response.json() on an empty body raises.
To use the previous request's response, store what you need with qc.set in its post-script and read it back as a variable.
The qc bindings
qc is how you change the request and record results. Python uses snake_case, JavaScript uses camelCase. Only set, expect and test are spelled the same in both.
| Python | JavaScript | Does |
|---|---|---|
qc.set(k, v) | qc.set(k, v) | Stores a value in shared run state, where later requests read it as {{k}} |
qc.set_header(k, v) | qc.setHeader(k, v) | Adds or replaces a request header |
qc.set_param(k, v) | qc.setParam(k, v) | Adds or replaces a query parameter |
qc.get_header(k, default=None) | qc.getHeader(k, d) | Reads a header already staged on the request. Case-insensitive. Returns the default when absent. |
qc.get_param(k, default=None) | qc.getParam(k, d) | Reads a staged query parameter. Case-sensitive. Returns the default when absent. |
qc.expect(condition, message) | qc.expect(condition, message) | Records one pass or fail. The message is the label shown in results; it defaults to assertion failed. |
qc.test(name, fn) | qc.test(name, fn) | Runs fn and records one named result. An exception inside fn becomes a failure carrying the error message, rather than aborting the script. |
qc.get_header and qc.get_param read what is currently staged on the outgoing request, not what came back. Response headers are on response.headers.
expect fails the request; test isolates the failure
Both record a result. The difference is what happens when your own code throws: qc.expect is a plain call, so an exception before it aborts the whole script and every effect is discarded. qc.test catches the exception and turns it into a single failed assertion, letting the rest of the script run.
Use qc.test when the check itself might throw — parsing, indexing, casting.
What is handed to your script
Alongside qc, these are available without importing anything.
| Value | Python | JavaScript |
|---|---|---|
| Environment variables | env — a dict | env — an object |
| Response body, parsed | response.json() | response.json() |
| Response body, raw | response.text() | response.text() |
| Response headers | response.headers | response.headers |
| Response status | status_code — a bare variable | response.status |
| Raw body, as a variable | response_body | — use response.text() |
| Raw headers, as a variable | response_headers | — use response.headers |
Status is the one thing that differs between the languages
JavaScript has response.status. Python does not. In Python the status is a separate variable called status_code.
This is the mistake people make when porting a script between the two. Everything else lines up; this does not.
python
qc.expect(status_code == 200, "expected 200") # correct
# qc.expect(response.status == 200, ...) # AttributeErrorjavascript
qc.expect(response.status === 200, "expected 200"); // correct
// qc.expect(status_code === 200, ...); // ReferenceErrorExecution limits
Each script runs in a separate process, so it cannot interfere with the run around it. Three consequences worth knowing:
- 30 seconds. A script running longer is stopped.
- Failure is silent and total. If the script errors, exceeds the timeout, or exits non-zero, every effect it had is discarded — headers, parameters, stored values and assertions alike. The request then carries on as though the script were not there.
- The runtime is the bundled one.Python runs under the agent's own interpreter and JavaScript under its bundled Node, so your system versions are irrelevant.
If a script seems to do nothing, it errored
Because a failing script is discarded rather than surfaced as a red assertion, “my header is not being set” usually means the script threw before reaching that line. Wrap the body in qc.test to turn the exception into a visible failed assertion.
Worked examples
Chaining a value into the next request
The most common reason to write a post-script. Sign in, keep the token, and let every later request use it as {{auth_token}}.
post-request · javascript
const body = response.json();
qc.set("auth_token", body.access_token);
qc.set("user_id", body.user.id);post-request · python
body = response.json()
qc.set("auth_token", body["access_token"])
qc.set("user_id", body["user"]["id"])A later request now uses {{auth_token}} in a header or {{user_id}} in its path, exactly like an environment variable. Values set this way persist for the rest of the run.
Asserting something an operator cannot express
Use qc.expect for a condition you can state in one line, and qc.test when the check might throw.
post-request · javascript
const orders = response.json().orders;
qc.expect(orders.length > 0, "expected at least one order");
qc.test("orders are newest first", () => {
const dates = orders.map(o => new Date(o.created_at).getTime());
const sorted = [...dates].sort((a, b) => b - a);
if (JSON.stringify(dates) !== JSON.stringify(sorted)) {
throw new Error("orders are not sorted by created_at descending");
}
});
qc.test("every order totals correctly", () => {
for (const o of orders) {
const sum = o.items.reduce((t, i) => t + i.price * i.qty, 0);
if (Math.abs(sum - o.total) > 0.01) {
throw new Error(`order ${o.id}: items sum to ${sum}, total says ${o.total}`);
}
}
});post-request · python
orders = response.json()["orders"]
qc.expect(len(orders) > 0, "expected at least one order")
qc.expect(status_code == 200, "expected 200")
def totals_match():
for o in orders:
total = sum(i["price"] * i["qty"] for i in o["items"])
assert abs(total - o["total"]) < 0.01, \
f"order {o['id']}: items sum to {total}, total says {o['total']}"
qc.test("every order totals correctly", totals_match)Each qc.expect and qc.test appears as its own line in the results, beside the no-code assertions, using the message or name you gave it. Write those as statements of what should be true — they are what somebody reads when the run goes red.
Setting a header and a parameter before sending
A pre-request script is for anything the request cannot express as a static value — a timestamp, a nonce, a computed signature.
pre-request · javascript
const ts = String(Math.floor(Date.now() / 1000));
qc.setHeader("X-Request-Time", ts);
qc.setHeader("X-Idempotency-Key", `order-${ts}`);
qc.setParam("trace", "qaclan");
// Only add the debug header when the environment asks for it.
if (env.DEBUG_MODE === "true") {
qc.setHeader("X-Debug", "1");
}pre-request · python
import time
ts = str(int(time.time()))
qc.set_header("X-Request-Time", ts)
qc.set_header("X-Idempotency-Key", f"order-{ts}")
qc.set_param("trace", "qaclan")
if env.get("DEBUG_MODE") == "true":
qc.set_header("X-Debug", "1")env holds the variables of whichever environment the run selected, so one script behaves differently against staging and production without being edited.
Reading what is already staged
qc.get_header is case-insensitive, so you do not have to match how the header was originally spelled.
pre-request · javascript
// Matches Authorization, authorization or AUTHORIZATION.
const auth = qc.getHeader("authorization", "");
if (!auth) {
qc.setHeader("Authorization", `Bearer ${env.FALLBACK_TOKEN}`);
}
const page = qc.getParam("page", "1"); // case-sensitive
qc.setParam("offset", String((Number(page) - 1) * 50));Extractor rules — pulling a value out without code
If all you want is one value from the response, you do not need a script. A request can carry extractor rules, each with three fields.
| Field | Meaning |
|---|---|
path | Where to look in the JSON response, as a dot-path — data.user.id |
name | What to store it as. Surrounding {{ }} are stripped, so either spelling works. |
prefix | Text put in front of the value — Bearer turns a raw token into a complete header value |
Integers index into arrays, so items.0.id takes the first item's id.
This is a dot-path, not JSONPath
The json_path assertion type accepts full JSONPath — $.data[*].id, filters and all. Extractor rules do not. There is no $, no [*], no filter expressions and no wildcards. Write data.0.id, not $.data[0].id.
Two things fail quietly rather than failing the request:
- If the response is not JSON, every rule is skipped.
- If a path does not resolve, that rule is skipped and the variable is not set.
So a later request finding {{token}} unresolved usually means the extractor path was wrong, not that the request failed. The stored value is always text, with the prefix applied.
Assertions — checking a response without code
The other common job with a no-code equivalent. An assertion is a type, an operator and an expected value; full detail is on API testing.
| Type | Checks |
|---|---|
status | The HTTP status code, numerically |
json_path | The value at a JSONPath expression in the JSON body |
header | A response header |
response_time | How long the request took, in milliseconds |
body_text | The raw response body as text |
Eight operators are available:
| Operator | Passes when |
|---|---|
eq / ne | The value equals, or does not equal, the expected one. Type-aware, so a JSON number matches a typed-in 200. |
lt / gt | Numeric comparison |
contains | Text contains the substring; a list contains the item; an object has it as a key or a value |
exists / not_exists | The value is present, or absent. A JSON null counts as present. |
matches | A regular expression search — case-sensitive, unanchored, so ^ and $ are yours to add |
Reach for a script when a check needs more than one operator can say — comparing two fields to each other, verifying an order, or checking a computed total.
Browser scripts
A recorded browser journey is a real Playwright file you can open and edit. The language is chosen when the script is created and cannot be changed afterwards.
| Value | Produces | Pick it when |
|---|---|---|
python | A plain Playwright script | The default. Nobody on the team needs to read it. |
javascript | A plain Playwright script | Your team reads JavaScript |
typescript | A plain Playwright script | Your team reads TypeScript |
javascript_test | A @playwright/test spec | You want a file that also runs under your own Playwright setup |
typescript_test | A @playwright/test spec | Same, typed |
The two _test values use an underscore. A hyphen is rejected.
qaclan web record --feature <feature_id> --name login --language typescript_testWhat a browser script is given at run time
Whatever the language, the generated file reads its configuration from environment variables set by the runner. If you hand-edit a script or import one of your own, honouring these keeps it working with the run dialog.
| Variable | Holds |
|---|---|
QACLAN_STORAGE_STATE | Path to the run's shared state.json, loaded on start and saved on exit. This is what makes one sign-in cover a whole suite. |
QACLAN_ARTIFACTS_PATH | Where to write captured console errors and network failures |
QACLAN_SCREENSHOT_PATH | Where to write a screenshot if the script fails |
QACLAN_BROWSER | chromium, firefox or webkit |
QACLAN_HEADLESS | 1 or 0 |
QACLAN_VIEWPORT | WxH, or empty |
Your secrets are not in that environment
Environment variables you define — passwords, tokens, hosts — are substituted into {{KEY}}placeholders when the script file is rendered for the run, not exported into the process environment. A secret therefore never appears in the parent process's environment, where anything on the machine could read it.
Where to go next
- API testing — collections, requests, auth and the assertion builder
- Negative testing and schema drift — checks that generate their own cases
- Environments and secrets — where
envcomes from - Web testing — recording, editing and hardening browser scripts