Skip to main content

Cache

Caching lets Loom jobs skip redundant work by saving and restoring outputs keyed on your inputs. When the inputs haven't changed, the cached outputs are restored instead of recomputing them — turning multi-minute dependency installs or build steps into seconds.

This page explains how caching works, how keys are resolved, what quarantine means, and how to design keys that are safe and effective.

Why caching matters

Every CI/CD run that re-downloads dependencies, re-compiles artifacts, or re-generates unchanged outputs wastes time and compute. Caching addresses this directly:

  • Faster feedback loops — cache hits restore outputs in seconds instead of minutes.
  • Lower resource usage — avoid redundant network fetches, disk I/O, and CPU cycles.
  • Deterministic verification--cache-diff mode lets you trust caches while still proving correctness.

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)

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 verify that cached outputs match what the job would produce from scratch:

loom run --local --cache-diff --workflow .loom/workflow.yml

In this mode, Loom restores cached outputs and runs the job, then compares the results. If they diverge, the cache entry is quarantined.

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

When --cache-diff detects that cached outputs diverge from freshly produced outputs, Loom quarantines the cache entry. Quarantined entries are skipped on future restore attempts until the underlying issue is resolved.

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: verify determinism before trusting 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 until you've proven determinism.

Common pitfalls

PitfallFix
Treating cache hits as proof of correctnessUse --cache-diff to verify
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