Cache
Cache configuration describes what directories and files to preserve between workflow runs, and how cache keys are computed. Loom restores those paths before running the job script. Package managers and build tools can reuse them to avoid work; the benefit depends on the workload.
A completed save publishes immediately for later jobs and runs with the same scope and key, including across commits. This applies to native Loom and GitLab workflows using host or Docker execution. Ordinary saves retain their producing revision as provenance and do not claim cache-diff verification.
For runtime cache behavior (where hit/miss shows up, host vs Docker constraints), also see:
Quick example
check:
stage: ci
target: linux
cache:
key:
prefix: loom-cache
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
- .nx/cache
policy: pull-push
when: always
script:
- pnpm install --frozen-lockfile
- pnpm nx run-many -t check
Schema rules
The schema validator enforces these constraints (verified via loom check):
Cache forms
| Form | Description |
|---|---|
| Mapping (single cache) | A single cache configuration applied to the job. |
| Sequence (multiple caches) | A list of named cache entries, each operating independently. |
null or [] | Explicitly disables caching for the job, overriding any default.cache. |
Cache subkeys
| Subkey | Required | Description |
|---|---|---|
paths | Yes (unless disabled: true) | Non-empty list of directories/files to cache. |
key | No | Cache key as a string or structured mapping. |
fallback_keys | No | Fallback keys tried in order if the primary key misses. |
policy | No | When to restore/save. Default: pull-push. |
when | No | Save based on job status. Default: on_success. |
name | Only in sequence form | Unique name identifying the cache entry. |
disabled | No (sequence only) | Disable a named cache entry without removing it. |
Unknown keys produce: remove unknown cache key; allowed keys are name, disabled, paths, key, fallback_keys, policy, when.
paths
Non-empty list of non-empty strings. Each string is a path relative to the project workspace root.
cache:
paths:
- .pnpm-store
- .nx/cache
- node_modules
For Docker jobs (image: set), paths refer to directories inside the container's workspace mount. Mismatches between host and container paths are a common source of "cache did nothing" issues — see Operational constraints.
key
Cache key determines which cached archive to restore and where to save. Runs in the same repository scope that produce the same key share the published cached data, even when their source revisions differ.
String form
A non-empty string that supports template variable placeholders.
cache:
key: "pnpm-${job_name}-${head_sha}"
paths:
- .pnpm-store
Mapping form
A mapping with an optional prefix and a required files list.
cache:
key:
prefix: loom-cache
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
| Subkey | Required | Description |
|---|---|---|
prefix | No | Non-empty string prepended to the files digest as prefix-digest. Supports template placeholders. Omit it for a bare digest. |
files | Yes | Non-empty list of file paths or glob patterns. When file contents change, a new cache key is generated. |
Files-only keys
When prefix is omitted, the key is the files digest alone: 64 lowercase hex characters (SHA-256 over the matched paths and contents) with no dash. With a prefix, the key is prefix-digest. This follows GitLab's cache:key:files semantics and applies to both source formats: a .loom/workflow.yml mapping key and a .gitlab-ci.yml cache:key:files entry without prefix both resolve to the digest at run time; neither format injects a synthetic prefix. A key with no prefix, no files, and no string value is rejected.
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
Glob support in files: entries support *, ?, and ** (doublestar recursive semantics). For example, "**/go.sum" matches go.sum files in any subdirectory.
cache:
key:
prefix: loom-cache-go
files:
- go.work
- go.work.sum
- "**/go.sum"
paths:
- .go
If key is omitted, the runtime uses a default key computation.
fallback_keys
Non-empty list of non-empty strings tried in order when the primary key misses. Supports template variable placeholders.
Fallback applies to missing keys. A quarantined, rejected, malformed, or corrupt candidate stays cold without trying another key.
cache:
key: "pnpm-${head_sha}"
fallback_keys:
- "pnpm-main"
- "pnpm-default"
paths:
- .pnpm-store
policy
Controls when the cache is restored and saved.
| Value | Restore at start | Save after finish | Use case |
|---|---|---|---|
pull-push (default) | Yes | Yes | Standard: restore and update the cache each run. |
pull | Yes | No | Read-only: jobs that consume but don't update the cache. |
push | No | Yes | Write-only: jobs that build/populate the cache for others. |
install:
stage: deps
target: linux
cache:
paths: [.pnpm-store]
policy: pull-push
script:
- pnpm install
lint:
stage: ci
target: linux
cache:
paths: [.pnpm-store]
policy: pull
script:
- pnpm run lint
In this example, install both restores and saves the cache. lint only restores it, avoiding a redundant save since the cache contents didn't change.
when
Controls when the cache is saved based on the job's exit status.
| Value | Behavior |
|---|---|
on_success (default) | Save only when the job succeeds. |
on_failure | Save only when the job fails. |
always | Save regardless of exit status. |
check:
stage: ci
target: linux
cache:
paths: [.pnpm-store]
when: always
script:
- pnpm install
- pnpm test
Use when: always when the cached artifacts (like installed dependencies) are valid regardless of whether the job's tests pass or fail.
name
Required when cache is a sequence. Each entry must have a unique name.
Duplicate names produce: use unique cache names; "xyz" is duplicated.
default:
cache:
- name: pnpm
key:
prefix: loom-cache
files: [pnpm-lock.yaml]
paths: [.pnpm-store]
policy: pull-push
when: always
- name: go
key:
prefix: loom-cache
files: [go.work, "**/go.sum"]
paths: [.go]
policy: pull-push
when: always
disabled
Set disabled: true in a sequence entry to disable that named cache without removing it. When disabled, paths is not required.
check:
stage: ci
target: linux
cache:
- name: pnpm
disabled: true
- name: go
paths: [.go]
script:
- go test ./...
Disabling cache entirely
To override a default.cache for a specific job, set cache to null or []:
default:
cache:
paths: [.pnpm-store]
policy: pull-push
when: always
build-image:
stage: deps
target: linux
cache: []
script:
- docker build -t myapp .
Cache key template variables
Native key, files-prefix and fallback fields also accept known authored variable
references after composition and caller precedence. Their resolved literal parts
are bound once; dollar bytes from a rich expand:false value stay data. Unknown
template names retain validation errors. See rich values
for GitLab's distinct main-key and fallback expansion passes.
When cache.key is a string, or in cache.key.prefix and cache.fallback_keys entries, these placeholders are expanded at runtime:
| Placeholder | Description |
|---|---|
${job_name} | Job name (graph node id). |
${job_id} | Job id (same as job name today). |
${run_id} | Executor run id. |
${pipeline_id} | Executor pipeline id. |
${head_sha} | Snapshot HEAD commit SHA. |
$name is accepted as well as ${name}. Beyond known authored references and
these runtime placeholders, unknown names, a bare $ or $$ in a key,
prefix or fallback field fail
loom check with a WF_PLANNER error naming the pointer; the runtime rejects
the same inputs instead of expanding them to an empty string.
cache:
key: "pnpm-${job_name}-${head_sha}"
paths:
- .pnpm-store
Runtime behavior
During local execution, cache operations wrap the selected Host or Docker
provider. Restore runs before commands; save follows execution when policy and
when permit it.
Where cache events appear
Cache messages share .loom/.runtime/runs/<run_id>/events.jsonl with command
output and other diagnostics. They carry job_id and describe restore/save
activity, keys and results in message.
loom logs <run_id> --json
MCP clients can use loom_read_events with an exact job_id filter. Follow the
Diagnostics ladder to compare messages
with the job and command states.
Cache store
Cached archives are stored locally under .loom/.runtime/cache/v1/. A scope hash
(derived from the repository remote URL or workspace path) and a key hash select
the candidate directory. Each candidate has an archive and an audit record with
the producing source revision and archive digest; an atomic active pointer
selects the completed save. Restore verifies that digest under a per-key lock.
Interrupted saves and malformed state cannot restore. A later completed save can repair them; it cannot clear key quarantine or republish a rejected candidate. See Cache storage format.
Key design checklist
Use this checklist to avoid the most common "cache is useless" failures:
- Pick stable
paths: cache directories that are expensive to recompute and safe to reuse (package manager stores, build caches). Keep them workspace-relative (e.g.,.pnpm-store,.nx/cache). - Choose
key.filesthat fully describe the cache contents: include lockfiles and any configuration that changes the generated output (e.g.,pnpm-lock.yaml,package.json, tool config). - Avoid key collisions: scope your key with
${job_name}or a distinctprefixso unrelated jobs don't overwrite each other. Use multi-cache (name) for different concerns. - Decide when to include
${head_sha}:- Include it when correctness matters more than reuse (per-commit caches, fewer stale mismatches).
- Omit it when reuse matters more than strictness (branch/shared caches), but be prepared to invalidate keys when behavior changes.
- Multi-cache: use one cache entry per concern (e.g.,
pnpmvsgo) so you can invalidate and iterate independently.
Operational constraints
These are the constraints that show up as "cache didn't restore" or "cache saved but didn't help":
| Constraint | Impact |
|---|---|
| Permissions | The runtime must be able to read/write each cache.paths directory. For Docker jobs, the container user must have write permission inside the mounted workspace. |
| Disk usage | Large caches grow quickly and affect performance and local disk pressure. Cache fewer, high-value directories rather than the whole workspace. |
| Key staleness | If cache keys don't change when inputs change, stale state is restored. Use key.files and ${head_sha} to keep invalidation aligned to real change. |
| Host vs Docker paths | Host jobs: paths are directories in your checkout. Docker jobs: paths are inside the container's workspace mount. Mismatches are the most common source of "cache did nothing". |
| Path safety | Cache paths must be relative to the workspace. Absolute paths and paths containing .. are rejected. |
Compare cache-on and cache-off runs
Use loom run --local --cache-diff with a native Loom workflow to compare the declared outputs of two executions. After collecting complete evidence for both executions, Loom quarantines a participating cache-hit candidate only when the node's declared outputs differ and exactly one eligible scope/key pair is associated with that node. It records ambiguous candidates without quarantine. This comparison does not establish universal cache safety and is not required for ordinary cache reuse. GitLab workflows do not support cache-diff. See Cache comparison and quarantine for the comparison contract and its limits.