Environment Variables
Last updated on October 22, 2026
You can keep station- and environment-specific values out of your repository: an instrument address that differs per bench, an MES URL per environment, an API token.
Your procedure declares the keys it reads. TofuPilot delivers their values to each station before the run starts.
Overview
name: Board Functional Test
config:
DMM_RESOURCE:
required: true
MES_URL:
default: https://mes.example.com
secrets:
MES_TOKEN:
required: true
plugs:
- name: DMM
python: plugs.dmm:Dmm
with:
resource: "{config.DMM_RESOURCE}"
main:
- name: Report to MES
python: phases.mes:reportimport requests
def report(unit, config, secrets):
requests.post(
f"{config.MES_URL}/results",
headers={"Authorization": f"Bearer {secrets.MES_TOKEN}"},
json={"serial": unit.serial_number},
timeout=10,
)# Read by local runs only
DMM_RESOURCE="TCPIP0::192.168.1.50::INSTR"
MES_TOKEN="sandbox-token"There are two types of environment variables:
| Config | Secret | |
|---|---|---|
| Declared under | config: | secrets: |
| Use for | Addresses, URLs, settings | Passwords, API keys, tokens |
| Readable after saving | Yes | No, a digest is shown |
| Recorded on each run | The value | The version only |
| Masked in logs | No | Yes |
Everyone who can read a run sees its Config values. Store anything sensitive as a Secret, and read Environment Variables Security before storing production credentials.
Declare Keys
You can declare each key under config: or secrets: in procedure.yaml.
config:
DMM_RESOURCE:
required: true
description: VISA resource of the bench DMM
SETTLE_MS:
type: integer
default: 200
secrets:
MES_TOKEN:
required: true| Field | Values | Default |
|---|---|---|
type | string, number, integer, boolean | string |
required | true stops the run when no value is set | false |
default | A value of the declared type. Config only | none |
description | Shown where the value is set | none |
Keys use upper case letters, digits and _. TOFUPILOT_* is reserved. A sequence file declares the keys its own phases read.
Read Values
In procedure.yaml
You can use {config.KEY} and {secrets.KEY} in with:, env: and unit default_value fields.
plugs:
- name: DMM
python: plugs.dmm:Dmm
with:
resource: "{config.DMM_RESOURCE}"
main:
- name: Upload
python: phases.mes:upload
with:
url: "{config.MES_URL}/api/results"
token: "{secrets.MES_TOKEN}"A value that is exactly one marker keeps its type. A marker inside a longer string is inserted as text.
In Python
You can add config or secrets to a phase's parameters, then read a key as an attribute or an item.
import time
def settle(config, log):
time.sleep(config.SETTLE_MS / 1000) # an int, as declared
log.info(f"MES at {config['MES_URL']}")| Case | Result |
|---|---|
| Key declared, no value | None. config.get("KEY", fallback) returns the fallback |
| Key not declared | AttributeError (KeyError for config["KEY"]) listing the declared keys |
| Assignment | An error: values are read-only |
print(secrets) | The keys only, never a value |
Set Values
You can set values in the dashboard, under Environment Variables, or with tofupilot env.
Preview
The dashboard pages are in preview. Add ?vars=1 to any dashboard URL to show them in your browser, ?vars=0 to hide them. tofupilot env works without it.
Each value is set at one of three levels:
| Level | Applies to | Where |
|---|---|---|
| Shared | Every procedure | Environment Variables with no procedure selected |
| Procedure | One procedure | The procedure's Environment Variables tab |
| Station | Every procedure on one station | The station's Environment Variables section |
Shared and procedure values target one or more environments: production, preview, development. Station values always count as production.
Stations receive a new value before their next run. A run in progress keeps the values it started with.
Permissions
| Role | Can |
|---|---|
| Viewer and above | List environment variables. Secrets show as digests |
| Developer | Edit Config values that do not target production |
| Admin, Owner | Edit everything, including Secrets and station values |
Operators cannot see environment variables.
Precedence
When a key is set at several levels, the narrowest one wins:
- Station
- Procedure, for the run's environment
- Shared, for the run's environment
- The station's own environment variable of the same name
- The
default:inprocedure.yaml
The run's environment is the one of the deployment the station runs. A preview build receives preview values, and never a production Secret. A station Secret only reaches production builds.
tofupilot env resolve shows which level wins for each key.
Development
Local runs (tofupilot run, Studio) never read values from TofuPilot. They read, in order:
- The shell environment
.env.localnext toprocedure.yaml.envnext toprocedure.yaml- The
default:
# Pull your development values into .env.local
tofupilot env pull
# Override one value for a single run
SETTLE_MS=500 tofupilot run .Keep .env.local and .env out of version control. Deployed runs never read them.
Station Not Configured
A required key without a value stops the run before any plug opens. The operator sees the missing keys:
Station not configured: config.DMM_RESOURCE, secrets.MES_TOKEN missingThe procedure's Environment Variables tab flags the keys too, and tofupilot service status lists them on the station. Set the values and start again.
With --json, the run emits run_crashed with kind: "not_configured".
Offline Stations
Stations keep the last values they received in an encrypted cache. A run still starts when the server is unreachable, using the cached values. After a new build is installed, cached Secrets stay unavailable until the station reaches the server again.
Run Records
Each run records the environment variables it used, under Environment Variables on the run page:
- A Config value: its key, value, version and level.
- A Secret: its key, version and level. Never the value.
The V2 API returns the same lists on GET /v2/runs/{id}, and accepts them on POST /v2/runs. You can send config from any client, for example a pytest upload recording a calibration offset.
Masking
Secret values of 8 characters or more are replaced by •••• in logs, the terminal UI, the kiosk, --json, the live stream and run uploads.
Masking catches accidents, like a token printed in a traceback. It cannot stop a phase that encodes or sends a secret elsewhere.
How is this guide?
Sequences
Write a group of phases once in a sequence file and call it from several TofuPilot procedures with uses, with inputs, plug bindings and its own outcome.
Run Context
Read the station, slot, procedure and deployment of a run with run.* in procedure.yaml and Python, and set process environment variables with env:.