Configuring Nx Named Inputs for Accurate Caching
Nx decides whether a task can be replayed from cache by hashing its inputs. Too many inputs, and editing a README invalidates every build, so the cache almost never hits. Too few, and a change to a shared configuration file is ignored, so the cache replays stale output — the more dangerous failure. Named inputs are how you describe inputs precisely and reuse those descriptions across tasks. This guide explains what goes into a task hash, how to define default, production and shared inputs, and how to debug a cache that hits or misses when it should not.
Symptoms of badly configured inputs
Two opposite symptoms point at the same configuration:
# Too many inputs: a docs-only change rebuilds everything
$ pnpm nx affected -t build
NX Running target build for 38 projects
✔ nx run @acme/ui:build (12s)
✔ nx run @acme/forms:build (9s)
...
NX Successfully ran target build for 38 projects (0 read from cache)
# Too few inputs: a changed tsconfig target is ignored
$ pnpm nx run @acme/api:build
✔ nx run @acme/api:build [local cache]
# ...but dist/ still contains ES2019 output after tsconfig.base.json moved to ES2022
The first costs time. The second ships wrong artefacts, and it is the reason to be deliberate rather than generous about inputs being "probably fine". Both are fixed in the same place — the inputs of each target — and both become easy to reason about once inputs are expressed through a small set of named, reusable definitions rather than ad hoc globs scattered across projects.
What goes into a task hash
For each task, Nx computes a hash from:
- File inputs — the files matched by the task's
inputspatterns, in the project and, via^inputs, in its dependencies. - Dependency outputs or inputs — for
^productionstyle inputs, the relevant files of every project this one depends on. - External dependencies — the resolved versions of npm packages the project uses, from the lockfile.
- Runtime inputs — the output of commands you declare, such as
node --version. - Environment inputs — the values of environment variables you declare.
- The task's own configuration — target options and the command itself.
Anything not in that list does not affect the hash. How this fits into Nx's graph model is covered in Nx Workspace Architecture.
Defining named inputs
Named inputs live in nx.json and can be referenced by name from any target's inputs:
{
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/*.{test,spec}.{ts,tsx}",
"!{projectRoot}/**/__fixtures__/**",
"!{projectRoot}/**/*.stories.{ts,tsx}",
"!{projectRoot}/{vitest,playwright}.config.ts",
"!{projectRoot}/README.md"
],
"sharedGlobals": [
"{workspaceRoot}/tsconfig.base.json",
"{workspaceRoot}/.nvmrc",
{ "runtime": "node --version" }
]
},
"targetDefaults": {
"build": {
"inputs": ["production", "^production", { "env": "NODE_ENV" }],
"outputs": ["{projectRoot}/dist"],
"dependsOn": ["^build"],
"cache": true
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/vitest.workspace.ts"],
"cache": true
},
"lint": {
"inputs": ["default", "{workspaceRoot}/eslint.config.js", "{workspaceRoot}/packages/eslint-config/**/*"],
"cache": true
}
}
}
The conventions that make this work:
default— everything in the project plus workspace-wide files that affect every task.production—defaultminus files that cannot affect shipped output: tests, fixtures, stories, test configs, docs. Builds use this.^production— theproductionfiles of every dependency. A build depends on its dependencies' source, not their tests.sharedGlobals— root files and runtime facts that affect everything, such as the basetsconfigand the Node.js version.
Tests use default for their own project (a test change must re-run tests) but ^production for dependencies (a dependency's test change should not re-run your tests).
Shared configuration packages
Monorepos often keep shared tooling configuration in workspace packages — @acme/eslint-config, @acme/tsconfig. If a project depends on them through devDependencies, ^production or ^default inputs pick up their files automatically, because they are dependencies in the graph. If they are referenced only by path (for example, "extends": "../../tsconfig.base.json"), they must be listed explicitly in sharedGlobals or target inputs. The second pattern is the most common source of stale cache hits: configuration changes that no input mentions.
Declaring shared configuration packages as real dependencies, as recommended in Sharing a Base tsconfig Across Workspaces, makes caching correct by construction.
External dependencies and the lockfile
Nx includes the versions of npm packages a project uses in its hash, derived from the lockfile. By default, a project's hash covers all external dependencies it imports or declares, so upgrading zod invalidates every project that uses Zod but not those that do not. That behaviour is usually right, but two cases need attention.
First, tools that are invoked by a target but not imported by the project — a bundler plugin, a code generator — may not be associated with the project automatically. Declare them with an externalDependencies input on the target so a version bump invalidates the cache:
{
"targetDefaults": {
"build": {
"inputs": ["production", "^production", { "externalDependencies": ["vite", "@vitejs/plugin-react"] }]
}
}
}
Second, when externalDependencies is specified explicitly for a target, it replaces the default "all external dependencies" behaviour for that target. Listing only the build tools there means runtime library upgrades no longer invalidate the build — which is what you want only if the build's output genuinely does not embed those libraries. For bundled applications it does, so keep the defaults for application builds and use explicit lists for targets such as lint, whose output depends only on the lint toolchain.
Environment variables and runtime inputs
Environment variables that change output must be declared, or two builds with different API_URL values share a cache entry:
{
"targetDefaults": {
"build": {
"inputs": ["production", "^production", { "env": "API_URL" }, { "env": "NODE_ENV" }]
}
}
}
Runtime inputs cover facts that are not files, such as the Node.js version or the platform: { "runtime": "node --version" } or { "runtime": "uname -m" } for builds that produce platform-specific output. Keep runtime commands fast — they run for every hash calculation.
Debugging cache behaviour
When a task misses or hits unexpectedly, compare what Nx hashed. Nx can print the inputs it used for a task:
# Show the resolved inputs and outputs for a target
pnpm nx show project @acme/api --json | jq '.targets.build.inputs, .targets.build.outputs'
# Run and print the task hash details
NX_VERBOSE_LOGGING=true pnpm nx run @acme/api:build --skip-nx-cache
# Compare hashes between two runs (hash details are stored per task)
ls .nx/workspace-data/ && pnpm nx run @acme/api:build --verbose
A practical technique for a surprising miss: run the task twice in a row locally. If the second run misses, something non-deterministic is in the inputs — a generated file inside the project that the build itself writes, a timestamp, or an input glob that matches dist/. Exclude generated files from inputs with a negated pattern.
For a surprising hit, change the file you suspect should matter and run again with NX_VERBOSE_LOGGING=true. If the hash does not change, that file is not in the inputs — add it.
Worked example: fixing a stale build after a tsconfig change
A team bumps target in tsconfig.base.json from ES2019 to ES2022 and deploys. Days later someone notices that several libraries still emit ES2019 syntax: their builds were replayed from cache because tsconfig.base.json was referenced by relative extends paths and was not in any input. Adding it to sharedGlobals changes every project's hash, and the next run rebuilds everything once. The team then converts the base configuration into a workspace package that every project depends on, so future changes invalidate exactly the right tasks, and adds a CI job that runs builds with --skip-nx-cache weekly and compares outputs with cached ones to catch any other missing input.
Prevention and CI/CD guardrails
- Use
productioninputs for build targets and^productionfor dependency inputs. - Declare every environment variable that affects output, per target.
- Keep generated files and outputs out of inputs.
- Periodically verify the cache by rebuilding without it and comparing results.
Frequently Asked Questions
Is it safe to use {projectRoot}//* for everything?** It is safe in the sense of never serving stale results for project files, but it makes the cache miss on every unrelated edit and still misses root-level configuration. Precise named inputs are both faster and safer.
Do inputs affect which projects are affected? Affected detection uses changed files and the project graph; inputs determine whether a task in an affected project can still be replayed from cache. Both matter for CI time.
How do Turborepo's inputs compare?
Turborepo has the same concept with inputs, env and globalDependencies in turbo.json, covered in Turborepo Pipeline Configuration.
Should outputs ever be part of inputs?
No. A task's outputs must not match its own input globs, or every run changes its own hash and the cache never hits. Exclude dist/, coverage folders and generated code explicitly if your default glob would otherwise include them.
Related
- Nx Workspace Architecture explains the project and task graph.
- Adding Nx to an Existing pnpm Workspace creates the
nx.jsonthese inputs live in. - Configuring Nx Remote Caching shares the cache that accurate inputs make trustworthy.
- Debugging Why a Turborepo Task Is Never Cached covers the same problem in Turborepo.