Config and Secrets

Last updated on October 21, 2026

Config and secrets are the values a procedure reads that do not belong in its repository: an instrument address that differs per station, an MES URL per environment, an API token. The procedure declares the keys it reads under config: and secrets:. Their values come from environment variables set in the dashboard or with tofupilot env: a Config environment variable feeds a config: key, a Secret feeds a secrets: key. During development they come from the shell, .env.local and .env.

ConfigSecrets
ExampleDMM_RESOURCE, MES_URLMES_TOKEN
Readable in the dashboard and the CLIYesNever: write-only, a digest is shown
Recorded on each runThe valueThe stored value's ID and version, never the value
Masked in logsNoYes, see Masking
default: in the YAMLYesNo

Config values are not confidential: everyone who can read a run sees them. Put anything sensitive in a secret, and read the trust model before you store production credentials.

Declare

procedure.yaml
name: Board Functional Test

config:
  DMM_RESOURCE:
    required: true
    description: VISA resource of the bench DMM
  MES_URL:
    default: https://mes.example.com
  SETTLE_MS:
    type: integer
    default: 200

secrets:
  MES_TOKEN:
    required: true
    description: MES API token of this environment
FieldValuesDefault
typestring, number, integer, booleanstring
requiredtrue: no run starts without a valuefalse: a missing value reads as None
defaultA value of the declared type. Config only, and not with required: truenone
descriptionFree text, shown where the value is setnone

A key is upper case letters, digits and _, not starting with a digit. TOFUPILOT_* is reserved. A key is a config value or a secret, never both. A key read anywhere must be declared: `{config.MES_ULR}` is not declared: add `MES_ULR` under config:; did you mean `MES_URL`?.

Read

In procedure.yaml

{config.KEY} and {secrets.KEY} stand for the value in with:, env: and unit default_value fields, the same places as {run.*}. A string that is exactly one marker takes the value and its type; a marker inside a longer string is written in as text, and a missing value as nothing.

procedure.yaml
plugs:
  - name: DMM
    key: dmm
    python: plugs.dmm:Keysight34465A
    with:
      resource: "{config.DMM_RESOURCE}"

main:
  - name: Upload Result
    python: phases.mes.upload
    with:
      url: "{config.MES_URL}/api/results"
      token: "{secrets.MES_TOKEN}"

In Python

A phase takes config or secrets as a parameter and reads a key as an attribute or an item:

phases/settle.py
import time

def settle(config, secrets, log):
    # Values are typed as declared: SETTLE_MS is an int
    time.sleep(config.SETTLE_MS / 1000)
    log.info(f"MES at {config['MES_URL']}")
    token = secrets.MES_TOKEN
BehaviourDetail
Missing valueA declared key without a value reads as None. config.get("KEY", fallback) returns the fallback then
Undeclared keyAttributeError (or KeyError) naming the keys declared for this phase
WritesRead-only: assigning raises AttributeError
print(secrets)Shows the keys only, never a value

A plug or a phase keyed config or secrets keeps its name: the parameter then receives the plug or the phase result, as before.

Types

Values set in the dashboard, the CLI, a .env file or the shell are text, read as the declared type when the run starts:

typeAccepted text
stringAnything
integer30, -4
number1.5, 30; not nan or infinity
booleantrue, false, 1, 0, yes, no, on, off, any case

A value that does not read as its type stops the run before it starts: config.SETTLE_MS is declared integer but its value (from station) is not one.

In sequences

A sequence file declares the config and secrets its phases read, under its own config: and secrets:. The values come from the procedure's run: a sequence never sets one. Its phases read only the keys their file declares.

Set values

Values are environment variables, set in the dashboard or with tofupilot env. Each has a type:

TypeFeedsAfter saving
Secret (default)A secrets: keyYou can't reveal this value after saving. Use for passwords, API keys and tokens
ConfigA config: keyReadable after saving for members with access. Use for non-sensitive values

and one of three levels:

LevelWhere in the dashboardTargets
SharedEnvironment Variables with no procedure selected (also under Settings)Every procedure of the organization, per environment
ProcedureProcedure → Environment Variables → ProcedureThat procedure, per environment
StationStation page → Environment VariablesEvery procedure on that station; counts as production

Shared environment variables are created, edited and deleted on the Environment Variables page with no procedure selected. A procedure's Shared tab lists the ones it inherits, read-only; override one with a procedure value.

Each row's menu offers Edit, Rotate (a new Secret value), Copy to Clipboard, View History and Delete. The optional Note says where to rotate a value or who to contact. Value Type (string, number, integer, boolean) is checked like the YAML type. Needs Attention flags a Config whose key looks like a secret: make it a Secret with Edit. A Secret cannot become a Config.

A Shared or procedure value targets one or more environments: production, preview, development. One key can have several values at one level, as long as no two share an environment. A station value has no environment. A station value can be promoted to the procedure from the station page.

Saving a value notifies the stations it reaches; they fetch the new values before their next run. A run in progress keeps the values it started with.

Permissions

RoleCan
Viewer and aboveList environment variables (Secrets as digests only)
DeveloperCreate, edit and delete Config variables that do not target production
Admin, OwnerAlso Secrets, station variables and anything that targets production

Operators cannot read environment variables. See Access Roles.

Precedence

The narrowest level wins:

  1. Station
  2. Procedure, for the run's environment
  3. Shared, for the run's environment
  4. The station process's environment variable of the same name (deployed runs only)
  5. The default: in procedure.yaml

The run's environment is the environment of the deployment the station runs for that procedure (run.deployment.environment): a preview build reads preview values, a production build production values. A procedure the station runs without a deployment counts as production for config, and receives no secret. Secrets reach only builds of the same environment as the build the station is meant to run: an older production build kept because the update has not been installed yet still receives production secrets, a preview build waiting to be replaced by a production one receives none.

Secrets follow extra rules, so a build only reaches the secrets of its own environment:

  • A procedure or Shared Secret applies only to builds of the environments it targets.
  • A station Secret applies only to production builds. A preview build on a production station does not receive it.

A station in an access group receives the same values as any other station. Suppliers are kept off your stations by the deploy, link and team-change checks, not by withholding secrets.

tofupilot env resolve shows which level wins for each key and what it shadows.

Development

A source run (tofupilot run ./my-test, or Studio) never reads values from TofuPilot. It reads:

  1. The shell environment variable of the same name,
  2. then the .env.local file next to procedure.yaml,
  3. then the .env file next to it,
  4. then the default:.

Its run.deployment.environment is development. Only declared keys are read; other names in the files or the shell are ignored.

.env.local
DMM_RESOURCE="TCPIP0::192.168.1.50::INSTR"
MES_URL="https://mes-sandbox.example.com"
MES_TOKEN="dev-token-123"
Terminal
# Override one value for a single run
SETTLE_MS=500 tofupilot run ./my-test

tofupilot env pull writes the procedure's development values into .env.local (mode 0600). A Secret is included only when it targets development. Keep .env.local and .env out of version control: a deployed run never reads them.

Station not configured

A required config value or secret without a value stops the run before anything starts: no plug opens and no unit is identified. The kiosk and the terminal UI show Station not configured with the missing keys, instead of "Run failed":

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

With --json, the run emits run_crashed with kind: "not_configured" and a missing list of {kind, key}, and tofupilot run exits with code 5 (error). A deployed station also reports the missing keys to TofuPilot, which flags them on the procedure's Environment Variables tab, and tofupilot service status lists them. Set the values and start again; a successful run clears the report.

Offline stations

A station keeps the last values it received in an encrypted cache, so runs start without the server. Before a deployed run, the station fetches fresh values for up to 5 seconds, then falls back to the cache, marked stale. Cached secrets only serve the deployment they were fetched for: after a new build is pulled, a run that cannot reach the server keeps the cached config but not the cached secrets, so a run that needs them is not configured until a fetch succeeds. tofupilot service status shows when the cache was last fetched and where its key is kept (credential-manager, keychain or file, a 0600 key file, always on Linux). When the key cannot be opened, the run still uses the values it fetched, without caching them. A key file that holds no key is renamed values.key.unreadable-<time> and replaced, and the cache is fetched again.

Station plugs

A station plug is reused across runs only while its resolved with:, the procedure's env: and the station login are unchanged. A changed config value or a rotated secret respawns that plug, and only that plug, on the next run.

Recorded on runs

Each run records the config and secrets it used, shown in the run's Environment Variables section:

  • A config value: its key, value, the stored value's ID and version, and its source.
  • A secret: its key, the stored value's ID and version, and its source. Never the value.

The source is station, procedure, organization (Shared), process, dotenv (.env.local or .env), shell or default. Identical sets are stored once, so recording them costs nothing per run. A config value longer than 32 KiB (32,768 characters) is left out of the record with a warning, and the run still uploads.

The V2 API carries the same lists: POST /v2/runs accepts optional config ({key, value, variable_id?, version?, source?}) and secrets ({key, variable_id?, version?, source?}), and GET /v2/runs/{id} returns them. A value on a secret is rejected with a 400. Any client can send config, for example an OpenHTF or pytest upload recording a calibration offset.

Masking

Every secret value of 8 characters or more is replaced by •••• wherever it would leave the CLI: the console and log file, the terminal UI, the kiosk, --json, the dashboard's live stream, and the run upload (logs, error messages, measurements, metadata). A number whose digits are a secret (an 8-digit PIN recorded as a measurement) becomes the string ••••, and so does a secret used as a key of a dictionary. Shorter values are not masked, since masking on or 42 would hide every occurrence of a common word.

Masking replaces exact matches only. It protects against accidents, such as a token printed in a traceback. It does not stop a phase that encodes, splits or sends a secret elsewhere. See the trust model.

How is this guide?

On this page