Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Compare the Nx approach

Configuring Nx Affected Commands in CI

Your CI runs nx run-many --target=build on every pull request, rebuilds all 40 packages, and burns 25 minutes even when the change touched a single README. The fix is nx affected: it inspects what actually changed against a base commit, walks the project graph to find every project that depends on the change, and runs the target only for that subset. This page shows the exact base/head SHA selection, the affected algorithm, distributed execution, and a complete GitHub Actions workflow.

Symptoms

You are running the full graph on every commit when you see signs like these:

Symptoms You are running the full graph on every commit when you see signs like these: Symptoms You are running the full graph on every commit when you see signs like these:
Symptoms — the core idea of this section at a glance.
> nx run-many --target=build --all
   ✔  nx run pkg-a:build
   ✔  nx run pkg-b:build
   ... (38 more) ...
   Successfully ran target build for 40 projects (24m 51s)
Error: No base and head SHAs could be calculated.
Assuming all projects are affected.

The second message is the most expensive trap: when Nx cannot resolve a base SHA, it conservatively marks every project as affected, silently undoing all your savings while the command still exits 0.

Root cause

nx affected is only as good as the two commits it diffs. It computes the set of changed files between a base and a head SHA, maps each file to its owning project, then expands that seed set across the project graph so every dependent is included. On a feature branch the natural base is the merge-base with main; on a push to main the natural base is the previous successful commit. CI runners default to a shallow fetch-depth: 1 clone, so the base commit is not present in .git, the diff cannot be computed, and Nx falls back to "all affected". Getting affected right in CI is a core part of Nx Workspace Architecture, because the project graph that powers nx graph is the same graph that powers affected targeting.

From changed files to a minimal task set Changed files seed the project graph, which Nx expands to dependents, producing the minimal set of tasks to run. changed files base..head diff project graph seed + dependents (transitive closure) minimal tasks build / test / lint Unchanged projects and their isolated subtrees are skipped entirely.
Affected targeting: only projects reachable from a changed file run their tasks.

How the affected algorithm works

Nx builds a project graph by statically analyzing imports, package.json dependencies, and explicit implicitDependencies. When you run nx affected --target=build, Nx:

How the affected algorithm works Nx builds a project graph by statically analyzing imports, package.json dependencies, and explicit implicitDependencies. How the affected algorithm works Nx builds a project graph by statically analyzing imports, package.json dependencies, and explicit implicitDependencies.
How the affected algorithm works — the core idea of this section at a glance.
  1. Diffs the working tree between --base and --head to get a list of changed files.
  2. Maps each file to its owning project (or to a global file like nx.json or the lockfile, which marks all projects affected).
  3. Computes the transitive closure of dependents: if ui changed and web imports ui, both ui and web are affected.
  4. Filters the affected set to projects that actually have the requested target configured, then schedules them as a task graph.

A change to a root-level file listed in namedInputs (such as a lockfile or tsconfig.base.json) intentionally invalidates the whole graph, because it can alter any project's resolution.

Setup

1. Confirm the project graph is correct

Setup Before trusting affected, verify the graph itself. Setup Before trusting affected, verify the graph itself.
Setup — the core idea of this section at a glance.

Before trusting affected, verify the graph itself. A missing edge means a dependent gets skipped and ships broken.

npx nx graph --file=graph.json
npx nx show projects --affected --base=origin/main --head=HEAD

2. Select the base and head SHAs

The base/head pair differs by event. For pull requests, diff against the merge-base with the target branch. For pushes to main, diff against the last successful commit. The nrwl/nx-set-shas action resolves both automatically by querying the last successful CI run on the branch and exporting NX_BASE and NX_HEAD:

- uses: nrwl/nx-set-shas@v4
  with:
    main-branch-name: 'main'

Without this action you can set them manually, but you must fetch enough history first:

git fetch origin main --depth=50
export NX_BASE=$(git merge-base origin/main HEAD)
export NX_HEAD=$HEAD
npx nx affected --target=build --base=$NX_BASE --head=$NX_HEAD

3. Configure targetDefaults and inputs in nx.json

nx.json targetDefaults declare each target's cache behavior and inputs. Correct inputs keep the cache hash stable so unchanged projects stay off the critical path:

{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": {
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  }
}

dependsOn: ["^build"] forces each project's upstream dependencies to build first, which is what makes parallel affected builds correct rather than racy.

4. Run targets with parallelism and DTE

Run multiple targets in one invocation and cap concurrency with --parallel. For large graphs, distributed task execution (DTE) shards the task graph across multiple agents via Nx Cloud:

npx nx affected -t lint test build --parallel=3
# Coordinator on the main runner, agents started separately
npx nx affected -t build --parallel=3 --distribute-on="3 linux-medium-js"

Full GitHub Actions workflow

Full GitHub Actions workflow fetch-depth: 0 is the single most important line: a shallow clone is the usual reason affected silently degrades to "all Full GitHub Actions workflow fetch-depth: 0 is the single most important line: a shallow clone is the usual reason affected silently degrades to "all projects".
Full GitHub Actions workflow — the core idea of this section at a glance.
name: ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          # Full history so merge-base and the last successful SHA resolve
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      # Resolves NX_BASE / NX_HEAD from the last successful run
      - uses: nrwl/nx-set-shas@v4
        with:
          main-branch-name: 'main'

      - run: pnpm install --frozen-lockfile

      # One command, three targets, scoped to affected projects only
      - run: npx nx affected -t lint test build --parallel=3
        env:
          NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}

fetch-depth: 0 is the single most important line: a shallow clone is the usual reason affected silently degrades to "all projects". Pair the install step with --frozen-lockfile exactly as in Lockfile Management Strategies so the dependency tree is deterministic across runners.

Validation

Confirm affected is actually narrowing the set before you trust it in merge gates:

Validation Confirm affected is actually narrowing the set before you trust it in merge gates: Validation Confirm affected is actually narrowing the set before you trust it in merge gates:
Validation — the core idea of this section at a glance.
# List the projects affected by the current branch
npx nx show projects --affected --base=origin/main --head=HEAD

# Dry-run the task graph without executing anything
npx nx affected -t build --base=origin/main --head=HEAD --graph=stdout

If the project list contains every project for a one-line change, your base SHA is wrong or a global file (lockfile, nx.json, a sharedGlobals input) changed.

CI guardrails

  • Set fetch-depth: 0 (or fetch at least to the merge-base) so the base SHA is always present.
  • Use nrwl/nx-set-shas rather than hand-rolled SHA math; it handles the "first run on a branch" edge case.
  • Pin NX_CLOUD_ACCESS_TOKEN as a read-write secret only on trusted branches; use a read-only token for fork PRs to prevent cache poisoning.
  • Add a scheduled nightly nx run-many --all build so a stale cache or a missing graph edge cannot hide a broken project indefinitely.
  • Treat any "Assuming all projects are affected" log line as a CI warning, not noise.
CI guardrails CI guardrails in production JavaScript package workflows. CI guardrails CI guardrails in production JavaScript package workflows.
CI guardrails — the core idea of this section at a glance.

Giving Nx affected the base it needs

Nx's affected commands compute what changed by diffing your working tree against a base ref, so the single most important CI configuration is making that base reachable. A shallow clone — the default on many CI platforms — lacks the base commit, so Nx cannot compute a diff and conservatively treats everything as affected, erasing the speed-up. Fetching enough history and passing an explicit --base and --head is the precondition for affected to work at all.

Reachable base Full history and an explicit base/head. fetch-depth: 0 full history --base / --head explicit refs real diff narrow affected
A reachable base is the precondition for nx affected to compute a real diff.
- uses: actions/checkout@v4
  with:
    fetch-depth: 0
- run: npx nx affected --target=build --base=origin/main --head=HEAD

For a pull request, diff against the merge base with the target branch so the affected set is exactly the PR's changes plus their dependents; for a push to main, diff against the previous commit. Nx's nx-set-shas action can compute the right base and head automatically for common CI setups, which avoids a subtly wrong base ref. Getting the base right is what makes the difference between an affected command that runs a small, correct set and one that falls back to the whole workspace — the same precondition that governs any change-based CI optimization, applied to Nx's graph-driven affected detection.

Distributing affected work across agents

Once affected has narrowed the set, a large workspace can still have more work than one runner should do serially, and Nx's distributed task execution spreads it across multiple agents. Nx Cloud coordinates the agents, farming out tasks in dependency order, replaying cached results between them, and reassembling the outputs, so a large affected set runs in parallel across machines rather than serializing on one.

Distributed execution Affected narrows, cache replays, agents parallelize. affected narrow the set cache replay computed distribute on agents parallelize rest
Distribution takes a well-tuned affected pipeline from fast-on-one to fast-at-scale.
- run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium"
- run: npx nx affected --target=build --base=origin/main
- run: npx nx affected --target=test --base=origin/main

This composes with affected and caching: affected removes work a change cannot reach, caching replays results already computed, and distribution parallelizes what remains across agents. The combination is what keeps Nx CI time bounded as both the workspace and the team grow — a change runs only its affected targets, replays the cached ones, and spreads the genuinely-new work across a fleet of runners. Configuring distribution is the step that takes a well-tuned affected pipeline from fast-on-one-machine to fast-at-scale, and because Nx's scheduler already knows the task graph and cache keys, distributing the work is a matter of declaring how many agents to use rather than manually partitioning the tasks.

Verifying the affected set is correct

Before trusting nx affected in CI, confirm it selects what you expect, because a silently-too-small set ships untested code and a too-large one wastes the speed-up. Nx can print the affected projects and render the graph without running anything, so you can compare the selection against your actual change.

Verify the selection List and graph the affected set, compare to the change. show projects --affected the selection affected:graph why included compare to change missing or extra
Reading the affected graph turns a wrong selection into a specific, fixable cause.
# List the affected projects for a change
npx nx show projects --affected --base=origin/main
# Visualize why each was included
npx nx affected:graph --base=origin/main

If a project that depends on your changed code is missing from the list, the dependency edge is undeclared — usually a deep import that bypasses the project's public entry — and Nx's inferred graph could not follow it. If unrelated projects appear, a shared input is too broad or the base ref is unreachable. Reading the affected graph turns a wrong selection into a specific, fixable cause: a missing edge to declare, an input to narrow, or history to fetch. Making this verification a habit on large changes keeps the graph honest over time, so the affected set stays both fast and complete as the workspace evolves — which is the property the whole affected-CI setup depends on.

Frequently Asked Questions

Why does nx affected run every project on a tiny change? Almost always because the base SHA is unavailable. Shallow clones (fetch-depth: 1) leave the merge-base out of .git, so Nx cannot diff and conservatively marks all projects affected. Set fetch-depth: 0 and use nrwl/nx-set-shas to resolve the base and head reliably.

What is the difference between nx affected and nx run-many? nx run-many runs a target for an explicit list of projects (or --all), with no change detection. nx affected first computes which projects changed relative to a base SHA and which projects depend on them, then runs the target only for that subset. Use run-many for full rebuilds and affected for PR validation.

How does --parallel relate to distributed task execution? --parallel=N runs up to N tasks concurrently on a single machine. Distributed task execution (DTE) spreads the task graph across multiple CI agents through Nx Cloud, so the wall-clock time scales with the number of agents rather than the cores on one runner. They compose: each DTE agent still honors its own --parallel limit.

Why does nx affected run everything in CI?

Almost always a shallow clone that lacks the base commit, so Nx cannot compute the diff and treats everything as affected. Set fetch-depth: 0 and pass an explicit --base/--head (or use nx-set-shas to compute them), so affected sees the real change set.

What base ref should nx affected diff against?

For a pull request, the merge base with the target branch, so the affected set is exactly the PR's changes plus dependents; for a push to main, the previous commit. Nx's nx-set-shas action computes the right base and head for common CI setups automatically.

How do I speed up a large affected set in Nx?

Use distributed task execution (Nx Cloud) to spread the affected tasks across multiple agents, which composes with affected and caching: affected narrows the set, caching replays computed results, and distribution parallelizes the rest across a fleet of runners.

How do I check that nx affected selected the right projects?

Run nx show projects --affected --base=<ref> to list them and nx affected:graph to see why each was included. Compare against your change: a missing dependent means an undeclared edge, and unrelated projects mean a too-broad input or an unreachable base.

Related

Nx Workspace Architecture