Cache
Loom caches declared paths and restores them before running a job's script. Package managers and build tools can reuse that data to avoid downloads or computation. Loom still runs the script on a cache hit; the time saved depends on the tool and cache configuration.
Use --cache-diff to compare successful cache-off and cache-on executions and their declared outputs. See Quarantine for the limits of that comparison.
How it works
When a job with cache: configuration runs:
- Key resolution — Loom computes a cache key from the
key:configuration (prefix + file hashes, or a template string). - Quarantine check — if the resolved key is quarantined, Loom skips the restore and runs the job fresh.
- Restore attempt — Loom looks up the key in the cache store. On a hit, cached paths are extracted into the workspace. On a miss, the job runs without cached outputs.
- Job execution — the job's
script:commands run. - Save — after execution, Loom archives the declared
paths:and writes them to the cache store under the resolved key.
The policy: and when: fields control which of these steps happen (see Configuration reference below).
Configuration reference
Cache is configured per-job in your workflow YAML. It supports two forms: a mapping (single cache) and a sequence (multiple named caches).
Cache fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Only in sequence form | Unique identifier for this cache entry |
disabled | boolean | No | Set true to skip this cache entry |
paths | string[] | Yes (unless disabled) | Workspace-relative paths to cache (files or directories) |
key | string or mapping | No | How to compute the cache key (see Key resolution) |
fallback_keys | string[] | No | Additional keys to try on restore if the primary key misses |
policy | string | No | One of pull, push, pull-push (default: pull-push) |
when | string | No | When to save: on_success, on_failure, always (default: on_success) |
Cache output paths
Each cache.paths entry must be relative to the workspace. It can be a literal file or directory, such as dist/app.js or .pnpm-store, or a trailing-child pattern with a literal root, such as dist/*.
A trailing-child pattern selects the root's immediate children. If a selected child is a directory, Loom archives that directory recursively. A missing or empty root contributes no entries; it never expands to the whole workspace. Matches are processed in lexical order, and overlapping selectors such as dist and dist/* emit each archive entry once.
Other wildcard forms are not supported. Loom rejects rootless patterns, **, partial-name wildcards, ?, character classes, braces, absolute paths, traversal, trailing whitespace in a pattern root, and malformed selectors at the offending cache.paths entry before execution.
Host and transport-backed jobs use the same contract. For dist/*, the transport exports or imports the literal dist root; Loom does not substitute the workspace root.
Policy values
| Policy | Restore on hit? | Save after job? | Use case |
|---|---|---|---|
pull-push | Yes | Yes | Default — full caching behavior |
pull | Yes | No | Read-only — consume caches without updating them |
push | No | Yes | Write-only — populate caches for other jobs/runs |
When values
| When | Saves on success? | Saves on failure? | Use case |
|---|---|---|---|
on_success | Yes | No | Default — only cache known-good outputs |
on_failure | No | Yes | Cache partial outputs for faster retry after failures |
always | Yes | Yes | Always save regardless of outcome |
Key resolution
Cache keys determine whether a restore is a hit or miss. Loom supports two key formats:
Prefix + files (recommended)
The most common form. Loom hashes the contents of the specified files and appends the digest to the prefix:
cache:
key:
prefix: my-project-deps
files: [pnpm-lock.yaml]
paths: [.pnpm-store, node_modules]
The resolved key becomes my-project-deps-<sha256>, where the SHA-256 digest is computed from the sorted file contents. If a listed file doesn't exist, Loom contributes a MISSING:<path> sentinel to the hash — so the key still changes when files appear or disappear.
Glob patterns are supported in files: (e.g., **/go.sum).
Template string
For full control, use a template string with runtime variables:
cache:
key: "deps-${job_name}-${head_sha}"
paths: [.pnpm-store]
Available template variables:
| Variable | Description |
|---|---|
$job_name | Job name (graph node ID) |
$job_id | Job identifier |
$run_id | Current run ID |
$pipeline_id | Pipeline ID |
$head_sha | HEAD commit SHA of the snapshot |
Fallback keys
When the primary key misses, Loom tries each fallback_keys entry in order. This is useful for partial cache hits — for example, restoring from a previous lockfile version:
cache:
key:
prefix: deps
files: [pnpm-lock.yaml]
fallback_keys:
- "deps-${job_name}"
paths: [.pnpm-store]
Cache storage format
Cached outputs are stored as zstd-compressed tar archives (.tar.zst). Each cache entry produces two files in the cache store:
<cache_root>/
<scope_hash>/
<key_hash>.tar.zst # archived paths
<key_hash>.json # manifest (key, scope, paths, timestamp)
The scope_hash isolates caches by scope (preventing cross-project collisions), and key_hash is the SHA-256 of the resolved key string.
Concrete example
Single cache (mapping form)
version: v1
stages: [ci]
unit:
stage: ci
target: linux
cache:
key:
prefix: loom-cache-unit
files: [pnpm-lock.yaml]
paths: [.pnpm-store, .nx/cache]
policy: pull-push
when: always
script:
- pnpm i --frozen-lockfile
- pnpm test
Multiple caches (sequence form)
When a job needs separate caches with independent lifecycles, use the sequence form. Each entry requires a unique name:
version: v1
stages: [ci]
build:
stage: ci
target: linux
cache:
- name: deps
key:
prefix: deps
files: [pnpm-lock.yaml]
paths: [.pnpm-store, node_modules]
policy: pull-push
- name: build-cache
key:
prefix: build
files: [pnpm-lock.yaml, tsconfig.base.json]
paths: [.nx/cache, dist]
policy: pull-push
when: on_success
script:
- pnpm i --frozen-lockfile
- pnpm build
Disabling cache
Set cache: null or cache: [] to explicitly disable caching for a job. In sequence form, set disabled: true on individual entries:
cache:
- name: deps
disabled: true
paths: [.pnpm-store]
Trust-but-verify with --cache-diff
Run with --cache-diff to compare a cache-off baseline with a cache-on variant. Both executions use independent workspaces derived from the same pinned input snapshot:
loom run --local --cache-diff --workflow .loom/workflow.yml
The versioned declared-output/v1 contract compares node presence, status, exit code, and content manifests for declared cache paths and declared artifacts. A successful comparison means those mechanical observations are equal between the two executions.
The contract does not observe or prove undeclared workspace side effects, stdout/stderr, timing, network behavior, or universal determinism.
Use --cache-diff when:
- Introducing caching for the first time
- Changing cache keys or paths
- Investigating "works locally but fails in CI" issues related to stale artifacts
Quarantine
Loom decides whether to quarantine participating cache-hit candidate keys after collecting complete evidence for both executions. For a divergent node, Loom quarantines an unambiguous candidate key and skips it on later restore attempts. It records ambiguous candidates without quarantine. This does not imply a universal cache-safety guarantee.
Where quarantine state lives
Quarantine entries are stored in a single JSON file at the cache store root:
<cache_root>/quarantine.json
The file contains an array of quarantine entries:
{
"schema_version": "v1",
"entries": [
{
"scope_hash": "abc123...",
"key_hash": "def456...",
"key": "deps-loom-cache-unit-<sha256>",
"reason": "cache_diff_divergence",
"quarantined_at": "2026-03-04T12:00:00.000Z",
"evidence": {}
}
]
}
How to recover from quarantine
- Confirm the divergence is real — check whether the outputs are genuinely nondeterministic (timestamps, randomness) or whether inputs changed without the key reflecting it.
- Fix the key — update
key.filesto include the inputs that actually changed, so the cache key changes when outputs should change. - Remove the quarantine entry — delete the relevant entry from
quarantine.json, or delete the file entirely to clear all quarantines.
Cache key design checklist
Use this checklist whenever you add or review cache configuration:
- Include inputs that change outputs: lockfiles, build configs, compiler flags, tool versions
- Scope keys to avoid collisions: use meaningful prefixes that include project/job context
- Cache only the right paths: include produced artifacts, exclude transient/temp files
- Avoid caching directories with absolute paths: paths that embed machine-specific state cause false hits
- Test with
--cache-diff: compare the declared-output contract before trusting participating cache hits
When not to cache
Avoid caching jobs or steps that are inherently nondeterministic or where reuse creates risk:
| Situation | Why caching is risky |
|---|---|
| Nondeterministic outputs | Timestamps, randomness, or "latest" tags produce different outputs from the same inputs |
| Secret-bearing outputs | Artifacts that could contain credentials, even transiently |
| External-state dependent steps | Queries against mutable services where outputs change without input changes |
| Debug or diagnostic steps | Where you need fresh logs/output every time |
If you're unsure, start with --cache-diff and review its declared-output evidence before enabling routine reuse.
Common pitfalls
| Pitfall | Fix |
|---|---|
| Treating cache hits as a universal correctness signal | Review --cache-diff declared-output evidence |
| Cache key doesn't include all relevant inputs | Add missing files/configs to key.files |
| Caching paths that embed absolute paths | Use relative, machine-independent paths |
Overly broad paths: that include transient files | Narrow paths: to only the outputs you need |
| Not scoping keys by project or job | Add meaningful prefixes to avoid cross-job collisions |
What to read next
- Syntax v1 — cache configuration
- Providers — how execution providers interact with cache storage
- Isolated workspace — the snapshot context where cached paths are restored
- Receipts — cache evidence is included in run receipts
- CLI
loom run—--cache-diffflag reference