Skip to main content

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.

Secrets are available for sensitive values

If a value is sensitive (password, token, private key), use Secrets instead of variables. secrets entries store references, inject via file path by default, and redact exact matches to resolved values in supported console, log, and receipt output. Transformed values and raw declared artifacts remain outside that protection, so inspect evidence before sharing it. 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

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:

  • variables must 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 YAML scalars. Loom stores the scalar text, including unquoted numbers and booleans, as a string. Lists, mappings, and other non-scalar values produce a schema error. Quote values to make the intended text clear.
  • A job cannot define the same key in both variables and secrets. 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)SourceDescription
1Predefined (system) variablesCI_*, LOOM_*, and other variables resolved from the registry (pipeline, job, and step scopes).
2Workflow variablesTop-level variables and default.variables, merged into each job via the default merge process.
3Job variablesVariables defined directly on the job override workflow-level values for the same key.
4Provider overridesVariables injected by the runtime provider (Host or Docker) take final precedence.

Within layer 2, the default merge process works as follows:

  • default.variables and top-level variables both 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:

JobMODESHARED
print-defaultdefaultfrom-default
print-overridejob-overridefrom-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. Declare required non-sensitive values in workflow or job variables to reduce environment differences between machines and 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. Docker jobs receive declared and predefined Loom variables plus image defaults. They do not inherit the host environment, and $VAR in a YAML variable value remains a literal string.

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

For a non-sensitive variable, capture its runtime value and inspect the corresponding event stream:

  1. Print the variable from your script: printf '%s\n' "$MY_VAR"
  2. Follow the Diagnostics ladder to the step's events.jsonl that captured the stdout output.
  3. 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:

  1. Validate schema first: run loom check to rule out invalid names or value types.
  2. 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_* or LOOM_* variable shadowing your key?
  3. Confirm provider routing:
    • No image: → Host provider (inherits host env)
    • image: set → Docker provider (isolated env)
  4. Prove the runtime value: print the variable from the job script, then follow pointers to the step events.jsonl via the Diagnostics ladder.
  5. 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).