Skip to main content

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:

  1. Key resolution — Loom computes a cache key from the key: configuration (prefix + file hashes, or a template string).
  2. Quarantine check — if the resolved key is quarantined, Loom skips the restore and runs the job fresh.
  3. 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.
  4. Job execution — the job's script: commands run.
  5. 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

FieldTypeRequiredDescription
namestringOnly in sequence formUnique identifier for this cache entry
disabledbooleanNoSet true to skip this cache entry
pathsstring[]Yes (unless disabled)Workspace-relative paths to cache (files or directories)
keystring or mappingNoHow to compute the cache key (see Key resolution)
fallback_keysstring[]NoAdditional keys to try on restore if the primary key misses
policystringNoOne of pull, push, pull-push (default: pull-push)
whenstringNoWhen 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

PolicyRestore on hit?Save after job?Use case
pull-pushYesYesDefault — full caching behavior
pullYesNoRead-only — consume caches without updating them
pushNoYesWrite-only — populate caches for other jobs/runs

When values

WhenSaves on success?Saves on failure?Use case
on_successYesNoDefault — only cache known-good outputs
on_failureNoYesCache partial outputs for faster retry after failures
alwaysYesYesAlways save regardless of outcome

Key resolution

Cache keys determine whether a restore is a hit or miss. Loom supports two key formats:

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:

VariableDescription
$job_nameJob name (graph node ID)
$job_idJob identifier
$run_idCurrent run ID
$pipeline_idPipeline ID
$head_shaHEAD 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

  1. Confirm the divergence is real — check whether the outputs are genuinely nondeterministic (timestamps, randomness) or whether inputs changed without the key reflecting it.
  2. Fix the key — update key.files to include the inputs that actually changed, so the cache key changes when outputs should change.
  3. 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:

SituationWhy caching is risky
Nondeterministic outputsTimestamps, randomness, or "latest" tags produce different outputs from the same inputs
Secret-bearing outputsArtifacts that could contain credentials, even transiently
External-state dependent stepsQueries against mutable services where outputs change without input changes
Debug or diagnostic stepsWhere 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

PitfallFix
Treating cache hits as a universal correctness signalReview --cache-diff declared-output evidence
Cache key doesn't include all relevant inputsAdd missing files/configs to key.files
Caching paths that embed absolute pathsUse relative, machine-independent paths
Overly broad paths: that include transient filesNarrow paths: to only the outputs you need
Not scoping keys by project or jobAdd meaningful prefixes to avoid cross-job collisions