Skip to main content

Variables

Variables let you parameterize Loom workflows and jobs without duplicating YAML. They are injected as environment variables during job execution and are also available in places that accept variable templates (such as cache key templates).

This page covers the three things that most often cause confusion: scalar values converted to text, which layer wins when keys collide, and how to debug precedence quickly.

Why variables matter

Without variables, every job that needs a shared value (a version string, an environment name, a build flag) would hard-code it. Variables let you define a value once and reference it everywhere — and override it at narrower scopes when you need to.

Variables affect two runtime artifacts:

  • Job environment: every variable is available as an environment variable in script: commands.
  • Cache key templates: variables can be interpolated into cache key expressions.

Types of variables

Loom has four categories of variables, each from a different source:

CategorySourceScopeExample
Workflow variablesvariables: block at workflow rootAll jobsAPP_ENV: "staging"
Default variablesdefault.variables: blockAll jobs (overridden by job variables)LANG: "en_US.UTF-8"
Job variablesvariables: block inside a jobSingle jobVERBOSE: "true"
Predefined runtime variablesProvided by Loom at execution timePer-pipeline or per-jobLOOM_RUN_ID, LOOM_PROVIDER

A fifth category — command-local overrides — comes from standard shell syntax (KEY=value command) inside a job's script: and affects only that single command.

The layering model

When the same variable key is defined in multiple places, Loom merges layers so that more specific scopes override more general scopes. The merge order from lowest to highest priority is:

 Lowest priority                                     Highest priority
┌──────────────┐ ┌───────────────┐ ┌──────────────┐ ┌────────────────┐
│ Predefined │ → │ Workflow │ → │ default + │ → │ Provider │
│ (registry) │ │ variables │ │ job vars │ │ overrides │
└──────────────┘ └───────────────┘ └──────────────┘ └────────────────┘

In practice, for the YAML layers you write, the relevant merge order is:

PriorityLayerWhere defined
1 (lowest)Workflow variablesRoot-level variables: block
2Default variablesdefault.variables: block
3 (highest)Job variablesJob-level variables: block

Default and job variables are merged via deep merge at the schema level — the default block's values are overlaid with the job's values before execution. Workflow-level variables provide the base, and job-level variables override keys from both default and workflow scope.

At runtime, Loom also injects predefined variables (like LOOM_RUN_ID and LOOM_PROVIDER). If your workflow defines a key that collides with a predefined variable name, the workflow value wins for that key.

Provider overrides (internal to Loom's execution engine) have the highest priority and cannot be overridden by workflow YAML.

Command-local overrides in script:

Inside a job's script:, you can use standard shell syntax to override a variable for a single command:

  • KEY=value command ... overrides KEY for that one command only.
  • The job's environment is unchanged for subsequent commands.

Precedence example

This example demonstrates how all three YAML layers interact:

version: v1
stages: [ci]

variables:
FOO: "workflow"
BAR: "workflow"

default:
target: linux
variables:
BAR: "default"

show-vars:
stage: ci
variables:
FOO: "job"
script:
- echo "FOO=$FOO BAR=$BAR"
- FOO="step" sh -c 'echo "FOO=$FOO (command-local override)"'
- echo "FOO=$FOO (back to job value)"

Expected output:

FOO=job BAR=default
FOO=step (command-local override)
FOO=job (back to job value)

What happened:

VariableWorkflow valueDefault valueJob valueResolved value
FOO"workflow""job""job" (job overrides workflow)
BAR"workflow""default""default" (default overrides workflow)

Predefined runtime variables

Loom provides runtime variables (prefixed LOOM_* and CI_*) that jobs can read. These are resolved at execution time based on the current run, job, and environment context.

Common examples:

VariableScopeDescription
LOOM_RUN_IDPipelineRun identifier — maps to .loom/.runtime/logs/<run_id>/
LOOM_PROVIDERJobExecution provider: host or docker
LOOM_JOB_NAMEJobCurrent job name (graph node ID)
CI_PROJECT_DIRJobWorkspace root directory where the job runs
CI_COMMIT_SHAPipelineSnapshot commit SHA for the current run

The authoritative list with scopes, availability, and stability metadata is at Predefined CI/CD variables (Loom).

caution

Avoid using CI_* or LOOM_* prefixes for your own variables. If you accidentally collide with a predefined variable name, the workflow value wins — but debugging becomes harder because the expected runtime value won't match.

Scalar values

Workflow and job variable values must be YAML scalars. Loom stores each scalar's text as the variable value, so unquoted numbers and booleans such as 3 and true are accepted as the text 3 and true. Lists, mappings, and other non-scalar values are rejected.

Quote values to make their intended text clear, especially values that YAML readers may interpret as numbers or booleans.

Incorrect (a list instead of a scalar):

variables:
RETRIES: [1, 2]

Recommended (explicit text values):

variables:
RETRIES: "3"
DEBUG: "true"

For a non-scalar value, the validator reports set variable value to a string at the offending key. For the invalid list above, that key is /variables/RETRIES. The diagnostic says "string", but the check accepts scalar values regardless of their implicit YAML type.

Variable keys must also follow a strict pattern: ^[A-Z_][A-Z0-9_]*$ (uppercase letters, digits, and underscores only).

How to debug precedence

When "the value I expected isn't the value I got," follow this checklist:

  1. Validate first: run loom check to catch schema-level mistakes (wrong keys, non-scalar values, typos in variable names).

  2. Confirm the layers: identify which layers define the key:

    • Workflow variables: (global defaults)
    • default.variables: (job-default overrides)
    • Job variables: (job-specific overrides)
    • Command-local overrides in script: lines (KEY=value cmd)
  3. Print what the job actually sees: add a temporary script line:

    env | grep -E '^CI_|^LOOM_|^(FOO|BAR)=' | sort
  4. Inspect the compiled graph: run loom compile to see the resolved variables for each job in the Graph IR JSON output.

  5. If the run fails, follow pointers instead of guessing:

  6. When sharing for help: use What to share and redact sensitive values.

Sensitive values belong in secrets, not variables

If a value is a password, token, certificate, or private key, use Secrets instead of variables. secrets entries store references (not plaintext values) in workflow YAML, and Loom redacts exact matches to resolved secret values in its supported console, log, and receipt output. Transformed values and raw declared artifacts remain outside that protection, so inspect evidence before sharing it.

VariablesSecrets
Values visible in workflow YAMLYesNo in secrets entries (references only)
Exact secret matches in supported console, log, and receipt outputUnprotectedRedacted
Transformed values and raw declared artifactsUnprotectedNot covered by textual redaction
Can coexist on same key in one jobNo — schema rejects this

To migrate existing sensitive variables, see Migrating from variables to secrets.

caution

Be careful when printing environments for debugging. Paths can be sensitive — variables like CI_PROJECT_DIR or LOOM_PROJECT_DIR can expose usernames or internal mount points. Share the receipt path + run ID first and redact any sensitive values per What to share.