Skip to content

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.

ScriptRunsTypically used to
Pre-requestAfter variables resolve, before the request is sentCompute a signature, stamp a timestamp, set a header or query parameter
Post-requestAfter the response arrives, after extractor rules have runPull a value out for the next request, assert something an operator cannot express

The order of a request

  1. {{variables}} are resolved from the environment and collection.
  2. Pre-request extractor rules run.
  3. The pre-request script runs. Its header and parameter changes are merged in.
  4. The body is built and the request is sent.
  5. Post-response extractor rules run.
  6. The post-request script runs, so it can override anything an extractor rule just set.
  7. 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.

PythonJavaScriptDoes
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.

ValuePythonJavaScript
Environment variablesenv — a dictenv — an object
Response body, parsedresponse.json()response.json()
Response body, rawresponse.text()response.text()
Response headersresponse.headersresponse.headers
Response statusstatus_code — a bare variableresponse.status
Raw body, as a variableresponse_body— use response.text()
Raw headers, as a variableresponse_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, ...)            # AttributeError

javascript

qc.expect(response.status === 200, "expected 200"); // correct
// qc.expect(status_code === 200, ...);              // ReferenceError

Execution 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.

FieldMeaning
pathWhere to look in the JSON response, as a dot-path — data.user.id
nameWhat to store it as. Surrounding {{ }} are stripped, so either spelling works.
prefixText 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.

TypeChecks
statusThe HTTP status code, numerically
json_pathThe value at a JSONPath expression in the JSON body
headerA response header
response_timeHow long the request took, in milliseconds
body_textThe raw response body as text

Eight operators are available:

OperatorPasses when
eq / neThe value equals, or does not equal, the expected one. Type-aware, so a JSON number matches a typed-in 200.
lt / gtNumeric comparison
containsText contains the substring; a list contains the item; an object has it as a key or a value
exists / not_existsThe value is present, or absent. A JSON null counts as present.
matchesA 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.

ValueProducesPick it when
pythonA plain Playwright scriptThe default. Nobody on the team needs to read it.
javascriptA plain Playwright scriptYour team reads JavaScript
typescriptA plain Playwright scriptYour team reads TypeScript
javascript_testA @playwright/test specYou want a file that also runs under your own Playwright setup
typescript_testA @playwright/test specSame, typed

The two _test values use an underscore. A hyphen is rejected.

qaclan web record --feature <feature_id> --name login --language typescript_test

What 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.

VariableHolds
QACLAN_STORAGE_STATEPath 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_PATHWhere to write captured console errors and network failures
QACLAN_SCREENSHOT_PATHWhere to write a screenshot if the script fails
QACLAN_BROWSERchromium, firefox or webkit
QACLAN_HEADLESS1 or 0
QACLAN_VIEWPORTWxH, 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