Live demo — data resets daily at 03:00 UTC. Nothing you enter is saved. Server UI →

product: assert audience: test-developer authority: normative

Reference — Runner environment

The environment variables a test step can rely on inside a runner container, and the one that looks like it belongs here and does not.

Variable Value on a station Read by What it is for
ARTIFACT_DIR /data/artifacts both runners Where a step puts files it wants attached to the report (directly, or through Python's emit_artifact)
PACKAGES_ROOT /data/tat-packages both runners Root of the installed packages, mounted read-only

Both are set explicitly for the python-runner and dotnet-runner services in docker-compose.station.yml, and both runners fall back to the same defaults if a variable is missing.

ARTIFACT_DIR

The artifact staging directory. A step attaches a file to the execution report by putting it here. The executor empties the directory before every step; when the step returns (whatever its verdict), the runner enumerates the directory and reports what it finds, and the executor reconciles that list against the shared staging volume and uploads each file against the step that produced it. Only files directly in the directory are collected, not subdirectories, and a file larger than Artifacts:MaxFileSizeBytes (10 MB by default) is skipped with a warning. The API does not read ARTIFACT_DIR: its side of the shared artifact-data volume is Artifacts:StagingRoot, default /data/artifacts, and the two must name the same mount.

A Python step can call emit_artifact(path, description, content_type=None) or emit_plotly(fig, description) instead - runner builtins, no import - which copy the file here and attach a description and content type, which a bare file cannot carry. From Python runner 1.2.0 (Assert 2.3.0); see sdk/python-sdk.md. Writing the file yourself works on every version, in both runners, and outside a station:

import os
ARTIFACT_DIR = os.environ.get("ARTIFACT_DIR", "/data/artifacts")
static readonly string ArtifactDir =
    Environment.GetEnvironmentVariable("ARTIFACT_DIR") ?? "/data/artifacts";

Always read the variable with that fallback rather than hard-coding /data/artifacts. The variable is what keeps the module correct if a deployment moves the volume; the fallback is what keeps it runnable outside a station, in a unit test or on a developer laptop.

mkdir before writing. The directory exists on a running station because the volume is mounted, but os.makedirs(ARTIFACT_DIR, exist_ok=True) (or Directory.CreateDirectory) costs nothing and is what makes the same module work off-station.

The file name decides how the artifact is displayed — unless the step declared a content type through emit_artifact/emit_plotly, the reconcile path infers it from the extension alone. *.png and *.svg render inline, *.csv renders as a table, and a Plotly figure must be named *.plotly.json (the double extension is matched, a plain .json is not). Full table in sdk/python-sdk.md and sdk/dotnet-sdk.md.

PACKAGES_ROOT

The root of the installed package cache, mounted read-only into both runners. The Python runner re-scans it before every import and adds each <package>/python_modules directory to sys.path, so a package installed after the runner started is picked up without a restart.

A step therefore imports a module by its bare name — module: "generate_csv_artifact", not a path — and the module must sit directly in that package's python_modules/ directory.

ALLOW_ONLINE_PIP

Lets the Python runner fall back to PyPI when a package declares pipPackages for which it has no vendored wheels. Development stations only: a production station should ship its wheels, and a station that reaches PyPI at test time can have its dependencies change underneath it between runs.

Two things control it, and they are not redundant:

Set in When it applies Wins
ALLOW_ONLINE_PIP (environment, from .env) Runner startup, and any step where the config key is unset The default
AllowOnlinePip (station config) From the first step of a test onwards Overrides the environment, in both directions

The station config key is authoritative because it can be changed on a running station without touching the deployment, and an explicit false there turns PyPI fallback off even when .env asked for it. But it cannot be read until a step executes and the API injects it as cfg.AllowOnlinePip, and a freshly installed station has no way to set it before someone opens the UI - which is what the environment variable is for.

Both paths resolve through resolve_allow_online_pip() in dependency_installer.py, so the precedence is defined once.

Regression, 2026-05-13 to 2026-09-17. For four months nothing read ALLOW_ONLINE_PIP while .env.example and all three compose files went on setting it. A station that enabled it installed nothing from PyPI and reported no reason. Fixed; test_allow_online_pip.py pins the precedence in both directions.

What is not an environment variable

Thing Where it actually comes from
Scripts root (/app/scripts) The runner's command line, argument 3
Runner port and identifier The runner's command line, arguments 1 and 2
Station name, Orchestra URL, ingest key The API's environment, not the runners'
AllowOnlinePip (the station config key) Station configuration, delivered in step variables. Distinct from the ALLOW_ONLINE_PIP env var above, which it overrides.
An unhandled error has occurred. Reload

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.