Tetiva runs JavaScript at two points of a request: before it is sent (Pre-request) and after the response arrives (Post-response). A pre-request script injects a header, computes a signature, or writes an environment variable; a post-response script reads the body, stores a token, and asserts the result with pm.test. Scripts live on the Scripts tab of a request or a collection, run in a sandbox with no file or network access, and everything they print shows up on the Tests tab next to the response.
Where scripts live#
Every request and every collection has a Scripts tab (for collections, open it through Open Details in the context menu). Inside are two buttons — Pre-request and Post-response — each with its own editor, and switching between them keeps the undo history.
The editor completes pm. as you type, and {{var}} placeholders resolve the same way as in a request body: hover one to see the value from the active environment, with secrets masked.


Inheritance: request → collection → parent#
A script on the request replaces the collection script entirely. When the request field is empty, Tetiva walks up the collection tree and takes the first non-empty script it finds (the walk stops after 50 levels). Pre and post inherit independently, so a post-response script can come from a collection while the pre-request script is defined on the request itself.
A shared auth script therefore belongs on the root collection: it applies to everything underneath until a request defines its own.
The pm.* surface#
Available in both phases:
| Call | What it does |
|---|---|
console.log(...) | Prints into the Console block on the Tests tab. warn, error and info write to the same stream — there are no levels |
pm.environment.get(key) | Variable value, or undefined |
pm.environment.set(key, value) | Sets a variable; the value is coerced to a string |
pm.environment.unset(key) | Drops a variable from this run |
pm.request.method | Request method |
pm.request.url | URL after variable substitution |
pm.request.protocol | http, grpc or graphql |
pm.request.headers.upsert({ key, value }) | Adds a header or replaces an existing one |
pm.request.headers.remove(key) | Removes a header |
pm.request exists in both phases, but headers.upsert and headers.remove do nothing in a post-response script: the request is already on the wire and the modified copy is never read.
gRPC adds pm.request.service, pm.request.grpcMethod, pm.request.message (the JSON body) and pm.request.metadata with get(key), set(key, value), remove(key) and toObject(). Metadata reaches the script as a copy: set changes what goes on the wire, while the stored request stays untouched. See gRPC.
GraphQL adds pm.request.graphqlVariables and pm.request.graphqlOperation. See GraphQL.
Post-response only:
| Call | What it does |
|---|---|
pm.response.code | Response code as a number |
pm.response.status | Alias of pm.response.code — the same number, not the reason phrase |
pm.response.text() | Response body as a string |
pm.response.json() | Body parsed as JSON; invalid JSON yields undefined instead of throwing |
pm.test(name, fn) | A test: fn returning normally is PASS, fn throwing is FAIL with the error text |
For gRPC there are two extras: pm.response.statusText (the status name) and pm.response.metadata, a plain object holding the first value of each key.
There is no assertion library — pm.expect and chai are not wired in, so assertions are plain JavaScript with throw.
What happens to variables#
Values written with pm.environment.set land in the active environment once the request finishes: existing variables are updated, missing ones created as regular (non-secret) variables. With no active environment, the changes live only inside that run. Resolution order is covered in Environments and variables.
Sandbox limits#
- No filesystem, no network:
fetch,XMLHttpRequest,WebSocket,require,process,module,Bufferand__dirnameare undefined. - No timers:
setTimeoutandsetIntervaldo not exist.Promiseis there, but nothing can defer work. - Standard
JSONandMathare available. - No state survives a run — each script gets a fresh VM, so globals from the previous run are gone.
- Five seconds per phase. A script that ignores the interrupt — a backtracking regular expression is the classic case — is left running in the background instead of holding the request.
- Memory is not capped. Worth remembering before you run someone else's collection.
An error or timeout in a pre-request script does not cancel the request: it goes out with the headers it had before the script ran, and the error text appears under Script Errors. The same holds for post-response scripts — you still get the response.
Tests and console#
The Tests tab holds three blocks: Test Results with PASS/FAIL per pm.test, Console with output from both phases (lines tagged [pre] and [post]), and Script Errors with the phase and message. The tab badge shows passed/total, or a dot when there is only console output. More on the response pane in Response viewer.
Examples#
Pre-request: a trace header and a guard.
pm.request.headers.upsert({ key: "X-Request-Id", value: String(Date.now()) })
var token = pm.environment.get("access_token")
if (!token) {
console.warn("access_token is empty — run the login request first")
}Post-response: store the token, assert the reply.
var data = pm.response.json()
pm.test("status is 200", function () {
if (pm.response.code !== 200) throw new Error("got " + pm.response.code)
})
pm.test("token returned", function () {
if (!data || !data.access_token) throw new Error("no access_token in body")
})
if (data && data.access_token) {
pm.environment.set("access_token", data.access_token)
}Copy as cURL and scripts#
Copy as cURL runs the pre-request script as a dry run: header and variable changes show up in the command, but nothing is written to the database and no history entry is created — see Import and export.
Scripts written for Postman usually need edits, since only the pm.* subset listed above is implemented. What survives a migration is covered in Migrate from Postman.