Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Speed up type-checking

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 inputs patterns, in the project and, via ^ inputs, in its dependencies.
  • Dependency outputs or inputs — for ^production style 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.

The ingredients of an Nx task hash A central task hash combines project file inputs, dependency inputs, external package versions, runtime commands, environment variables and target configuration. task hash @acme/api:build project files production named input dependency files ^production npm versions from the lockfile runtime node --version env vars API_URL, NODE_ENV target config command and options
If something can change a task's output, it must appear in one of these inputs.

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.
  • productiondefault minus files that cannot affect shipped output: tests, fixtures, stories, test configs, docs. Builds use this.
  • ^production — the production files 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 base tsconfig and 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).

Which inputs each target should use Maps build, test, lint and typecheck targets to their own-project and dependency inputs and typical extra inputs. own project dependencies extra inputs build production ^production env: NODE_ENV test default ^production test runner config lint default none root ESLint config typecheck production + tests ^production tsconfig.base.json
Builds hash production files only; tests and lint hash their own project fully but only production files of dependencies.

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.

Debugging an unexpected cache result For a miss, check for outputs or generated files inside inputs and undeclared env vars; for a hit, check whether the changed file is covered by any input. Does a second identical run miss? Non-deterministic input exclude dist/ and generated files yes Did a docs or test edit trigger a build? Too broad use production for build inputs yes no Did a config change not rebuild? Missing input add it to sharedGlobals or target inputs yes no Check env and runtime inputs declare env vars that change output no
Misses usually mean too much or non-deterministic input; stale hits mean a missing input.

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 production inputs for build targets and ^production for 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