nx.json Explained: Every Field in Nx Monorepo Configuration

Last updated:

nx.jsonis the workspace configuration file for Nx monorepos — a strict JSON document at the repo root that controls task running, caching, the affected command, plugins, and release. Workspace-wide settings live here; individual project settings live in each project's project.json (or are inferred from plugins). The two files cooperate: nx.json defines defaults under targetDefaults, and each project's targets inherit those defaults unless they override. For Nx 19+ workspaces, the most-edited fields are namedInputs, targetDefaults, and plugins — the three together govern what gets cached, how task graphs are built, and which targets get inferred automatically from your file structure.

Hand-editing nx.json and your nx build just failed with a JSON parse error? Paste it into Jsonic's JSON Validator — it points to the exact line, with diagnostics for trailing commas and quote mismatches.

Validate nx.json

What nx.json controls (and what lives in project.json instead)

nx.json is the only workspace-wide config file Nx requires. Everything you set here applies to the whole monorepo: every app, every library, every plugin. The fields that show up most often in a real nx.json are namedInputs, targetDefaults, plugins, tasksRunnerOptions, defaultBase, release, and nxCloudId.

Per-project settings live in project.jsonnext to each project's source: name, sourceRoot, projectType (application or library), tags, and any targets that need to override the workspace defaults. In Nx 19+, many projects have no project.json at all — targets are inferred from the plugin list (@nx/vite reads vite.config.ts, @nx/jest reads jest.config.ts, and so on). The split is clean: workspace policy in nx.json, project identity and overrides in project.json.

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"]
  },
  "targetDefaults": {
    "build": { "cache": true, "dependsOn": ["^build"] }
  },
  "defaultBase": "main"
}

The $schema field is optional but makes VS Code autocomplete every other field. Nx ignores unknown top-level keys, so adding it costs nothing. For broader context on monorepo config files, see our JSON config files overview.

targetDefaults: shared task configuration

targetDefaults sets default behavior for any target with a given name — every project that has a build target inherits whatever targetDefaults.build defines. The most common fields under each target entry are cache, dependsOn, inputs, outputs, and options.

{
  "targetDefaults": {
    "build": {
      "cache": true,
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "outputs": ["{projectRoot}/dist", "{projectRoot}/.next"]
    },
    "test": {
      "cache": true,
      "inputs": ["default", "^production"],
      "options": { "passWithNoTests": true }
    },
    "lint": {
      "cache": true,
      "inputs": ["default", "{workspaceRoot}/.eslintrc.json"]
    },
    "e2e": {
      "cache": true,
      "inputs": ["default", "^production"]
    }
  }
}

The ^build notation in dependsOnmeans “run the build target on every project this one depends on, first.” This is topological ordering — if web depends on ui and utils, both ui and utils build before web. Without this, you would get a runtime error or stale output when building the leaf.

FieldTypePurpose
cachebooleanEnable caching for this target (replaces older cacheableOperations list)
dependsOnstring[]Targets that must run first. ^build = dependencies' build first; build = same project's build first.
inputs(string | object)[]Files/patterns whose changes invalidate the cache for this target
outputsstring[]Paths Nx copies into the cache and restores on a cache hit
optionsobjectDefault options merged into every project's target options

namedInputs and inputs: defining cache keys

namedInputs defines reusable groups of file patterns that targetDefaults.<target>.inputs can reference by name. Two names are conventional: default covers everything that could affect any task on a project, and production is the subset that affects production builds — typically default minus test files, story files, and dev-only configs.

{
  "namedInputs": {
    "default": [
      "{projectRoot}/**/*",
      "sharedGlobals"
    ],
    "production": [
      "default",
      "!{projectRoot}/**/*.spec.ts",
      "!{projectRoot}/**/*.test.ts",
      "!{projectRoot}/**/*.stories.{ts,tsx,js,jsx}",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/jest.config.{ts,js}",
      "!{projectRoot}/.eslintrc.json"
    ],
    "sharedGlobals": [
      "{workspaceRoot}/tsconfig.base.json",
      "{workspaceRoot}/.env",
      "{workspaceRoot}/package-lock.json"
    ]
  }
}

The exclamation mark prefix excludes a pattern. When a target's inputs references "production", edits to *.spec.tsdo not change the build's cache key, so the build cache survives a test-only edit. The test target still references default, so it correctly busts when test files change.

The carat prefix on an input name (^production) means “the production inputs of every project this one depends on.” A project's build cache key includes its own production files plus its dependencies' production files. Without the carat, a change in libs/ui would not invalidate the cache of apps/web that depends on it.

Custom namedInputs entries are fine. Common ones include assets (image/font files), sharedConfigs (root-level configs that affect many projects), and target-specific groups like storybookConfig.

release configuration for nx release

nx release publishes packages from an Nx monorepo with changelog generation, version bumping, and git tagging. The release field in nx.json tells it which projects to release, how to version them, and what changelog format to use. For npm publishing, this often replaces tools like changesets or semantic-release.

{
  "release": {
    "projects": ["libs/*"],
    "projectsRelationship": "independent",
    "version": {
      "conventionalCommits": true,
      "generatorOptions": {
        "currentVersionResolver": "git-tag",
        "specifierSource": "conventional-commits"
      }
    },
    "changelog": {
      "workspaceChangelog": {
        "createRelease": "github"
      },
      "projectChangelogs": true
    },
    "git": {
      "commitMessage": "chore(release): publish {version}",
      "tagMessage": "Release {version}"
    }
  }
}

projects accepts globs and tag matchers — libs/* picks every project under libs/, {tag:publishable} picks every project tagged publishable in its project.json. projectsRelationship: "independent" versions each project on its own schedule; "fixed" versions them together (all bumped to the same number on release).

conventionalCommits: true tells Nx to read commit messages and pick the bump level automatically (fix: → patch, feat: → minor, BREAKING CHANGE → major). With createRelease: "github", Nx creates a GitHub Release alongside the tag. Run nx release with no arguments for an interactive prompt; nx release --dry-run previews everything without writing.

tasksRunnerOptions and Nx Cloud setup

tasksRunnerOptions chooses the task runner — the component that schedules and executes targets — and configures it. The default runner is nx/tasks-runners/default, which uses local caching only. Connecting to Nx Cloud switches the runner to nx-cloud for remote caching and Distributed Task Execution (DTE).

{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx-cloud",
      "options": {
        "cacheableOperations": ["build", "lint", "test", "e2e"],
        "accessToken": "...",
        "parallel": 3
      }
    }
  },
  "nxCloudId": "abc123def456"
}

In Nx 19+, the recommended pattern moves cacheableOperations off the runner and onto each targetDefaults entry via the cache flag — same effect, more readable. Both styles still work; older workspaces commonly keep the runner-level list during migration.

nxCloudId at the workspace root replaces the older nxCloudAccessToken approach. Once set, every cacheable target on every machine — laptops, CI, ephemeral preview agents — reads from and writes to the same remote cache. A fresh PR branch on CI can replay results from a previous build without re-executing anything that did not change.

Distributed Task Execution (DTE): with Nx Cloud connected, CI can fan a single nx affected -t build test lint across multiple agents. Nx Cloud orchestrates which agent runs which target based on a learned task duration model, then aggregates results into one report. The wall-clock impact on large monorepos is typically 5–10× faster CI without changing pipeline structure. Run nx connect to wire everything up.

plugins array: @nx/eslint, @nx/jest, @nx/vite, @nx/playwright

The pluginsarray tells Nx to load “target inference” plugins. Each plugin scans the workspace for files it recognizes (a vite.config.ts, a jest.config.ts, an eslint.config.js) and infers targets automatically — no need for a project.json with hand-written target objects.

{
  "plugins": [
    {
      "plugin": "@nx/eslint/plugin",
      "options": { "targetName": "lint" }
    },
    {
      "plugin": "@nx/vite/plugin",
      "options": {
        "buildTargetName": "build",
        "testTargetName": "test",
        "serveTargetName": "serve"
      }
    },
    {
      "plugin": "@nx/jest/plugin",
      "options": { "targetName": "test" }
    },
    {
      "plugin": "@nx/playwright/plugin",
      "options": { "targetName": "e2e" }
    }
  ]
}

When @nx/vite/plugin is loaded, Nx finds every vite.config.ts in the workspace and registers a build, test, and servetarget on the project that owns it. The plugin also wires inputs and outputs correctly — Vite's output directory becomes the cached path, the config file becomes part of the input hash. You write the Vite config you would have written anyway; Nx infers the rest.

The trade-off vs explicit project.json targets is discoverability: with inferred targets, the only way to see what targets a project has is nx show project <name> or nx graph. The win is far less boilerplate — a typical Nx 19+ library has a one-line project.json and inherits everything else.

For the project-side view of how inferred targets compose with explicit ones, see our package.json workspaces guide.

affected command, base branch, and CI optimization

nx affected runs a target only on projects affected by changes in your git diff. nx affected -t test tests just the projects whose files, or their transitive dependencies, changed between two refs. On large monorepos this is the difference between a 2-minute CI and a 40-minute CI.

{
  "defaultBase": "main",
  "namedInputs": {
    "sharedGlobals": [
      "{workspaceRoot}/tsconfig.base.json",
      "{workspaceRoot}/package-lock.json"
    ]
  }
}

defaultBase sets the git ref nx affected diffs against by default. Most workspaces set it to main (or master on older repos). On CI you typically override per run:

# GitHub Actions example
- run: npx nx affected -t lint test build --base=origin/main --head=HEAD

# For nightly full runs (everything, ignoring affected)
- run: npx nx run-many -t lint test build

The affected calculation has three steps: (1) build the project graph by scanning imports, (2) diff the files between base and head, (3) map files to projects, then expand to every dependent project. For correctness, the project graph must be complete — if project A reads a JSON file from project B at runtime (not via import), Nx cannot detect the link statically. Use implicitDependencies in project.json to declare those edges manually.

Common pitfall: forgetting that root-level files affect every project. A typo in tsconfig.base.json should rebuild everything, not nothing. Add the file to sharedGlobalsso it's part of every project's default input set.

Migrating from nx 16 to nx 19+: nx.json schema changes

The biggest nx.json shifts between Nx 16 and Nx 19+ are around target caching, plugin loading, and Nx Cloud auth. nx migrate latest handles most of these automatically — it reads your current nx.json, applies the relevant codemods, and writes a migrations.json for follow-up steps you can review before running.

ConceptNx 16Nx 19+
Declaring cacheable targetstasksRunnerOptions.default.options.cacheableOperations: [...]targetDefaults.<target>.cache: true (per-target)
Nx Cloud authnxCloudAccessToken in nx.jsonnxCloudId at top level + token in env var
Inferred targetsExplicit targets in project.jsonPlugin entries in plugins array; targets auto-detected
Cache restorationRestore on miss onlyReplay from remote cache by default with Nx Cloud
Workspace generators pathworkspace.json or angular.jsonNo global generator config — each project handles its own

Migration steps:

  1. Commit your working tree.
  2. Run nx migrate latest. This updates package.json and writes migrations.json.
  3. Install: npm install (or your package manager).
  4. Run the recorded migrations: nx migrate --run-migrations.
  5. Inspect nx.json diff. Move any leftover cacheableOperations entries to per-target cache: true.
  6. Reset the cache once to discard stale entries: nx reset.
  7. Run nx graph to verify the project graph still resolves correctly.

If your repo also has a Turborepo turbo.json or a pnpm-workspace.yaml, those keep working unchanged — Nx 19+ coexists with both, and the migration touches only nx.json and per-project files.

Key terms

nx.json
The workspace-wide configuration file for an Nx monorepo. Strict JSON at the repo root. Holds namedInputs, targetDefaults, plugins, tasksRunnerOptions, release, and the affected base branch.
target
A named task a project can run — typically build, test, lint, serve, e2e. Defined per-project (project.json) or inferred from plugins, with defaults from targetDefaults.
namedInputs
Reusable file-pattern groups referenced by name from inputs. Conventional names: default (everything) and production (default minus test/dev files). Used to compute cache hashes.
targetDefaults
Workspace-wide defaults applied to every project's target with a given name. A project's explicit target settings override the defaults field-by-field; absent fields fall back to the default.
affected
The set of projects whose files (or transitive dependencies' files) changed between two git refs. nx affected -t <target> runs the target only on those projects — the foundation of fast CI on large monorepos.
Nx Cloud
Vercel-acquired remote caching and Distributed Task Execution service for Nx. Connected via nxCloudId in nx.json. Lets every machine in a team share cached task outputs and fans CI work across multiple agents.

Frequently asked questions

What's the difference between nx.json and project.json?

nx.json lives at the workspace root and holds settings that apply to the whole monorepo — namedInputs, targetDefaults, plugins, tasksRunnerOptions, release config, and the affected base branch. project.json lives inside each individual project (apps/web/project.json, libs/ui/project.json) and holds settings specific to that project: its name, sourceRoot, projectType, tags, and targets. Targets defined in project.json inherit from targetDefaults in nx.json. So if nx.json sets targetDefaults.build.cache to true, every project that defines a build target inherits caching without restating it. The split keeps repo-wide defaults in one place while letting individual projects override or extend them. In Nx 19+, many projects skip project.json entirely and rely on inferred targets from plugins (@nx/vite, @nx/jest, @nx/playwright) — nx.json then becomes the only config file you actually edit.

How do I configure caching in nx.json?

Set cache: true under the relevant entry in targetDefaults. Example: targetDefaults.build = { cache: true, inputs: ["production", "^production"], outputs: ["{projectRoot}/dist"] }. The cache flag enables local caching by default — Nx computes a hash from the listed inputs, and if the hash matches a previous run, it replays the cached outputs instead of executing the target. To make caching work correctly you also need accurate inputs (so the hash captures everything that affects the output) and accurate outputs (so Nx knows what files to cache and restore). The default cacheable operations in a fresh workspace are build, lint, test, and e2e. If your build is not being cached, the usual cause is missing or wrong inputs — for example, the build reads from .env but .env is not declared as an input, so changes do not invalidate the hash.

What is namedInputs and when should I use it?

namedInputs lets you define reusable groups of file patterns and reference them by name in targetDefaults.inputs and project-level target inputs. Two names are conventional: default (everything that could affect any task in the project) and production (the subset that affects production builds — typically default minus test files, story files, and dev-only configs). Defining production: ["default", "!{projectRoot}/**/*.spec.ts", "!{projectRoot}/**/*.test.ts", "!{projectRoot}/jest.config.ts"] means a test file change does not invalidate the build cache, but does invalidate the test cache. This is how Nx avoids cache misses on test-only edits. Use namedInputs whenever multiple targets share the same input set — defining it once at the workspace level keeps target configs short and consistent. Custom names (e.g., assets, sharedConfigs) are fine; the production name has special status as the conventional input for build-like tasks.

How does Nx Cloud integrate with nx.json?

Nx Cloud connects through two places. First, the nxCloudId (or nxCloudAccessToken on older setups) at the top level of nx.json tells the Nx CLI which Nx Cloud workspace to talk to. Second, tasksRunnerOptions.default.runner is set to "nx-cloud" (or you use the newer nxCloud: true shorthand) so task execution goes through the remote cache and distributed task execution layer. Once connected, every cacheable target writes to and reads from Nx Cloud — your CI builds populate the cache, your laptop reads from it, and a fresh PR branch can replay results from a previous CI run without re-executing. Distributed Task Execution (DTE) splits a single nx affected -t build across multiple CI agents while Nx Cloud handles orchestration. Run nx connect to wire everything up — it adds the right fields to nx.json and prints an auth URL.

Why are my targets not being cached?

Three causes account for nearly every cache miss. First — the target is not declared cacheable. Check targetDefaults.<target>.cache; if it is missing or false, Nx will not cache that target. Second — inputs are wrong. If a file your build depends on is not declared as an input, edits to that file do not change the cache key, and a stale cached output gets replayed. The opposite happens too: if inputs include files that change on every run (a timestamp file, the .git directory), every run misses the cache. Third — outputs are wrong. Nx caches by copying the declared outputs paths; if the real output goes somewhere else, Nx caches nothing useful. Run nx build my-app --verbose to see what inputs Nx hashed. For a true diagnosis, run nx reset, then run the target twice and compare. If the second run replays from cache, your config is correct.

How do I share configuration across multiple projects?

Use targetDefaults in nx.json. Anything defined under targetDefaults.<target-name> applies to every project that has a target with that name. Example: targetDefaults.build = { cache: true, dependsOn: ["^build"], inputs: ["production", "^production"], outputs: ["{projectRoot}/dist"] } sets caching, topological ordering (build deps first), inputs, and outputs for every build target across the repo. Projects override on a field-by-field basis by setting their own value in project.json; absent an override, the targetDefaults value applies. For configuration that is not target-specific — shared tsconfig settings, lint rules, formatting — use file-level mechanisms: a tsconfig.base.json at the root extended by each project, a single eslint.config.js, a workspace-wide prettier config. nx.json handles task configuration; tsconfig and lint configs handle code-level configuration.

What is the affected command and how do I configure it?

nx affected runs a target only on projects affected by the changes in your git diff. nx affected -t test runs tests just for projects whose files (or dependencies) changed between two git refs. Configure the base via nx.json: { "defaultBase": "main" } sets the default comparison ref. On CI, override per run with --base=origin/main --head=HEAD. Nx computes the project graph, finds files touched in the diff, maps them to projects, then includes every project that depends on those projects (transitively). The CI win is massive on large monorepos — a typo fix in one library does not re-run the entire test suite. For correctness, namedInputs must accurately describe what each project depends on, including shared files (root tsconfig, .env, lockfile). The implicitDependencies field on project.json covers cases where a dependency is not statically analyzable (a project that reads a JSON manifest at runtime, for example).

How do I migrate from a Lerna or Turborepo nx.json equivalent?

From Lerna: run npx nx@latest init in your existing Lerna repo. The Nx init command detects the workspace structure, infers targets from package.json scripts, and creates nx.json with sensible defaults. Lerna can keep handling publishing while Nx handles task running and caching — they coexist. From Turborepo: the concepts map cleanly. turbo.json pipeline entries become nx.json targetDefaults entries. dependsOn in turbo.json matches dependsOn in nx.json (^build means same thing in both: build dependencies first). turbo.json globalDependencies become nx.json namedInputs.default at the workspace level. turbo.json outputs and inputs translate directly. The biggest difference: Nx has a richer plugin system (@nx/jest, @nx/eslint, @nx/vite) that infers targets automatically, so you write less explicit config. See our Turborepo turbo.json comparison guide for the field-by-field mapping. The migration is mechanical — most teams complete it in an afternoon.

For the broader landscape of build-tool config files, JSON config patterns covers the patterns shared across nx.json, turbo.json, and similar tools.

Further reading and primary sources