Caching
How Forge caches task results to avoid redundant work
Caching
Forge uses Nx's computation caching to skip tasks whose inputs haven't changed. When you run a task like build or test, Forge computes a hash of the task's inputs. If that hash matches a previous run, the cached output is replayed instantly instead of re-executing the task.
Caching works both locally and remotely via Nx Cloud, so your CI pipelines and teammates benefit from the same cache.
How It Works
Every cacheable task defines:
- Inputs — the files and configuration that affect the task's output
- Outputs — the directories or files the task produces
When inputs haven't changed since the last run, Forge restores the outputs from cache.
inputs changed?
|
┌──┴──┐
no yes
| |
replay execute
cache task
| |
└──┬──┘
doneNamed Inputs
Rather than listing file patterns on every target, Forge uses named inputs — reusable sets of file patterns that targets reference by name.
default
Nx's built-in fallback. When no explicit inputs are configured, targets use default, which includes all files in the project:
{projectRoot}/**/*production
A subset of default that excludes test files and tooling configuration. Used by build targets so that editing a test file doesn't invalidate the build cache.
Each technology plugin defines its own production input with appropriate exclusions:
| Technology | Exclusions |
|---|---|
| JavaScript/TypeScript | *.spec.ts, *.test.ts, jest.config.*, eslint.config.*, tsconfig.spec.json, test-setup.* |
| Maven | src/test/**/* |
| Python | test_*.py, tests/**/*.py, conftest.py |
| .NET | *Tests*/**/*.cs |
| OpenAPI | (none — all files are production-relevant) |
Shared Global Inputs
Some workspace-level files affect all projects regardless of technology. Forge includes these shared global inputs in every target:
{workspaceRoot}/mise.toml— tool versions managed by mise
When mise.toml changes (e.g. upgrading a tool version), all task caches are invalidated because the toolchain changed.
Shared global inputs are defined centrally in @bjb-forge/nx-plugin-devkit and automatically included in all production and test input constants.
Input Patterns by Target Type
Forge follows a consistent pattern across all technologies:
| Target type | Inputs | Why |
|---|---|---|
| build | production, ^production | Only rebuild when production source changes. Test file edits don't bust the cache. |
| test | default, ^production | Re-run when any project file changes (including tests), but only track production files of dependencies. |
| lint | default, ^production | Same as test — lint checks all files, but only cares about dependency production output. |
| verify | default, ^production | Same as test. |
The ^ prefix means "evaluated against dependency projects." So ^production means the production inputs of all projects this project depends on.
Example
Consider a Java library my-lib that depends on shared-utils:
-
my-lib:builduses['production', '^production']- Re-runs if
my-lib/src/main/**changes - Re-runs if
shared-utilsproduction files change - Does not re-run if
my-lib/src/test/**changes
- Re-runs if
-
my-lib:testuses['default', '^production']- Re-runs if any
my-libfile changes (including tests) - Re-runs if
shared-utilsproduction files change
- Re-runs if any
Architecture
Cache inputs are defined at two levels:
1. Workspace Fallback (nx.json)
A minimal fallback ensures non-Forge projects still work:
{
"namedInputs": {
"production": ["default"]
}
}This means production equals default (all files, no exclusions) unless a Forge plugin overrides it.
2. Per-Project Overrides (Forge Plugins)
Each technology's project plugin sets namedInputs.production on every project it manages, with technology-specific exclusions and shared global inputs:
nx-js-devkit → JS_PRODUCTION_INPUTS
nx-maven-devkit → MAVEN_PRODUCTION_INPUTS
nx-python-devkit → PYTHON_PRODUCTION_INPUTS
nx-dotnet-devkit → DOTNET_PRODUCTION_INPUTS
nx-openapi-devkit → OPENAPI_PRODUCTION_INPUTS
nx-storybook-devkit → STORYBOOK_TEST_INPUTS
nx-plugin-devkit → SHARED_GLOBAL_INPUTS (included in all of the above)This layered approach means:
- Forge-managed projects get optimized caching with proper exclusions
- Non-Forge projects fall back to conservative caching (no exclusions, never misses a change)
Adding a New Shared Global Input
If a new workspace-level file should invalidate all caches when changed, add it to SHARED_GLOBAL_INPUTS in @bjb-forge/nx-plugin-devkit:
// packages/nx-plugin-devkit/src/lib/utils/cache.util.ts
export const SHARED_GLOBAL_INPUTS: CacheInput[] = [
'{workspaceRoot}/mise.toml',
'{workspaceRoot}/your-new-file', // add here
];All technology plugins spread SHARED_GLOBAL_INPUTS into their input constants, so the change propagates everywhere automatically.
Adding a New Technology's Production Inputs
When creating a new technology devkit:
- Create a
production-inputs.tsin your devkit:
import {
type CacheInput,
SHARED_GLOBAL_INPUTS,
} from '@bjb-forge/nx-plugin-devkit';
export const MY_TECH_PRODUCTION_INPUTS: CacheInput[] = [
'default',
...SHARED_GLOBAL_INPUTS,
'!{projectRoot}/**/test-files-to-exclude',
];-
Export it from your devkit's
index.ts -
Set it in your project plugin:
namedInputs: {
production: MY_TECH_PRODUCTION_INPUTS,
}- Use standard input patterns in your create-nodes:
- Build targets:
['production', '^production'] - Test/lint targets:
['default', ...SHARED_GLOBAL_INPUTS, '^production']
- Build targets: