Fixing Nx 'Affected' Detecting All Projects as Changed
nx affected reports every project as affected on a trivial change, so you lose the speed-up entirely. This page explains why Nx widens the affected set and how to narrow it back to the real change.
Exact symptoms and error messages
A one-line change to a single library marks the whole graph affected:
> nx affected:build --base=origin/main
Note: Nx affected calculated 37 projects to run...
(expected 2)
Root cause analysis
Nx marks everything affected when a change touches a global input — the root package.json, the lockfile, nx.json, or a shared tsconfig.base.json — because those legitimately influence every project. It also widens the set when the base ref is unreachable (a shallow clone) or when namedInputs are configured so that unrelated files feed a project's hash. The mechanics are the same graph model described in Nx Workspace Architecture.
Nx computes affected by hashing a set of named inputs per project and diffing against the base. Anything included in an input that many projects share — a root config, a workspace-wide glob — becomes a shared input, so touching it invalidates every project that lists it. That is correct when the file genuinely affects all projects, and wasteful when an input set is broader than it needs to be. Tuning namedInputs is really about telling Nx precisely which files change a project's output.
The base ref is the other half. Nx diffs your working tree against --base, and if that commit is missing from a shallow CI clone, it cannot compute a diff and conservatively marks everything affected. So an all-projects result on a trivial change is almost always one of two things: an input set that folds in unrelated files, or a base that does not resolve — and the affected graph tells you which.
Nx marks everything affected when a change touches a global input — the root package.json, the lockfile, nx.json, or a shared tsconfig.base.json — because those legitimately influence every project. It also widens the set when the base ref is unreachable (a shallow clone) or when namedInputs are configured so that unrelated files feed a project's hash. So an all-projects result on a trivial change is almost always one of two things: an input set that folds in unrelated files, or a base that does not resolve.
The mechanics are that Nx computes affected by hashing each project's named inputs and diffing against the base. Anything included in an input that many projects share becomes a shared input, so touching it invalidates every project that lists it. Tuning namedInputs is really about telling Nx precisely which files change a project's output, and keeping sharedGlobals minimal is about ensuring only genuinely global files invalidate the whole workspace.
Resolution and configuration patch
Scope namedInputs so formatting and docs changes do not invalidate builds, and ensure the base is reachable:
// nx.json
{
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/*.spec.ts", "!{projectRoot}/**/*.md"],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
}
}
Separate a lean production input from the default so that test-only and docs changes do not invalidate build and deploy targets:
// nx.json
{
"targetDefaults": {
"build": { "inputs": ["production", "^production"] }
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": ["default", "!{projectRoot}/**/*.spec.ts", "!{projectRoot}/**/*.md", "!{projectRoot}/**/*.stories.ts"],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
}
}
Now a change to a spec file re-runs tests but not builds, and sharedGlobals is kept deliberately small so only a genuinely global file invalidates the whole graph.
Scope the inputs so test-only and docs changes do not invalidate builds, and ensure the base is reachable:
// nx.json
{
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/*.spec.ts",
"!{projectRoot}/**/*.stories.ts",
"!{projectRoot}/**/*.md"
],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
},
"targetDefaults": { "build": { "inputs": ["production", "^production"] } }
}
# ensure the base commit is present
git fetch origin main --depth=100
With build targets pointing at the lean production input and sharedGlobals kept minimal, only production sources invalidate a build and only a genuinely global file invalidates everything.
CLI validation and debug commands
# Show exactly why a project is affected
nx affected:graph --base=origin/main
# Ensure the base commit is present
git fetch origin main --depth=100
# List the affected projects only
nx show projects --affected --base=origin/main
Prevention and CI guardrails
- Fetch enough history that
--baseresolves to a real commit. - Exclude spec, markdown, and config-only files from the
productioninput set. - Keep
sharedGlobalsminimal — every file listed there invalidates the whole graph. - Review
nx affected:graphwhen the affected set looks too large.
- Fetch enough history that
--baseresolves to a real commit. - Exclude spec, story, and markdown files from the
productioninput set. - Keep
sharedGlobalsminimal — every file there invalidates the whole workspace. - Review
nx affected:graphwhen the affected set looks larger than the change.
Diagnosing affected with the project graph
When the affected set is wrong, guessing is slow; Nx can show you exactly why a project was included. The affected graph renders the change and the edges that propagated it, so you can see whether a project was pulled in by a real dependency or by a shared input.
# Visualize what the change affected and why
nx affected:graph --base=origin/main
If a project is highlighted with no path back to your change, the culprit is a shared input, not a dependency edge — trim the input set. If it is highlighted through a chain of dependencies you did not expect, you have found real coupling worth knowing about. Either way, the graph turns 'Nx ran too much' from a mystery into a specific, fixable cause.
Keeping sharedGlobals honest
sharedGlobals is the most dangerous input to get wrong, because every file listed there invalidates the entire workspace. It is easy to add a root file 'just in case' and quietly convert every future change to that file into a full rebuild. Treat the list as a deliberate, reviewed set: the lockfile, the base tsconfig, and little else.
Audit it whenever affected starts running more than it should. A .env file, a formatting config, or a docs generator config sitting in sharedGlobals will force a full run on every change to it — usually not what you want. The discipline is to ask, for each entry, 'does changing this genuinely change every project's output?' If the honest answer is no, it does not belong in the global set, and moving it out restores affected's ability to keep changes small.
Diagnosing affected with the project graph
When the affected set is wrong, guessing is slow; Nx can show you exactly why a project was included. The affected graph renders the change and the edges that propagated it, so you can see whether a project was pulled in by a real dependency or by a shared input. Running nx affected:graph --base=origin/main and reading the result tells you which of the two causes you are looking at, which points directly at the fix.
If a project is highlighted with no path back to your change, the culprit is a shared input, not a dependency edge — trim the input set so that unrelated file no longer feeds the project's hash. If it is highlighted through a chain of dependencies you did not expect, you have found real coupling worth knowing about, and the fix may be to break the dependency rather than to change the inputs. Either way, the graph turns 'Nx ran too much' from a mystery into a specific, fixable cause, which is far faster than adjusting configuration by trial and error and re-running the whole pipeline to see if it helped.
Keeping sharedGlobals honest
sharedGlobals is the most dangerous input to get wrong, because every file listed there invalidates the entire workspace on any change to it. It is easy to add a root file 'just in case' and quietly convert every future change to that file into a full rebuild. Treat the list as a deliberate, reviewed set: the lockfile, the base tsconfig, and little else — files that genuinely change every project's output.
Audit the list whenever affected starts running more than it should. A .env file, a formatting config, or a documentation generator config sitting in sharedGlobals will force a full run on every change to it, which is usually not what you want. The discipline is to ask, for each entry, whether changing this genuinely changes every project's output; if the honest answer is no, it does not belong in the global set, and moving it out restores affected's ability to keep changes small. Keeping sharedGlobals minimal is the single highest-leverage tuning for an over-broad affected set, because a wrongly-included global file invalidates everything while a wrongly-included project input invalidates only that project.
Scoping named inputs precisely
The named-inputs configuration is where you tell Nx exactly which files change each project's output, and getting it precise is what keeps affected detection narrow. The default input covers a project's whole directory plus shared globals; a production input refines it by excluding files that do not affect the build — spec files, stories, markdown — so a test-only change re-runs tests but not builds. Pointing build and deploy targets at production rather than default means those expensive targets are invalidated only by production sources.
{
"targetDefaults": {
"build": { "inputs": ["production", "^production"] },
"test": { "inputs": ["default", "^production"] }
}
}
The ^production entry extends the input to a project's dependencies, so a project rebuilds when a dependency's production source changes but not when only its tests do. This precise scoping is what makes affected detection track real output-affecting changes rather than any file edit. The payoff compounds across every build: a change to a spec file runs one project's tests, a change to a shared library rebuilds its production dependents, and a docs edit runs nothing expensive — which is exactly the behavior an over-broad affected set was failing to deliver.
Frequently Asked Questions
Is it ever correct for Nx to mark everything affected?
Yes — a change to the lockfile, root tsconfig, or nx.json genuinely can change every project's output, so a full run is the safe result. The goal is to stop unrelated files from triggering it.
Why does the base branch matter so much?
Nx diffs your working tree against the base commit. If a shallow clone means that commit is absent, Nx cannot compute a diff and conservatively treats everything as changed.
How do I see why Nx marked a project affected?
Run nx affected:graph --base=<ref>. It renders the change and the edges that propagated it, so you can tell whether a project was pulled in by a real dependency or by a shared input — and fix the right thing.
What belongs in sharedGlobals?
Only files that genuinely change every project's output — typically the lockfile and the base tsconfig. Anything else there forces a full rebuild on every change to it, which is almost never intended.
Why do spec-file changes trigger builds?
Because the build target's inputs include the spec files. Define a lean production input that excludes *.spec.ts, *.md, and stories, and point build/deploy targets at it so only production sources invalidate them.
Why does Nx mark every project affected on a small change?
Usually the change touched a global input (the lockfile, nx.json, a base tsconfig) — which legitimately invalidates everything — or the base ref is unreachable in a shallow clone, or an over-broad namedInput folds unrelated files into projects' hashes. Check nx affected:graph to see which.
How do I see why Nx included a specific project?
Run nx affected:graph --base=<ref>. It renders the change and the edges that propagated it, so you can tell whether a project was pulled in by a real dependency or a shared input — and fix the right thing.
Why do spec-file changes trigger builds in Nx?
Because the build target's inputs include the spec files. Define a lean production input that excludes *.spec.ts, stories, and markdown, and point build targets at it so only production sources invalidate them.
How do I keep test changes from invalidating Nx builds?
Define a production named input that excludes *.spec.ts, stories, and markdown, and point build/deploy targets at production and ^production. Then only production sources invalidate builds, while tests still re-run on spec changes.
Is it ever correct for Nx to mark everything affected?
Yes — a change to the lockfile, the root tsconfig, or nx.json genuinely can change every project's output, so a full run is the safe result. The goal is to stop unrelated files from triggering it, not to suppress a legitimate global invalidation.
Related
- CI/CD Pipeline Optimization for Monorepos — the overview for fast monorepo pipelines.