How to Configure Station Variables with TofuPilot

Last updated on October 21, 2026

Learn how to run one procedure on several benches when each bench has its own DMM address and each environment talks to its own MES. The addresses become station Config environment variables, the MES URL a Config per environment and the MES token a Secret per environment, all set in TofuPilot instead of the repository.

KeyTypeRead asSet per
DMM_RESOURCEConfig{config.DMM_RESOURCE}station
MES_URLConfig{config.MES_URL}environment
MES_TOKENSecret{secrets.MES_TOKEN}environment

Prerequisites

  • A procedure linked to TofuPilot and deployed with git deployments.
  • Two stations running it, here line-1-bench-1 and line-1-bench-2.
  • The Admin or Owner role to set station variables and Secrets, and the CLI logged in as yourself (tofupilot login).

Step 1: Declare the keys

Declare what the procedure reads, then use the keys in with:.

procedure.yaml
name: Board Functional Test
version: 1.4.0

config:
  DMM_RESOURCE:
    required: true
    description: VISA resource of this bench's DMM
  MES_URL:
    required: true

secrets:
  MES_TOKEN:
    required: true

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

main:
  - name: Measure 3V3
    python: phases.rails.measure_3v3
  - name: Report to MES
    python: phases.mes.report
    with:
      url: "{config.MES_URL}"
      token: "{secrets.MES_TOKEN}"

The plug receives the resource as a constructor argument, and the phase receives the URL and the token as function arguments:

plugs/dmm.py
import pyvisa

class Dmm:
    # resource comes from {config.DMM_RESOURCE}, set per station
    def __init__(self, resource):
        self.inst = pyvisa.ResourceManager().open_resource(resource)

    def read_voltage(self):
        return float(self.inst.query("MEAS:VOLT:DC?"))

    def close(self):
        self.inst.close()
phases/mes.py
import requests

def report(unit, run, url, token):
    # url and token come from with:, resolved for this run's environment
    response = requests.post(
        f"{url}/results",
        headers={"Authorization": f"Bearer {token}"},
        json={"serial": unit.serial_number, "station": run.station.name},
        timeout=10,
    )
    response.raise_for_status()

Step 2: Run it locally with .env.local

A source run reads the shell, then .env.local, then .env next to procedure.yaml, never the values stored in TofuPilot. Write the bench on your desk and the MES sandbox:

.env.local
DMM_RESOURCE="TCPIP0::192.168.1.50::INSTR"
MES_URL="https://mes-sandbox.example.com"
MES_TOKEN="sandbox-token-0001"
Terminal
echo ".env*" >> .gitignore
tofupilot run .

Once development values are stored in TofuPilot (Step 3), tofupilot env pull writes this file for you.

Step 3: Set the values

Set the DMM address on each station as a Config. A station variable counts as production and applies to every procedure the station runs:

Terminal
tofupilot env add DMM_RESOURCE --station line-1-bench-1 --no-sensitive --value "TCPIP0::10.0.1.21::INSTR"
tofupilot env add DMM_RESOURCE --station line-1-bench-2 --no-sensitive --value "TCPIP0::10.0.1.22::INSTR"

Set the MES URL per environment on the procedure, from the linked directory:

Terminal
tofupilot env add MES_URL production --no-sensitive --value https://mes.example.com
tofupilot env add MES_URL preview,development --no-sensitive --value https://mes-sandbox.example.com

Set the token per environment. Secret is the default type, and the CLI asks for each value at a hidden prompt:

Terminal
tofupilot env add MES_TOKEN production --note "Rotate in the MES admin console"
tofupilot env add MES_TOKEN preview,development

The same values can be set in the dashboard: the station page's Environment Variables section, and the procedure's Environment Variables tab.

Step 4: Check what each station receives

Terminal
tofupilot env resolve production --station line-1-bench-2
Output
Key            Value                       Wins       Shadows
DMM_RESOURCE   TCPIP0::10.0.1.22::INSTR    station    -
MES_URL        https://mes.example.com     procedure  -

A Secret shows a digest, never its value. The narrowest level wins: station, then procedure, then Shared.

Step 5: Deploy and run

Merge to the production branch. The stations pull the build, fetch their values and keep them in an encrypted cache, so a run also starts when the server is unreachable. Saving a value later notifies the stations; the next run uses it, and the DMM plug reconnects with the new address.

A station missing a required value does not start the run. The operator sees Station not configured with the missing keys, the procedure's Environment Variables tab flags them, and tofupilot service status on the station lists them:

tofupilot service status
Board Functional Test: not configured: config.DMM_RESOURCE missing

Each run then records the values it used in its Environment Variables section: the DMM address and MES URL with their level and version, and MES_TOKEN by version only.

A preview build pushed to a line station receives the preview MES token, and no Secret set on the station itself. Production credentials only reach production builds. See the trust model for who can read a station's secrets.

Step 6: Rotate the token

Terminal
tofupilot env update MES_TOKEN production

Or use Rotate in the row menu of the procedure's Environment Variables tab. The value is replaced and its version increases. Runs from then on record the new version, so you can tell which runs used which token. Only a value of 8 characters or more is masked in logs, so use long tokens.

How is this guide?

On this page