Environment Variables

Last updated on October 22, 2026

PreviewThis feature is in preview, so its scope and behavior may change. Request access.

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:report
import 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:

ConfigSecret
Declared underconfig:secrets:
Use forAddresses, URLs, settingsPasswords, API keys, tokens
Readable after savingYesNo, a digest is shown
Recorded on each runThe valueThe version only
Masked in logsNoYes

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.

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
FieldValuesDefault
typestring, number, integer, booleanstring
requiredtrue stops the run when no value is setfalse
defaultA value of the declared type. Config onlynone
descriptionShown where the value is setnone

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.

procedure.yaml
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.

phases/settle.py
import time

def settle(config, log):
    time.sleep(config.SETTLE_MS / 1000)  # an int, as declared
    log.info(f"MES at {config['MES_URL']}")
CaseResult
Key declared, no valueNone. config.get("KEY", fallback) returns the fallback
Key not declaredAttributeError (KeyError for config["KEY"]) listing the declared keys
AssignmentAn 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:

LevelApplies toWhere
SharedEvery procedureEnvironment Variables with no procedure selected
ProcedureOne procedureThe procedure's Environment Variables tab
StationEvery procedure on one stationThe 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

RoleCan
Viewer and aboveList environment variables. Secrets show as digests
DeveloperEdit Config values that do not target production
Admin, OwnerEdit everything, including Secrets and station values

Operators cannot see environment variables.

Precedence

When a key is set at several levels, the narrowest one wins:

  1. Station
  2. Procedure, for the run's environment
  3. Shared, for the run's environment
  4. The station's own environment variable of the same name
  5. The default: in procedure.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:

  1. The shell environment
  2. .env.local next to procedure.yaml
  3. .env next to procedure.yaml
  4. The default:
Terminal
# 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:

Kiosk
Station not configured: config.DMM_RESOURCE, secrets.MES_TOKEN missing

The 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?

On this page