Forge
Concepts

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
    |      |
    └──┬──┘
     done

Named 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:

TechnologyExclusions
JavaScript/TypeScript*.spec.ts, *.test.ts, jest.config.*, eslint.config.*, tsconfig.spec.json, test-setup.*
Mavensrc/test/**/*
Pythontest_*.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 typeInputsWhy
buildproduction, ^productionOnly rebuild when production source changes. Test file edits don't bust the cache.
testdefault, ^productionRe-run when any project file changes (including tests), but only track production files of dependencies.
lintdefault, ^productionSame as test — lint checks all files, but only cares about dependency production output.
verifydefault, ^productionSame 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:build uses ['production', '^production']

    • Re-runs if my-lib/src/main/** changes
    • Re-runs if shared-utils production files change
    • Does not re-run if my-lib/src/test/** changes
  • my-lib:test uses ['default', '^production']

    • Re-runs if any my-lib file changes (including tests)
    • Re-runs if shared-utils production files change

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:

  1. Create a production-inputs.ts in 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',
];
  1. Export it from your devkit's index.ts

  2. Set it in your project plugin:

namedInputs: {
  production: MY_TECH_PRODUCTION_INPUTS,
}
  1. Use standard input patterns in your create-nodes:
    • Build targets: ['production', '^production']
    • Test/lint targets: ['default', ...SHARED_GLOBAL_INPUTS, '^production']

On this page