Caching
Declare what a task reads and writes, and outdo skips it when nothing changed:
build: {
desc: "Build the project",
inputs: ["src/**/*.ts", "tsconfig.json"],
outputs: ["dist/**"],
run: async ({ $ }) => {
await $`bun build src/index.ts --outdir dist`;
},
},What's in the fingerprint
A task is skipped only when all of these are unchanged:
- Input file contents — real content hashes with an mtime+size fast path:
touchwon't bust the cache, actual edits will - The task definition itself — including the text of the
runbody; edit the code, invalidate the cache - Validated env output — change
DEPLOY_URLand the task re-runs (only for tasks with anenvschema) - CLI args and
--passthrough - Dependency fingerprints — upstream changes cascade downstream automatically
- Dependency data outputs — a dep whose
run()returned a different value invalidates its consumers, even when the dep itself isn't cacheable
Plus: every declared outputs glob must still match at least one file. Delete dist/ and the task re-runs even though inputs are unchanged.
Each component is hashed separately and stored in the manifest, which is what lets outdo explain tell you exactly which one changed.
What it is — and isn't
outdo's cache is skip-if-unchanged, not artifact restore. On a hit, the task simply doesn't run (✓ build (cached) in ~0ms). outdo does not store copies of your outputs and cannot re-materialize a deleted dist/ from a cache archive — that's Turborepo/Nx territory (and why those tools exist). See Why outdo? for the honest comparison.
Fingerprints live in .outdo/cache/ next to your do.ts.
WARNING
Add .outdo/ to .gitignore — outdo init does this for you. Fingerprints are per-machine state, not shared artifacts.
On a hit, one thing is restored: the task's data output is replayed from the manifest, so dependents keep working across cache skips.
Why will this re-run?
Stop guessing — ask:
outdo explain buildbuild — Build the project
backend: sequential (in-process)
plan (dependencies first):
✓ codegen would skip (cached)
↻ build would run — inputs changed (modified: src/app.ts)
details for build:
deps: codegen
inputs: src/**/*.ts (14 files), tsconfig.json (1 file)
outputs: dist/** (present)
env: no schema
cache: miss — inputs changed (modified: src/app.ts)explain walks the plan read-only (nothing runs, nothing is written) and diagnoses each task's cache verdict by fingerprint component: no previous run, inputs changed (with the exact files), env changed, args changed, task definition changed, dependency outputs changed, declared outputs missing, or --force. Add --json for the machine-readable version.
Controlling it
outdo --force build # ignore stored fingerprints (still records new ones)
outdo --dry build # shows which planned tasks would be cache-skipped
outdo explain build # explains *why* each task will run or skipTasks without inputs never cache — they run every time. Services (persistent: true) never cache either.
Known blind spot: closures
The fingerprint hashes the run function's source text. A value captured from outside the function body (a top-level constant, an import) can change without the text changing. Keep task-relevant values inside the body, model them as env/args, or use --force when in doubt.