Variables
Variables are key/value string pairs attached to a workflow or a job. They are the primary mechanism to parameterize scripts without duplicating jobs.
For predefined variables that Loom injects into every job environment (including CI_* and LOOM_*), see Predefined CI/CD variables (Loom).
If you're looking for the broader conceptual model and common pitfalls, see Concepts → Variables.
If a value is sensitive (password, token, private key), use Secrets instead of variables. Secrets store references only, inject via file path by default, and are automatically redacted in all runtime output. See Concepts → Secrets for the mental model, or Migrating from variables to secrets for a step-by-step guide.
Syntax
Variables can be defined at three levels in the workflow YAML. For canonical schema anchors, see:
- Workflow-level
variables: Syntax (v1) →variables - Default
variables: Syntax (v1) →default - Job
variables: Syntax (v1) → Jobvariables
Workflow-level variables
variables:
PNPM_STORE_DIR: .pnpm-store
NODE_ENV: production
Default variables
default:
variables:
PNPM_STORE_DIR: .pnpm-store
Job-level variables
check:
stage: ci
target: linux
variables:
PNPM_STORE_DIR: .pnpm-store
script:
- echo "$PNPM_STORE_DIR"
Schema rules
The schema validator enforces:
variablesmust be a YAML mapping.- Variable names must match
^[A-Z_][A-Z0-9_]*$(uppercase letters, digits, underscores; must start with a letter or underscore). - Values must be strings. Lists, mappings, numbers, and booleans produce a schema error.
- A job cannot define the same key in both
variablesandsecrets. See Syntax (v1) →secrets.
Precedence
Variables are merged before the job executes. The merge applies in layers, where later layers override earlier ones for the same key:
| Priority (lowest → highest) | Source | Description |
|---|---|---|
| 1 | Predefined (system) variables | CI_*, LOOM_*, and other variables resolved from the registry (pipeline, job, and step scopes). |
| 2 | Workflow variables | Top-level variables and default.variables, merged into each job via the default merge process. |
| 3 | Job variables | Variables defined directly on the job override workflow-level values for the same key. |
| 4 | Provider overrides | Variables injected by the runtime provider (Host or Docker) take final precedence. |
Within layer 2, the default merge process works as follows:
default.variablesand top-levelvariablesboth contribute default values.- The default mapping is merged into each job mapping before validation.
- For
variables(a mapping), keys merge recursively — the job provides overrides, the default provides fallbacks.
Merge example
version: v1
stages: [ci]
default:
target: linux
variables:
MODE: default
SHARED: from-default
print-default:
stage: ci
script: ["printf '%s\n' \"$MODE\" \"$SHARED\""]
print-override:
stage: ci
variables:
MODE: job-override
script: ["printf '%s\n' \"$MODE\" \"$SHARED\""]
Final values:
| Job | MODE | SHARED |
|---|---|---|
print-default | default | from-default |
print-override | job-override | from-default |
print-override overrides MODE via its own variables but inherits SHARED from the default.
Provider behavior
Variables interact with providers in ways that matter for reproducibility:
Host provider
When no image: is set, the job runs on the host shell. The host provider inherits the environment of the loom run --local process, then applies workflow and job variables on top.
Recommendation: don't rely on shell environment variables that happen to be set on your machine. Put required values in workflow or job variables so runs behave identically on other machines and in CI.
See Host provider → Runtime behavior.
Docker provider
When image: is set, the job runs inside a container. The Docker provider passes merged variables into the container environment. Host environment variables are not inherited unless explicitly set via variables.
Recommendation: treat Docker jobs as isolated environments. Assume only variables declared in the workflow are available.
See Docker provider → Runtime behavior.
How to confirm the final value
Because Loom is pointer-first, you can confirm the resolved value from runtime artifacts:
- Print the variable from your script:
printf '%s\n' "$MY_VAR" - Follow the Diagnostics ladder to the step's
events.jsonlthat captured the stdout output. - If sharing for support, start with the receipt path (see Receipts contract) and follow What to share.
Troubleshooting: precedence surprises
Use this checklist when a job sees an unexpected variable value:
- Validate schema first: run
loom checkto rule out invalid names or value types. - Identify the layer:
- Is the variable set in
default.variables? - Is it overridden in job
variables? - Is it coming from the host environment via the Host provider?
- Is a predefined
CI_*orLOOM_*variable shadowing your key?
- Is the variable set in
- Confirm provider routing:
- No
image:→ Host provider (inherits host env) image:set → Docker provider (isolated env)
- No
- Prove the runtime value: print the variable from the job script, then follow pointers to the step
events.jsonlvia the Diagnostics ladder. - Share minimal evidence: follow What to share — receipt path, failing unit pointer, small excerpt.
Planned
- Explicit multi-layer precedence documentation covering template extends chains.
- Typed variables and interpolation rules.
- Variable expansion in non-script contexts (e.g.
image, cache keys).