Environments & variables

One request, many deployments. An environment is a named bag of string variables, and {{name}} is how you reach into it.

An environment is a file

# environments/staging.toml
name = "staging"

[vars]
baseUrl = "https://api.staging.example.com"
tenantId = "acme"
apiVersion = "2026-08-01"

Values are strings — TOML gives you numbers and booleans, but a variable is substituted into text, so Mándalo stores every value as a string. Environments live in environments/ inside the workspace and are selected from the picker in the title bar; exactly one is active at a time.

Where interpolation applies

{{name}} is substituted, with surrounding whitespace inside the braces trimmed, in all of these places:

  • The URL — including the host, so {{baseUrl}}/v1/charges works.
  • Header names and values.
  • The request body.
  • Auth fields — bearer token, basic username and password, API key name and value.
  • The GraphQL query and, key by key and value by value, the GraphQL variables JSON.
  • The gRPC message JSON, the target URL, and gRPC metadata values.

Interpolation happens in the Rust core, before auth is applied and before the request is built. There is no second, different substitution pass in the UI to drift out of sync with it.

Unresolved variables fail loud

If a template names a variable that nothing resolves, the request is not sent. You get an error naming the variable:

unresolved variable: token

This is deliberate and it is not configurable. The alternative — substituting an empty string — turns Authorization: Bearer {{token}} into Authorization: Bearer and gives you a 401 to debug instead of a one-line message telling you exactly what is missing. An unclosed {{ is an error too, rather than being passed through as literal text.

Resolution order

For a saved request executed by the runner, a variable name is resolved in this order:

  1. Values set by a pre-request script during this run (pm.environment.set(…)), and values captured earlier in the same run.
  2. The active environment's [vars].
  3. The process environment, under MANDALO_SECRET__<ENV>__<KEY> — the environment name and the variable name upper-cased, with - turned into _. In the staging environment, {{token}} falls back to MANDALO_SECRET__STAGING__TOKEN.
  4. Otherwise: the unresolved-variable error above.

Note the direction: the file wins over the process environment. The secret lookup is a fallback for names the file deliberately does not define, which is what makes it safe to commit the file. See Collections & git for the full secret-hygiene story.

Captures

A capture pulls a value out of a response and stores it in a variable, so the next request can use it. Log in, capture the token, and every following request sends it — with no script involved.

A capture has three fields:

  • from — the source. One of status, header.<Name> (case-insensitive), or body.$.<jsonpath>. The body form takes a real JSONPath expression that must start with $, so body.$.data.users[0].id is valid.
  • into — the variable name to write. Letters, digits, - and _.
  • scoperun, session or persist.
[[captures]]
from = "body.$.token"
into = "token"
scope = "session"

A capture that matches nothing is a failure, not an empty string — the same fail-loud rule as interpolation. If the source is a JSON path and the body is not JSON, that is an error too.

The three scopes

  • run — the value is meant to live only for the current run: available to later requests in the same collection run, then gone.
  • session — the value is meant to live as long as the app is open, across runs.
  • persist — the value is meant to be written back into the environment file on disk.
Honestly

The scope is stored on the capture and shown in the editor, but the three are not yet fully differentiated at execution time. Today, when an environment is selected in the app, captured values are written into it; a runner-driven run keeps them in that run's variable frame. Assume a capture may end up in your environment file, and do not capture a production token into an environment you commit.

Setting variables from scripts

Scripts can read and write the same variable bag: pm.environment.get, set, unset, has, toObject and replaceIn. Non-string values are coerced to strings on the way in, objects via JSON.stringify. The full surface is documented in Scripts & tests.

const body = pm.response.json();
pm.environment.set("token", body.access_token);
pm.environment.set("expiresAt", Date.now() + body.expires_in * 1000);

Prefer a declarative capture when it does the job. It survives review better than a script, and it is one line instead of a JavaScript block a reviewer has to read.