Run Context
Last updated on October 21, 2026
The run context tells a procedure where it runs: the slot, the station, the procedure and the deployment. A phase reads it on its run parameter, and procedure.yaml reads the same fields through {run.*} markers.
Fields
| Field | Value |
|---|---|
run.slot.key | The slot's key from execution.slots. None in a phase that runs once for every slot. |
run.slot.name | The slot's display name, or its key when it has none. |
run.station.id | The station's ID. None on a source run. |
run.station.name | The station's name. On a source run, the machine's hostname. |
run.procedure.id | The procedure's ID. On a source run, set only when the directory is linked. |
run.procedure.name | The procedure's name. |
run.procedure.version | The version: of procedure.yaml. |
run.deployment.id | The deployment the station runs. None on a source run. |
run.deployment.environment | production, preview or development. Source and Studio runs are development. |
run.deployment.commit | The deployed commit SHA. |
run.deployment.branch | The branch the deployed commit is on. |
A field the CLI does not know reads as None. Every field is read-only.
In Python
def report(run, log):
# Read-only context of this run
log.info(f"{run.station.name} / {run.slot.name} on {run.deployment.environment}")
if run.retry.count > 0:
log.warning(f"Retry {run.retry.count} (limit {run.retry.limit})")
run.operated_by = "jdoe" # the only writable field, with run.metadataBesides the fields above, run carries run.retry.count (0 on the first attempt) and run.retry.limit, plus the writable run.operated_by and run.metadata.
In procedure.yaml
{run.<field>} stands for a field in the values that are kept as written until the run starts. A string that is exactly one marker takes the field's value and type; a marker inside a longer string is written in as text.
execution:
slots: 4
plugs:
- name: Fixture
python: plugs.fixture:Fixture
with:
nest: "{run.slot.key}"
label: "{run.station.name}-{run.slot.name}"
unit:
serial_number:
default_value: "BURNIN-{run.slot.key}"Where markers are allowed
The markers resolved at run start ({run.*}, {config.*} and {secrets.*}) are allowed only in:
with:of a phase or a plug (config:included),env:of the procedure, a phase or a plug,- a unit field's
default_value, and the rootoperated_by:'s.
Anywhere else they stay as written, with a warning: `{run.slot.key}` is kept as text: markers resolve only in `with:`, `env:` and a unit field's `default_value`. Typed fields such as limits, timeout and retry are checked when the procedure loads, before these values exist.
A sequence call's with: is resolved when the procedure loads, so it cannot pass them either. Declare the key in the sequence file and read it there.
{inputs.*} markers of a sequence are resolved at load and work in any field of the sequence file.
Braces naming any other namespace are text, so a plug keeps its own format strings ("{record.levelname}: {record.message}"). A name close to a marker namespace, such as {configs.MES_URL}, gets a warning that suggests the marker.
Slot fields
{run.slot.*} needs a slot. It is a load error where one instance serves every slot:
| Place | Why it has no slot |
|---|---|
The procedure's env: | Shared by every process of the run |
with: or env: of an execution or station plug | One instance serves every slot, or every run |
A setup or teardown phase with scope: execution | Runs once for all slots |
An unknown field is a load error that lists the valid ones and suggests the closest, for example unknown field `{run.slot.nme}`… did you mean `{run.slot.name}`?.
Old names
The names below still work and nothing is scheduled to remove them. In procedure.yaml, the old spellings load with a deprecation warning at their line; the old Python attributes are kept without one.
| Old | New |
|---|---|
{slot} | {run.slot.key} |
{slot_name} | {run.slot.name} |
run.slot_id | run.slot.key (<shared> in a phase that runs once for every slot) |
run.retry_count | run.retry.count |
run.retry_limit | run.retry.limit |
config: on a plug | with: |
{slot} and {slot_name} are aliases only where run-start markers resolve. Elsewhere, for example in a phase description, and under a plug's old config: key, they stay plain text as before. Rename config: to with: to have them resolve.
Environment variables
env: sets environment variables on the processes of the run. Set it on the procedure, a phase or a plug:
env:
MES_URL: "{config.MES_URL}"
LOG_LEVEL: debug
PORT: 8080 # read as the text "8080"
plugs:
- name: DMM
python: plugs.dmm:Dmm
env:
VISA_LIBRARY: "@py"
main:
- name: Flash
executable: ./flash.sh
env:
FLASH_BAUD: "921600"| Level | Applies to |
|---|---|
Procedure env: | Every phase worker and plug process of the run |
Plug env: | That plug's process, over the procedure's |
Phase env: | That phase while it runs (its module import included), over the procedure's; restored after |
Values are text: a number or a boolean is read as its text. Markers work as in with:, except {run.slot.*} in the procedure's env:. Keys are letters, digits and _, not starting with a digit, and TOFUPILOT_* is reserved.
A station plug serves runs of several procedures, so it only receives the procedure's env:. A plug env: on it is a load error.
{secrets.X} in env: loads with a warning: every child process inherits the environment. Pass a secret through with: or read secrets in the phase instead.
How is this guide?
Config and Secrets
Declare config and secrets in procedure.yaml, feed them from Config and Secret environment variables per station and environment, and read them in YAML and Python phases.
Logs
Log structured messages during test execution for debugging, traceability, and post-run analysis of TofuPilot test procedures.