Migration Guide
Migrating from Postman to Bruno: a step-by-step guide.
Hands-on walkthrough for teams that have decided to move — export, import, script translation, CI, and the command reference you will actually need.
Verified May 2026 · Bruno 3.3.0 · Postman v12
Still deciding whether to migrate? See the full API clients comparison →
// Introduction
This guide is for engineers who have already decided — or are close to deciding — to migrate from Postman to Bruno. It does not make the case for migration: if you are still evaluating, read the API clients comparison page or the overview of code-first vs GUI tools first, then come back. What this guide covers: exporting your Postman collections cleanly, importing and verifying them in Bruno, reorganising into a Git-friendly directory layout, translating pre-request and test scripts from pm.* to Bruno's bru.* API, wiring bru run into your CI pipeline, and establishing a team workflow that uses Git as the collaboration layer.
Why teams migrate
The reasons teams move from Postman to Bruno are specific, not aesthetic. The most common:
- Forced cloud sync. Postman discontinued Scratch Pad — its offline mode — in May 2023. All collections and environments now require a Postman account and are stored on Postman's servers. Teams with data-sovereignty requirements, security policies that restrict SaaS tools, or HIPAA/GDPR obligations have no opt-out under the current product.
- Licensing preference. Bruno is MIT-licensed with no cloud subscription, no paywalled features, and no account required. The v3 release made previously paid features — File Mode, Git integration, and run History — free for all users.
- Git-native workflow. Postman collaboration happens through cloud-hosted workspaces where changes appear instantly with no code review step. Bruno's collection format is plain .bru files on disk that diff and commit cleanly into any Git repository. Teams that want API changes reviewed in pull requests before they land get that for free with Bruno.
- Cost. Postman's free tier covers basic API calls; team collaboration, mock servers, and API monitoring require a paid plan. Bruno has no paid tier.
// Before you migrate: what you lose and what you gain
Migration has real costs. Reading this section carefully before committing your team to the process can prevent regret.
What you lose
These are genuine capability gaps, grounded in the API clients comparison matrix. Dismissing them is how migrations stall three months in.
- Real-time multiplayer editing. Postman's cloud workspaces support simultaneous editing — multiple contributors can work in the same collection at once. Bruno's model is Git-native by design: changes go through commits and pull requests. Real-time collaboration is impossible — there is no live multiplayer, no shared cursor, no instant visibility of a teammate's change.
- Hosted mock servers. Postman provides cloud-hosted mock servers that return collection-defined example responses via a shareable URL. Bruno has no built-in mock server. Teams requiring service virtualisation must run a separate tool — Mockoon, WireMock, or json-server — alongside Bruno.
- Granular role-based access. Postman's Team and Enterprise plans support workspace roles, collection-level permissions, and audit trails. Bruno has no access control model at all: a Git repository's branch permissions are the only gate. There are no Bruno-managed viewer vs editor roles, no collection comment threads, and no per-user audit log.
- NTLM and Hawk authentication. Postman supports a broad set of auth schemes including NTLM, Hawk, and AWS Signature v4. Bruno's auth UI covers OAuth 2.0 (authorization code, client credentials, implicit, PKCE), Bearer, Basic, API key, Digest, and WSSE — but NTLM and Hawk are not supported. If your APIs use either scheme, Bruno cannot authenticate natively.
- The Postman Vault. Postman Vault stores credentials in local encrypted storage without syncing them to the cloud, separate from environment variables. Bruno has no equivalent managed credential store. Secrets in Bruno live in a .env file on disk at the collection root — useful, but a different operational model.
What you gain
These are Bruno's genuine advantages — also grounded in the comparison matrix, not marketing copy.
- Full data sovereignty. Bruno stores every collection, environment, and request as files on disk. There is no Bruno account, no cloud connection, and no telemetry by default. Sensitive credentials stay on your machine. This is a hard requirement for some teams; for others it is just a preference. Either way, it is a real architectural difference from Postman's cloud-mandatory model.
- Git-native collections. Every Bruno collection is a directory of plain .bru files that diff and commit cleanly into any Git repository. An API change — a new header, a changed URL, an added assertion — is a pull request diff. Your existing review workflow applies with no changes. Compare this to Postman's workspace model, where there is no formal change review process for collection updates.
- MIT license, no paid tier. Bruno is free for unlimited team members with no feature gating. The v3 release made Git integration, run History, and File Mode — previously paid — free for everyone. There is no future billing risk from team growth or feature usage.
- Built-in CLI runner. bru run ships with Bruno. There is no separate package to install or version-pin. Newman, Postman's CLI runner, is a standalone npm package — useful, but an additional dependency. bru run generates JUnit XML, JSON, and HTML reports natively.
- Diffable, human-readable format. Bruno's .bru file format is plain text. A teammate reviewing a request change in a pull request sees the diff in their standard code review tool — no Bruno account, no GUI required. Postman's collection JSON is less readable as a diff, even when committed to Git.
Decision checkpoint
If any of the following are true for your team, stop here and reconsider before committing to migration: you depend on NTLM or Hawk authentication and have no workaround; you need real-time collaborative editing with simultaneous contributors; your workflow requires collection-level role permissions or audit trails; or your team relies on Postman's cloud mock servers with no appetite for a separate mock tool. None of these gaps are Bruno's fault — they are architectural trade-offs of the file-first model. Migration only makes sense when your specific blockers on Postman are worse than these gaps.
// Prerequisites
Before starting the migration, have the following in place:
- Bruno 3.3.0 or later installed on every engineer's machine. Download from usebruno.com/downloads. The desktop app runs on macOS, Linux (deb, rpm, AppImage), and Windows.
- A Git repository where Bruno collections will live — either your existing application repository or a dedicated api-tests repository. Bruno collections are directories on disk, so any Git remote works.
- Your Postman collections exported as Collection v2.1 JSON. If you have multiple collections, export them all before starting. Instructions for exporting are in Step 1.
- Your Postman environments exported separately as JSON files. Environment variables are not included in collection exports — you need to export them from the Environments sidebar.
- A list of every Postman Vault secret and its purpose. Vault contents are not exported by any Postman export format. Document them now; you will need to re-enter them in Bruno's .env files.
- Optional: @usebruno/cli installed as a dev dependency (npm install --save-dev @usebruno/cli) if you plan to run bru run in CI immediately after migration.
// STEP 1 — Export your Postman collections
Export everything in a single session before opening Bruno. Doing it piecemeal makes it easy to miss an environment or a collection with an important test suite.
How to export
For each collection: right-click the collection name in the sidebar → Export → select Collection v2.1 (recommended). Save the JSON file with a name that matches the collection — you will reference these filenames during import. Avoid the v1 format; Bruno's importer does not fully support it. For environments: open the Environments sidebar → click the three-dot menu next to each environment → Export. Save each environment JSON file alongside the collection exports. For global variables: open the Globals section in the Environments sidebar → Export. Globals map to Bruno's global environment variables.
What exports and what does not
A Postman Collection v2.1 export captures: all requests, folder structure, pre-request scripts, test scripts, example responses, and request-level auth configuration. It does not capture:
- Vault secrets. Postman Vault credentials are intentionally excluded from all export formats. Before exporting, open the Vault and document every secret key and its purpose. You will need to recreate them in Bruno's .env files.
- Cloud mock server configuration. Postman's hosted mock servers are account-specific cloud resources — they are not exportable. If your collection uses mock server URLs, note them. In Bruno, mocking requires a separate tool.
- Monitors and scheduled runs. These are cloud services with no file equivalent. Replace them with scheduled CI jobs running bru run.
- Run history. Postman's per-user run history is stored in the cloud and has no export format.
Sanity-check the export
Before importing, verify the export is complete. Open a terminal and check that the JSON is valid and the item array is non-empty: node -e "const c = require('./collection.json'); console.log(c.item.length + ' requests');". For a collection with 50 requests you should see a number in that range. If the count is zero or the command errors, the export file is corrupt or empty — re-export before continuing.
// STEP 2 — Import into Bruno
Bruno can import Postman Collection v2.1 JSON directly. The import converts each Postman request to a .bru file and replicates the folder hierarchy as nested directories on disk. Environments and global variables are imported separately.
Import steps
In Bruno: open the Collections panel → click the three-dot menu next to any collection or use File → Import → select Import Collection. Choose your exported v2.1 JSON file. Bruno creates a new collection directory and writes a .bru file for every request. Repeat for each collection JSON. To import environments: open the Environments panel → Create Environment for each of your Postman environments, then copy the variable key-value pairs from the exported environment JSON. There is no automatic environment import from Postman's JSON format — each environment must be recreated by hand in Bruno's UI, which writes it as an environments/<name>.json file in the collection root.
What auto-converts and what needs manual work
The importer translates these faithfully: request URLs and methods, query parameters, headers (static values), request body (raw, form-data, urlencoded), and basic Bearer and API key auth. These need manual work after import:
- Pre-request scripts using pm.* APIs. The importer copies the script text as-is. Every pm.environment.get(), pm.request.headers.add(), and pm.sendRequest() reference must be translated to its bru.* equivalent. The command reference table at the bottom of this page covers every mapping.
- Test scripts using pm.test() and pm.expect(). These copy as-is but fail silently in Bruno because pm is not defined. Translate pm.test() to test() and pm.expect() to expect() — the Chai assertion API is identical.
- Environment variable references in scripts. pm.environment.get('KEY') becomes bru.getEnvVar('KEY') for standard environment variables and bru.getProcessEnv('KEY') for secrets sourced from the .env file.
- OAuth 2.0 flows beyond client credentials and authorization code. If your Postman collection uses Device Code flow or other advanced OAuth variants, verify these in Bruno's Auth tab after import — they may need manual configuration.
Script translation example
A representative 10-line pre-request and test script — the patterns you will encounter most often:
Postman
// Pre-request Script tab
const userId = pm.environment.get("CURRENT_USER_ID");
const apiKey = pm.environment.get("API_KEY");
pm.request.headers.add({ key: "X-User-Id", value: userId });
pm.request.headers.add({ key: "X-Api-Key", value: apiKey });
pm.request.headers.add({ key: "X-Timestamp", value: String(Date.now()) });
// Tests tab
pm.test("Status is 200", () => pm.expect(pm.response.code).to.equal(200));
pm.test("Response has data", () => {
pm.expect(pm.response.json()).to.have.property("data");
});
pm.environment.set("lastId", pm.response.json().id);Bruno
// script:pre-request block
const userId = bru.getEnvVar("CURRENT_USER_ID");
const apiKey = bru.getProcessEnv("API_KEY"); // from .env file
req.setHeader("X-User-Id", userId);
req.setHeader("X-Api-Key", apiKey);
req.setHeader("X-Timestamp", String(Date.now()));
// tests block (separate block in the .bru file)
test("Status is 200", function() {
expect(res.status).to.equal(200);
});
test("Response has data", function() {
const body = JSON.parse(res.getBody());
expect(body).to.have.property("data");
bru.setEnvVar("lastId", body.id);
});Note: API_KEY is read from bru.getProcessEnv() rather than bru.getEnvVar() — secrets belong in the .env file (gitignored), not in the committed environment JSON. res.getBody() returns a string; parse it explicitly with JSON.parse().
// STEP 3 — Reorganize into a Git-friendly structure
Bruno's auto-import replicates your Postman folder structure exactly. That structure was designed for a cloud workspace GUI — it may not be the right shape for a file-based Git repository. This step is optional but strongly recommended before your first commit.
Why this step matters
In Postman, collection organisation is a GUI concern — folders group requests visually in the sidebar. In Bruno, every folder is a filesystem directory. The shape of that directory becomes the shape of your Git history. A flat collection with dozens of requests in the root directory is hard to review in a pull request, hard to gitignore selectively, and hard to extend as the API surface grows. A small reorganisation now pays off every time someone opens git diff in the future.
Recommended directory layout
A collection organised by domain area with environments and secrets separated:
Postman
# Postman: all requests in a single collection JSON
# collection.json
{
"info": { "name": "My API" },
"item": [
{ "name": "GET /users", ... },
{ "name": "POST /users", ... },
{ "name": "GET /orders", ... },
{ "name": "auth/oauth", ... },
...
],
"auth": { ... },
"variable": [ ... ]
}
# Postman environments: separate JSON files
# dev.postman_environment.json
# staging.postman_environment.jsonBruno
# Bruno: directory-first, Git-native structure
api-tests/
├── bruno.json # collection config (committed)
├── .env # secrets (in .gitignore)
├── .env.sample # placeholder keys (committed)
├── environments/
│ ├── local.json # non-secret vars (committed)
│ ├── staging.json
│ └── production.json
├── auth/
│ ├── oauth-token.bru
│ └── api-key-check.bru
└── endpoints/
├── users/
│ ├── get-user.bru
│ └── create-user.bru
└── orders/
├── list-orders.bru
└── create-order.bruNote: Group requests by domain area, not by HTTP method. Each subdirectory becomes a reviewable unit in pull requests — a PR that only touches endpoints/orders/ is immediately scoped for the reviewer.
What to commit and what not to
A minimal .gitignore for a Bruno collection root:
- .env — your local secrets file. Never commit this. Every team member maintains their own copy.
- node_modules/ — if you have a package.json for the bru CLI dependency.
- results/ — generated report files from bru run. These belong in CI artifacts, not in the repository.
- Everything else should be committed: bruno.json, all .bru request files, all environments/<name>.json files (with non-secret variable values only), and .env.sample with placeholder values for every key that belongs in .env.
// STEP 4 — Port pre-request and test scripts
Script translation is the most time-consuming part of migration. Bruno uses the same JavaScript runtime for scripts, and the Chai assertion library is available in test blocks — the logic translates cleanly. What changes is the API: Postman's pm.* namespace becomes bru.*, req.*, and res.* in Bruno.
Script blocks in .bru files
A .bru file has three script-related blocks: script:pre-request (runs before the request is sent), script:post-response (runs after the response is received), and tests (contains test assertions). In Postman these are the Pre-request Script tab and the Tests tab. One important difference: Postman supports collection-level and folder-level scripts that run before every request in a collection or folder. Bruno has no collection-level or folder-level script equivalent — any shared logic must be inlined into individual request files or handled via a separate module imported with require() under --sandbox=developer.
Variable scope translation
Postman has five variable scopes: global, collection, environment, data (local), and Vault. Bruno maps them to distinct APIs:
- Environment variables — bru.getEnvVar(key) / bru.setEnvVar(key, val): correspond to Postman's environment scope. Values are read from the active environment file (environments/<name>.json) and can be updated during a run.
- Runtime variables — bru.getVar(key) / bru.setVar(key, val): in-memory for the duration of a single runner execution. Use these for values computed in one request and consumed by the next within a run — equivalent to Postman's data (local) scope.
- Collection variables — bru.getCollectionVar(key): reads from bruno.json. This is read-only at runtime. Unlike Postman's collection variables, you cannot write to collection variables from a script.
- Global variables — bru.getGlobalEnvVar(key) / bru.setGlobalEnvVar(key, val): workspace-level variables equivalent to Postman's global scope.
- Process environment / secrets — bru.getProcessEnv(key): reads from the OS environment or the collection's .env file. Use this for credentials that must not appear in committed environment files. Note: the correct method name is bru.getProcessEnv() — there is no bru.getProcessEnvVar() variant.
Porting pm.sendRequest (token fetch flows)
A common Postman pattern is fetching an OAuth token in a pre-request script before a protected request. Postman uses pm.sendRequest with a callback. Bruno supports async/await natively — the callback form also works but is not necessary.
Postman
// Postman: Pre-request Script tab
// Callback-based pm.sendRequest
pm.sendRequest({
url: pm.environment.get("AUTH_URL") + "/oauth/token",
method: "POST",
header: { "Content-Type": "application/x-www-form-urlencoded" },
body: {
mode: "urlencoded",
urlencoded: [
{ key: "grant_type", value: "client_credentials" },
{ key: "client_id", value: pm.environment.get("CLIENT_ID") },
{ key: "client_secret", value: pm.environment.get("CLIENT_SECRET") },
],
},
}, function(err, response) {
if (!err) {
pm.environment.set("ACCESS_TOKEN", response.json().access_token);
}
});Bruno
// Bruno: script:pre-request block
// async/await natively supported — no callback required
const response = await bru.sendRequest({
url: bru.getEnvVar("AUTH_URL") + "/oauth/token",
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: {
mode: "urlencoded",
urlencoded: [
{ name: "grant_type", value: "client_credentials" },
{ name: "client_id", value: bru.getProcessEnv("CLIENT_ID") },
{ name: "client_secret", value: bru.getProcessEnv("CLIENT_SECRET") },
],
},
});
const token = JSON.parse(response.body).access_token;
bru.setEnvVar("ACCESS_TOKEN", token);Note: CLIENT_ID and CLIENT_SECRET are read from bru.getProcessEnv() so they come from the .env file and are never committed to the repository. The async/await form is clearer and avoids callback nesting.
Sandbox mode and Node.js built-ins
By default, Bruno scripts run in a restricted sandbox. If your Postman scripts use require(), access the filesystem, or rely on Node.js built-in modules (crypto, fs, path), you need to pass --sandbox=developer to bru run. The developer sandbox enables require() and relaxes module restrictions. Do not use --sandbox=developer in a shared CI environment unless every script author is trusted — it grants full Node.js access to the runtime. For most migrations, the built-in bru.* API covers the common cases without needing the developer sandbox.
// STEP 5 — Replace Newman with bru run
Newman is Postman's standalone CLI runner. Bruno's equivalent is bru run, which ships as part of the @usebruno/cli npm package. The flag set differs from Newman but the purpose is identical: run a collection non-interactively, exit non-zero on failure, and emit structured reports.
Basic collection run
Unlike Newman, which takes a collection JSON file path, bru run operates on the collection directory. Run it from the collection root or pass the collection path as the first positional argument. The environment is selected by name, matching the environments/<name>.json file.
Postman
# Newman: separate install, file-path based
npm install --save-dev newman
npx newman run collections/api.json \
--environment environments/staging.json \
--reporters cli,junit \
--reporter-junit-export results/junit.xml \
--bailBruno
# bru run: ships with @usebruno/cli, directory based
npm install --save-dev @usebruno/cli
# Run from the collection root directory
npx bru run \
--env staging \
--reporter-junit results/junit.xml \
--reporter-json results/report.json \
--bailNote: bru run reads the environment by name (matching environments/staging.json in the collection), not by file path. Run from the collection root or pass the collection directory as the first argument: npx bru run ./api-tests --env staging.
GitHub Actions workflow
A minimal workflow that runs the Bruno collection on every push to main and uploads the JUnit report as an artifact:
Postman
# .github/workflows/api-tests.yml (Newman)
name: API Tests
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: |
npx newman run collections/api.json \
--environment environments/staging.json \
--reporters cli,junit \
--reporter-junit-export results/junit.xml \
--bail
env:
API_KEY: ${{ secrets.API_KEY }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: api-test-report
path: results/Bruno
# .github/workflows/api-tests.yml (bru run)
name: API Tests
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: |
npx bru run \
--env staging \
--reporter-junit results/junit.xml \
--reporter-json results/report.json \
--bail
working-directory: ./api-tests
env:
API_KEY: ${{ secrets.API_KEY }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: api-test-report
path: results/Note: Set working-directory to your collection root. Secrets injected as CI environment variables are accessible in scripts via bru.getProcessEnv('API_KEY') and in request templates as {{process.env.API_KEY}} — no .env file needed in CI.
Report formats
bru run supports three report formats, which can all be combined in a single invocation:
- --reporter-junit path: JUnit XML output. Compatible with GitHub Actions test annotations, GitLab CI test reports, and Jenkins test result publishing.
- --reporter-json path: JSON summary of all request results, assertions, and timings. Useful for custom processing or dashboard integration.
- --reporter-html path: self-contained HTML report suitable for artifact storage or stakeholder sharing without tooling.
- --bail: stop the run on the first test failure and exit with a non-zero code. Almost always the right default for CI pipelines where a failure should block the build immediately.
// STEP 6 — Establish a Git-based team workflow
The primary reason many teams switch from Postman to Bruno is Git-native collaboration. Every request, environment, and script is a plain text file. This step establishes the team conventions that make that work in practice.
Managing secrets across the team
Each team member maintains their own .env file at the collection root. For onboarding:
- Commit a .env.sample listing every required key with empty or placeholder values. Update it whenever a new secret is added — the sample file is the canonical list of what every team member needs to configure locally.
- Document in the repository README where each secret value comes from: which password manager, which secrets manager (HashiCorp Vault, AWS Secrets Manager), or which CI environment. New team members should not have to ask.
- In CI, inject secrets as environment variables in the pipeline rather than committing a populated .env file. bru run picks up OS environment variables automatically alongside the .env file.
Reviewing API changes in pull requests
Because .bru files and environment JSON files are plain text, API changes are reviewable in pull requests with the same tooling as code. A request that gains a new assertion shows the addition as a green line. A request that changes its base URL shows a clear before/after. A new environment variable in environments/staging.json shows as a simple addition. Add bru run --env staging --bail to your CI required status checks so that any collection change that breaks a test also blocks the pull request. Encourage reviewers to run bru run locally on the branch before approving, especially for changes to pre-request scripts or authentication flows.
Onboarding new team members
The onboarding steps for a new engineer on a Bruno-based team:
- Clone the repository. The Bruno collection is in the repository — there is no separate Postman workspace invite or account to create.
- Copy .env.sample to .env and fill in the real secret values (from the password manager or secrets manager documented in the README).
- Open Bruno, create a new collection from the cloned directory, and verify a few requests run against the local or staging environment.
- There is no Bruno account to create, no workspace to join, and no license to acquire.
// Common pitfalls
Teams migrating from Postman to Bruno consistently hit the same problems. Here are the most common, with specific fixes.
- Calling res.json() instead of JSON.parse(res.getBody()). Bruno's response object has no res.json() shortcut. pm.response.json() is a Postman convenience method. In Bruno, res.getBody() returns a string — call JSON.parse() on it explicitly.
- Using bru.getProcessEnvVar() instead of bru.getProcessEnv(). The correct Bruno method name is bru.getProcessEnv(key). There is no getProcessEnvVar variant. Scripts that copy outdated documentation or AI-generated snippets sometimes use the wrong name; the call returns undefined silently rather than throwing.
- Committing .env instead of .env.sample. Bruno loads .env from the collection root automatically, which makes it easy to accidentally stage and commit it. Add .env to .gitignore before your first git add. A committed .env in even a private repository is a credentials exposure risk.
- Expecting collection variables to be writable. bru.getCollectionVar(key) is read-only at runtime. If your Postman pre-request scripts use pm.collectionVariables.set(), translate those writes to bru.setVar() for run-scoped memory or bru.setEnvVar() for environment scope.
- Forgetting --sandbox=developer for require(). Scripts that use Node.js built-ins (crypto, fs) or require() fail in Bruno's default sandbox — often with 'require is not defined'. Add --sandbox=developer to bru run if any script imports a module.
- Running bru run from the wrong directory. Unlike Newman, which takes a file path, bru run must run from the collection root or have the collection path as its first argument. Running from a parent directory fails or picks up the wrong bruno.json.
- Trusting the importer too much for complex scripts. Bruno's Postman importer translates request structure accurately but copies pre-request and test script text verbatim — still referencing pm.*. After importing, run the collection manually in the Bruno GUI against a test environment and compare responses to your Postman baseline before switching CI.
- Skipping the environment recreation step. Postman environments do not auto-import from their JSON export format. If you import a collection but skip recreating the environments in Bruno, all {{variable}} references will resolve to empty strings and requests will fail in non-obvious ways.
// What next
If migration is complete, two resources are most relevant from here. The command reference table immediately below covers every pm.* → bru.* mapping you are likely to need during script translation. For a broader view of how Bruno fits alongside other GUI API clients, the API clients comparison page covers Bruno, Postman, Insomnia, Hoppscotch, and Yaak across 14 dimensions — useful if you are evaluating whether Bruno meets all your team's needs or whether a hybrid tool setup makes sense.
Related reading
Both pages are designed to be read alongside this guide:
- API clients comparison — full feature matrix across all five major GUI clients with an honest verdict for each. Covers collaboration model, CLI runner maturity, data sovereignty, and pricing.
- Choosing between code-first libraries and GUI clients — if your team is re-evaluating whether Bruno (or any GUI client) is the right category, this article covers six decision factors.
// Command reference
| Category | Postman | Bruno | Notes |
|---|---|---|---|
| Env vars | pm.environment.get(key) | bru.getEnvVar(key) | Read active environment variable |
| pm.environment.set(key, val) | bru.setEnvVar(key, val) | Write to active environment | |
| pm.environment.unset(key) | bru.deleteEnvVar(key) | Remove a key from the active environment | |
| pm.variables.get(key) | bru.getVar(key) | Runtime-scoped in-memory variable; lost after run ends | |
| pm.variables.set(key, val) | bru.setVar(key, val) | Write runtime variable for cross-request data within a run | |
| pm.collectionVariables.get(key) | bru.getCollectionVar(key) | Read collection config; read-only at runtime in Bruno | |
| pm.globals.get(key) | bru.getGlobalEnvVar(key) | Read workspace-level global variable | |
| pm.globals.set(key, val) | bru.setGlobalEnvVar(key, val) | Write workspace-level global variable | |
| process.env.KEY (via dotenv) | bru.getProcessEnv(key) | Read OS env or .env file — correct name, no 'Var' suffix | |
| {{variable}} | {{variable}} | Template syntax is identical in both tools | |
| {{$processEnv.KEY}} | {{process.env.KEY}} | Process env in request URL / header templates | |
| Response | pm.response.code | res.status | Numeric HTTP status code |
| pm.response.status | res.statusText | Status text, e.g. 'OK', 'Not Found' | |
| pm.response.json() | JSON.parse(res.getBody()) | res.getBody() returns a string — no res.json() shortcut | |
| pm.response.text() | res.getBody() | Raw response body as string | |
| pm.response.headers.get(name) | res.getHeader(name) | Read a single response header value | |
| pm.response.headers | res.headers | Full headers object | |
| pm.response.responseTime | res.responseTime | Response time in milliseconds | |
| pm.response.size() | res.getSize() | Response size in bytes | |
| pm.response.url | res.url | Final resolved URL of the response | |
| Request | pm.request.url.toString() | req.getUrl() | Read the request URL in pre-request script |
| pm.request.method | req.getMethod() | Read the HTTP method | |
| pm.request.headers.get(name) | req.getHeader(name) | Read a single request header | |
| pm.request.headers | req.getHeaders() | Read all request headers as an object | |
| pm.request.body.raw | req.getBody() | Read the request body string | |
| pm.request.headers.add({key, value}) | req.setHeader(name, val) | Add or overwrite a request header | |
| pm.request.body.update(str) | req.setBody(body) | Replace the request body | |
| Tests | pm.test('name', () => { ... }) | test('name', function() { ... }) | Test block; Chai assertions inside both |
| pm.expect(val) | expect(val) | Direct Chai expect — no pm namespace in Bruno | |
| pm.expect(val).to.equal(x) | expect(val).to.equal(x) | Strict equality | |
| pm.expect(val).to.include(x) | expect(val).to.include(x) | String or array includes | |
| pm.expect(obj).to.have.property('k') | expect(obj).to.have.property('k') | Property existence check | |
| pm.response.to.have.status(200) | expect(res.status).to.equal(200) | Status assertion via res.status directly | |
| pm.response.to.be.json | expect(res.getHeader('content-type')).to.include('json') | Content-type assertion | |
| Runner control | postman.setNextRequest('name') | bru.setNextRequest('name') | Jump to a named request in the runner |
| pm.execution.setNextRequest('name') | bru.setNextRequest('name') | Alternate Postman API — same Bruno equivalent | |
| postman.setNextRequest(null) | bru.runner.stopExecution() | Stop runner after current request | |
| (no equivalent) | bru.runner.skipRequest() | Skip the current request and advance to next | |
| pm.sendRequest(options, callback) | await bru.sendRequest(options) | HTTP call in scripts; Bruno supports async/await natively | |
| (no equivalent) | await bru.sleep(ms) | Pause execution for ms milliseconds | |
| Dynamic vars | {{$timestamp}} | {{$timestamp}} | Unix timestamp — identical syntax |
| {{$isoTimestamp}} | {{$isoTimestamp}} | ISO 8601 timestamp | |
| {{$guid}} | {{$guid}} | Random UUID v4 | |
| {{$randomInt}} | {{$randomInt}} | Random integer 0–1000 | |
| {{$randomEmail}} | (use script) | Not a built-in Bruno variable; generate in script:pre-request with bru.setVar | |
| CLI | newman run collection.json | bru run | Run all requests; bru run uses the collection directory, not a JSON file |
| --environment env.json | --env envName | Bruno uses the environment name, not a file path | |
| --env-var KEY=val | --env-var KEY=val | Override a single env variable; same flag in both | |
| --env-file path | --env-file path | Load extra env variables from a file | |
| --folder 'folder name' | --folder 'folder name' | Run only requests inside a named folder | |
| --reporters junit | --reporter-junit path | JUnit XML; Bruno requires an explicit output path | |
| --reporter-json-export path | --reporter-json path | JSON report; flag name differs slightly | |
| --reporter-html-export path | --reporter-html path | HTML report | |
| --bail | --bail | Stop run on first failure; same flag | |
| --delay-request ms | --delay ms | Delay between requests in milliseconds | |
| --iteration-count N | --iteration-count N | Run the collection N times; same flag | |
| --iteration-data file.csv | --csv-file-path file.csv | Data-driven runs from CSV; flag name differs | |
| --iteration-data file.json | --json-file-path file.json | Data-driven runs from JSON; flag name differs | |
| --insecure | --insecure | Skip TLS certificate verification; same flag | |
| (no equivalent) | --sandbox=developer | Enable require() and Node.js built-ins in scripts | |
| (no equivalent) | -r | Run requests recursively through all subdirectories | |
| (no equivalent) | --tests-only | Skip requests that have no test assertions | |
| (no equivalent) | --tags tagname | Run only requests tagged with the specified tag | |
| (no equivalent) | --parallel | Run requests in parallel rather than sequentially |