A workspace with six projects and three tasks each has 13 tasks (for us). Running all of them on every change would soon be too slow in CI. Nx solves this with two means: the cache (what has already been computed is not computed again) and affected (what the change does not touch is not even approached). The lesson shows both on the workspace from 0101 and 0102 and sketches how a pipeline uses them.
State of the checks. Run (Node 24.22.3, Nx 24.0.6, Angular 22.2, Vitest 4.1.12, throwaway workspace outside the repository):
nx run-many -t lint test buildwithout and with cache,nx affected -t lint test build --base=HEAD~1 --head=HEADwith--explainand without; the outputs stand there as they were printed (paths and duration shortened). Read: the Nx pages "Cache task results", "Run only tasks affected by a PR", "Remote cache" and the example for Azure Pipelines (only as a summary of the page text; we did not see the YAML wording). Not run: any pipeline (GitHub Actions, Azure DevOps), Nx Cloud, a self-hosted cache,nx migrate. Open: prices and terms of Nx Cloud (the page names none), the operation of a self-hosted cache (only the page "Self-hosted remote cache" is named, not read).
The task pipeline
In Nx, a task (build, test, lint) has three properties that matter for cache and affected:
inputs: the files and values the result depends on. They form the hash. Named inputs (namedInputsinnx.json) group them:defaultmeans everything in the project,productionleaves out test files and the lint configuration.outputs: what the task produces and the cache restores (forbuild, the output folder).dependsOn: tasks that have to run beforehand."^build"means: firstbuildof the projects this one depends on.
How Nx sees them for our project kontakte-ui is shown by nx show project kontakte-ui --json. For us, lint and test were cacheable (cache: true); the input of test contains default, ^production (the production files of the dependencies), the file tsconfig.spec.json, the external dependency vitest and the value of the environment variable CI.
The last one is instructive: a test that you run locally has a different hash than the same test with CI set, and the results are not shared.
According to the page on the cache, the hash includes a project's source files and dependencies, relevant workspace configuration, versions of external packages, runtime values such as the operating system and CPU architecture, and the command's arguments. Nx does not store the source itself, only its hashes; an entry contains the terminal output, the files from outputs and the hash.
A cache presupposes a task without side effects: according to the page, an E2E test against a running server does not belong in it.
Proving the cache
Let everything run once and then a second time, without changing anything:
npx nx run-many -t lint test build
npx nx run-many -t lint test build
For us, the first run ended with exit 0 and the line "Cache: 0/13 hit (0%)" (after a change to tsconfig.base.json; a run with a partly warm cache had shown "4/13 hit (31%)" before). The second ended with exit 0 in 61 milliseconds:
√ nx run kontakte-ui:test [local cache]
√ nx run kladde-buero:build:production [local cache]
√ nx run kontakte-ui:lint [existing outputs match the cache, left as is]
NX Successfully ran targets lint, test, build for 6 projects
Nx read the output from the cache instead of running the command for 13 out of 13 tasks.
Run duration: 61ms
Cache: 13/13 hit (100%)
Two marks prove the hit: the suffix [local cache] or [existing outputs match the cache, left as is] on the task and the summary "13/13 hit". The Nx page describes no fixed mark; we read it from the output of this version. A cache hit is no guarantee that the test is green, but that it was green with the same inputs.
That is the point, but also the limit: what your test reads outside the inputs (time of day, network, a file outside the project) does not change the hash.
When the cache is missing. We changed strict in tsconfig.base.json: afterward 0 of 13 hits, because the root configuration is an input of all tasks. We changed a comment in shared-util-datum: only its lint and test ran again (section below). nx reset clears the cache and the daemon's data; the location of the local cache is not named by the page we read, and we did not determine it.
Affected
nx affected -t <tasks> runs only what a change can touch. According to the Nx page: Nx asks Git for the changed files, selects every task whose inputs contain a changed file, selects tasks that read the outputs of a selected task, and runs them together with their dependencies (unchanged dependencies come from the cache).
The selection depends on inputs, not on the task graph: a dependsOn alone does not make a task affected. That is why correct inputs and outputs are necessary even for tasks that are not cached.
Task-based from Nx 24. Up to Nx 23, a change to a project triggered all tasks of that project and of all its dependents (project-based). Nx 24 selects individual tasks. According to the page, a workspace that is raised to 24 with nx migrate keeps the old behavior through "affected": { "legacy": true } in nx.json; whoever removes the setting or sets it to false gets the new one. Our workspace was created with Nx 24 and had no such setting.
The probe. We committed a small change in libs/kontakte/ui (the card's name got a class) and asked with --explain what would be affected; --explain runs nothing:
npx nx affected -t lint test build --base=HEAD~1 --head=HEAD --explain
NX 7 out of 13 lint, test and build tasks are affected
Changing 1 file touches 3 lint tasks, 3 test tasks and 1 build task. Pass --verbose to list each with its reasons.
libs/kontakte/ui/src/lib/kontakt-karte.ts:
- kladde-buero:build:production
- kladde-buero:lint
- kladde-buero:test
- kontakte-feature-liste:lint
- kontakte-feature-liste:test
- and 1 more lint task and 1 more test task
Without --explain, exactly these seven tasks ran (exit 0, "Cache: 0/7 hit", 18.6 seconds): kontakte-ui itself, the feature library and the application. The three other libraries did not run; termine-data-access, kontakte-data-access and shared-util-datum did not import kontakte-ui. That is the gain: the graph from lesson 0102 decides who runs again.
Two more commits: a comment in shared-util-datum affected "2 out of 13" with --explain (its lint and test; no application imports it so far). A line in the README.md in the root folder resulted in "Nothing affected". That is due to the inputs: the README is in no inputs list.
The counterpart: according to the page, a change to the lock file affects all tasks by default; with projectsAffectedByDependencyUpdates set to "auto", only those whose resolved dependencies change (not run).
The run in CI
In CI there is no "current state of the disk against the last commit", but a comparison of two commits. The page says: the default for --base is the main branch, for --head the state of the file system; in CI you give both explicitly (--base=origin/main --head=<branch>) or set NX_BASE and NX_HEAD. As the base, the last successful commit on the main branch is recommended, so that everything since the last green run is included.
The whole history is a prerequisite: without fetch-depth: 0 (GitHub) or fetchDepth: '0' (Azure DevOps), there is no base commit to compare against.
GitHub Actions (sketch, not run). For GitHub, the page names the action nrwl/nx-set-shas, which sets NX_BASE and NX_HEAD from the last successful run of the main branch. For the versions of the actions, see the page; which input it needs for your main branch, we did not read: open.
steps:
- uses: actions/checkout@<version>
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@<version>
- run: npm ci
- run: node tools/boundary-probe.mts
- run: npx nx affected -t lint test build
Azure DevOps (sketch, not run). According to our summary of the Nx example, the pipeline checks out with fetchDepth: '0', computes BASE_SHA and HEAD_SHA (for pull requests the merge base with the target branch, otherwise the last successful build, fallback HEAD~1) and calls npx nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) -t lint test build. A sketch of our own for it, without the computation of the base:
steps:
- checkout: self
fetchDepth: '0'
- script: npm ci
- script: node tools/boundary-probe.mts
- script: npx nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) -t lint test build
Two things belong in every pipeline: the exit code counts (a red lint run has to turn the pipeline red, otherwise the boundaries from 0102 are wishes), and the probe boundary-probe.mts runs as a step of its own. npm ci instead of npm install installs exactly what the lock file says.
Remote cache
The local cache lives on your machine. In CI, every run has a fresh machine, so it is empty unless someone shares it. The page "Remote cache" names two ways: Nx Cloud (connect with npx nx@latest connect; shares results between development machines and CI) and a self-hosted cache server against an OpenAPI description (page "Self-hosted remote cache", not read).
Which storage is possible for that, the page does not say. Prices and license terms of Nx Cloud are not there: open, read them at Nx before using it. Note what a shared cache means: results that someone else produced run on your machine unchecked; access rights and trust are part of the decision (an ADR, K4 4.1.3).
Conventions for commits
A monorepo with many projects benefits from readable commits, because versions and changelogs later come from them (lesson 0302). Conventional Commits prescribe that every message begins with a type, optionally with a scope: feat(kontakte-ui): add card class, fix(shared-util-datum): ..., chore(deps): ..., docs: ..., test: ..., ci: .... The project name that Nx knows suggests itself as the scope. The topic is not in the lessons K1 to K3 (the plan referred to a K2 lesson that does not exist); we explain it here only to this extent. We did not read the specification: open.
Exercise 1
Produce and prove a cache hit.
- In your workspace, run
npx nx run-many -t lint test build; read the exit code. - Run the same command once more, without changing anything.
- Change a value in
tsconfig.base.json(and leave it changed; resetting it would give the old hash again) and run the command again. - Note for each run the line with "Cache:" and the duration in
docs/cache.md.
How to check: run 1 has 0 hits or only some, run 2 "13/13 hit" (for you the number of your tasks) with "Nx read the output from the cache", run 3 0 hits again. All three end with exit=0.
Solution 1
- Run 2 has no hits: something changes between the runs (a file is generated, and the
inputsof a task contain it).nx show project <name> --jsonshows theinputs. - Run 2 has hits, but the test did not "really" run: expected; it was already green with the same inputs.
testreports exit 1 "No test files found" on the first run: see lesson 0102 (passWithNoTests).- Run 3 still has hits: the changed file is in no input.
tsconfig.base.jsonwas one for us (0 of 13).
Exercise 2
Affected: a commit in a library.
- Commit the current state. Then change a file in
libs/kontakte/ui/src(for example a class in the template) and commit withfeat(kontakte-ui): add name class. - Run
npx nx affected -t lint test build --base=HEAD~1 --head=HEAD --explainand note how many of how many tasks are affected and which projects are among them. - Run it without
--explainand read the exit code. - Then change only the
README.md, commit withdocs: update readmeand ask again with--explain.
How to check: in step 2, kontakte-ui itself, the feature library and the application are among them, but not termine-data-access; step 3 ends with exit=0; step 4 reports "Nothing affected".
Solution 2
- Everything is affected: you changed a file at the root (for example
tsconfig.base.jsonor the lock file); those apply to all tasks. - Nothing is affected although you changed
libs/: the change is not committed (--head=HEADreads the history), or--basepoints to the same commit. - Nx reports Git errors: no repository or no commit; in CI a copy that is too shallow (
fetch-depth: 0). - The numbers differ: you have other projects; what matters is that projects with no path to the change are missing.
What you can do now
- You read
inputs,outputsanddependsOnof a task and know what determines the hash (includingCIfor tests). - You prove a cache hit from the output and explain when it fails to appear.
- You have
nx affected -twith--explainshow which tasks a commit triggers, and you know the difference between project-based and task-based (Nx 24). - You sketch a pipeline with full history, an explicit base,
npm ciand a boundary probe, and you know what was not checked there. - You place the remote cache, name the open points and write commits following Conventional Commits.
Further reading
- Nx: Cache task results: hash,
inputs,outputs,targetDefaults. - Nx: Run only tasks affected by a PR:
--base,--head,--explain, changes in Nx 24, notes for GitHub, CircleCI, GitLab. - Nx: Remote cache: Nx Cloud and self-hosting.
- Nx: CI workflow examples, Azure: the example for Azure Pipelines.
The Kladde: nx.json with its namedInputs and a pipeline sketch with full history, affected and the probe. That completes the module. Next comes lesson 0201 in module W10.2: strategic DDD and the contexts of the Kladde.