What the Nx project graph represents

The Nx project graph is a model Nx derives from your workspace. Each node is either a project in the repository (an application or library) or an external dependency installed from npm, such as React or Vite. Each edge says that one node depends on another. You rarely declare these edges yourself. Nx reads source imports, installed packages and TypeScript configuration and infers them, then caches the result so later runs only reanalyse changed files (Nx mental model).

Because the graph is derived rather than hand-maintained, it is only as accurate as what Nx can see in your files. Later sections cover where that breaks down.

The project graph is different from the task graph. Nx builds a task graph each time you run a command. Its nodes are project:target pairs such as myapp:build, and the project graph determines which of those nodes exist and in what order they run.

Project graph                     Task graph for "nx build api"

  api ──► shared-db ──► pg (npm)    shared-db:build
   │                                      │
   └────► shared-types                    ▼
                │               shared-types:build ──► api:build
Aspect Project graph Task graph
Node Project or npm package project:target pair
Edge Code or package dependency Task must finish first
When built Continuously, cached Per command invocation
Main use Structure, affected analysis Execution order, parallelism

The engineers who benefit most are backend and platform engineers who own CI pipelines, build order and shared libraries. For them, a wrong edge means either a broken build or wasted CI time.

 PROJECT GRAPH (derived, cached)          TASK GRAPH ("nx build api")
 nodes: projects + npm packages           nodes: project:target pairs
 edge:  A ──► B = A depends on B          edge:  A ──► B = A runs before B

 ┌──────────┐      ┌───────────┐          ┌─────────────────┐
 │   api    │─────►│ shared-db │          │ shared-db:build │
 │  (app)   │      │   (lib)   │          └────────┬────────┘
 └────┬─────┘      └─────┬─────┘                   │
      │                  │                         ▼
      │                  ▼                ┌─────────────────┐
      │            ┌───────────┐          │    api:build    │
      │            │    pg     │          └─────────────────┘
      │            │   (npm)   │                   ▲
      │            └───────────┘                   │
      ▼                                   ┌────────┴──────────┐
 ┌──────────────┐                         │ shared-types:build│
 │ shared-types │                         └───────────────────┘
 │    (lib)     │
 └──────────────┘

 pg is an external npm node: it is in the project graph but has no task.
 Project graph ──► decides which task nodes exist and the order they run.

The project graph links projects and npm packages by dependency, and Nx uses it to derive the per-command task graph, where shared-db:build and shared-types:build must finish before api:build.

Who should use the project graph

Anyone working in an Nx workspace already depends on the project graph, because every nx build, nx test and nx affected call reads it. The question is who should inspect and maintain it on purpose:

  • Platform and CI owners. They decide which projects run on each pull request. Affected-only CI is only as correct as the graph's edges, so they need to know where those edges come from and where they are missing.
  • Maintainers of shared libraries. A library's dependents in the graph are its blast radius. Before changing a widely imported library, check how many projects depend on it.
  • Engineers debugging build order or unexpected CI runs. "Why did CI test this project?" and "Why didn't it?" are both graph questions. nx graph and nx show project answer them faster than reading CI logs.
  • Tech leads enforcing architecture. The graph shows deep imports between services, cycles and hub libraries that code review often misses.

It matters less in a workspace with only a few tightly coupled projects. There, nearly every change affects everything, and the graph adds little beyond build ordering. It matters most when a repository has many projects and most changes touch a small part of them.

How it works, end to end

Building and using the graph happens in this order:

  1. Detection. Nx scans the workspace for project.json files, package.json files included by your package manager's workspaces configuration, and files that registered plugins recognise. Each match becomes a project node.
  2. Inference. Nx statically analyses source imports, installed packages and TypeScript path mappings, and turns each resolved import into an edge. npm packages become external nodes.
  3. Manual declarations. Edges you declare yourself, such as implicitDependencies, are added for links that static analysis cannot see.
  4. Caching. Nx stores the result and reanalyses only changed files on later runs.
  5. Task graph. For each command, Nx combines the project graph with each target's dependsOn configuration to decide which tasks run, in what order, and which can run in parallel.
  6. Affected and computation caching. nx affected walks the graph from changed projects to their dependents. Computation caching then skips any task whose inputs haven't changed.

The next sections cover each step in detail, along with how to check it when the result looks wrong.

How Nx builds the graph: project detection, dependency inference and caching

Nx builds the graph in two passes: first it finds the nodes, then it works out the edges between them.

Project detection

Nx scans the file system for projects. A directory becomes a project if it contains a project.json file, or a package.json that your package manager's workspaces configuration includes. Plugins can change how Nx identifies projects, so a tool's own config file can mark a directory as a project. Because detection is based on files, project boundaries always match what is on disk. If a project is missing from the graph, check the workspaces globs and the plugin configuration first.

Dependency inference

Nx infers edges by analyzing three things: source code imports, installed dependencies, and TypeScript configuration such as path mappings. An import of @acme/billing resolves through a path alias or workspace package to a concrete project, and that resolution becomes an edge. This analysis is static, so it cannot see dependencies that are only expressed at runtime. Examples include a service that shells out to another project's build output, a dynamically constructed import path, or a shared contract consumed over HTTP. For these cases you declare the dependency manually, for example with implicitDependencies in project.json.

Caching the graph

Nx stores the computed graph and reanalyzes only the files that changed. Large repositories stay fast this way. The cost is a stale cache: if the graph shows an edge you deleted, or misses one you just added, the cache is a likely cause. Run nx reset to clear the cache before you investigate further.

nx show projects

nx show project billing-worker

nx reset

nx graph --file=graph.json
jq '.graph.dependencies["billing-worker"]' graph.json

A debugging sequence that checks project detection, declares a runtime-only dependency with implicitDependencies, clears a possibly stale cache with nx reset, and exports the rebuilt graph so you can inspect one project's edges.

Inspecting the graph with nx graph and nx show project

Once the cache is clean, inspect what Nx actually computed instead of assuming it matches your mental model.

nx graph opens an interactive view of every project and the project dependencies between them. Use it to answer structural questions: does api really depend on shared-utils, and why does a change in logging reach half the workspace? Each edge comes from something Nx found in source imports, installed packages, TypeScript path mappings or an explicit declaration. If an edge looks wrong, look for the import or config that produced it rather than treating the edge as correct by default.

nx show project <name> answers a different question: what tasks this project has and where each setting came from. The second part matters because Nx merges task configuration from three layers, and later sources win:

Layer Where it lives Typical use
1. Plugin inference Plugins registered in nx.json read tool config such as vite.config.ts Default commands, inputs, outputs, cache settings
2. targetDefaults nx.json Workspace-wide overrides for a target name
3. Project config The project's project.json or package.json Per-project exceptions

Inference removes duplication, but it also means no single file describes a task. A build target's outputs might come from a plugin reading Vite config, with its inputs overridden in nx.json. Before you edit anything, use nx show project to see which layer set the value you want to change.

nx graph --focus=api

nx graph --file=graph-before.json

nx show projects

nx show project api --json

nx show project api --json | jq '.targets.build'

nx show project api --web

Shell commands for checking the project graph and then tracing a single project's merged task configuration back to the layer that set each value.

How the graph drives task execution, affected commands and caching

When you run a task, Nx builds a task graph from two inputs: the project graph and the task dependencies declared for each target. Suppose api:build has dependsOn: ["^build"]. Nx then follows the edges from api in the project graph and schedules build for every project api depends on, before api itself. Two tasks that don't depend on each other can run in parallel. The project graph says which projects are connected, and the task configuration says which of those connections affect build order. That is why the task graph comes out wrong whenever the project graph is wrong (Nx mental model).

Affected commands use the same edges. nx affected maps the changed files to the projects that own them, then walks the graph to every project that depends on those projects. Only those projects get their targets run. Computation caching then works on the resulting tasks. If a task's inputs are unchanged, Nx restores the stored output and doesn't run the task again.

This pays off most in repositories with dozens or hundreds of projects, where most changes touch only a small part of the graph. In a repo with five tightly coupled services, nearly every change affects everything, and affected-only CI skips almost nothing.

How much CI work gets skipped depends on how accurate the graph is. A missing edge means a dependent project isn't rebuilt or retested, and a broken change can merge with green checks. An extra edge means CI rebuilds projects that didn't change, which costs runner time and engineering time spent waiting on pipelines.

npx nx graph --file=project-graph.json

npx nx build api --graph=task-graph.json

npx nx show projects --affected --base=origin/main --head=HEAD

npx nx affected -t build test --base=origin/main --head=HEAD

npx nx affected -t build test --base=origin/main --head=HEAD

These commands show how Nx derives a task graph from the project graph, uses the same graph edges to pick affected projects in CI, and skips unchanged tasks through computation caching.

Reading the graph for architectural problems and their cost

The shape of the graph decides how much work CI does on every pull request, and how long engineers spend waiting on it. Two patterns cause most of that waste.

Unexpected edges and hub libraries

The first pattern is an edge that shouldn't exist, such as orders-service depending on billing-service. This usually comes from a deep import into the other service's source tree instead of through a shared library. Nx records the edge from the import, so every change to billing now makes orders affected too. To fix it, move the shared code into a library both services depend on. You can also add project tags with a boundary lint rule so the edge can't come back.

The second pattern is a hub: one shared-utils library that almost every project imports. In nx graph it appears as a single node with edges fanning out to most of the workspace. Whenever someone edits it, nearly everything is affected.

Illustrative estimate (assumed numbers, not a benchmark). Assume 60 projects, with shared-utils imported by 50 of them. Assume each affected project takes 3 runner-minutes to build and test, and that the library changes 10 times a week. Then split it into four focused libraries, each imported by about 12 projects.

Runner-minutes per week = (dependents + 1) × minutes per project × changes per week

Layout Affected per change Runner-minutes per week
One shared-utils 51 51 × 3 × 10 = 1,530
Four focused libraries 13 13 × 3 × 10 = 390

Developers feel this less than the runner-minute totals suggest, because affected tasks run in parallel and cache hits skip work. Still, a smaller affected set means fewer tasks queued for each pull request and fewer unrelated test failures to sort through.

How the project graph saves engineering time

The graph saves time in four places, and each depends on the graph being accurate.

  • Smaller CI runs. Affected commands run targets only for changed projects and their dependents, instead of the whole workspace. Fewer tasks per pull request means shorter queues and less waiting.
  • Fewer repeated tasks. Computation caching restores outputs for tasks whose inputs haven't changed, so re-running a pipeline after a flaky failure, or rebuilding locally after switching branches, skips work that was already done.
  • No hand-maintained build order. Because edges are inferred from imports, adding a new library or a new import updates build order automatically. Nobody has to edit a CI script or a dependency list to keep them in sync.
  • Faster debugging. "Why did this project run?" and "Where does this target's config come from?" are answered by nx graph and nx show project, not by reading CI scripts and several config files by hand.

Illustrative estimate (assumed numbers, not a benchmark). Assume a workspace of 40 projects, where a typical pull request changes one library with 5 dependents, and each project takes an assumed 4 runner-minutes to build and test. Running everything costs 40 × 4 = 160 runner-minutes per pull request. An affected-only run costs (1 + 5) × 4 = 24 runner-minutes. Wall-clock savings will be smaller than this ratio, because tasks run in parallel.

There is also a time cost to account for. Manual declarations need maintenance, stale caches occasionally need nx reset, and a missing edge can cost far more debugging time than it ever saved in CI. The savings are real only if someone owns the graph's accuracy.

When to avoid relying on the graph: blind spots and trade-offs

Static analysis only sees what the source code states. An import whose path is assembled at runtime, such as a plugin loader that concatenates a directory name, gives Nx no literal to resolve. Two services that talk over HTTP or a message queue share no import at all. A JSON schema or feature-flag file that several projects read from disk at startup is just a file path in a string. None of these show up as edges, so the graph understates real coupling.

The two kinds of error cost you different things:

Failure mode Effect Risk type Typical symptom
Missing edge Affected-only CI skips a project that actually changed behaviour Correctness A green pull request, then a broken deploy
Extra edge Unrelated projects get rebuilt and retested Cost Low cache hit rate and large affected sets

Missing edges do more damage because nothing flags them. Extra edges show up in CI time, but missing edges show up later as production incidents. Manual declarations such as implicitDependencies patch the gaps, but nothing checks them against the code. When the code changes, a manual edge can become stale or just wrong, and nobody notices.

The graph also models code dependencies, not deployment coupling. Two services built from unrelated projects can still break each other through an API change. Cover those concerns separately:

  • Explicit CI path filters for shared config files and other inputs the graph can't see.
  • Full test suites on a schedule to catch anything affected-only runs skipped.
  • Contract or integration tests between backend services that communicate at runtime.

Alternatives to an inferred project graph

An inferred graph is one way to decide what to build and test. The main alternatives trade accuracy against maintenance effort differently:

Approach How dependencies are known Strengths Weaknesses
Run everything on every change Not tracked Simple; nothing can be skipped by mistake CI time grows with the whole repository, not the size of the change
CI path filters per directory Hand-written globs in pipeline config Explicit and easy to read Transitive dependents are missed unless someone lists them; filters drift as code moves
Hand-maintained build scripts or dependency lists Declared manually by engineers Full control over order Every new import needs a matching config edit; mistakes are silent
Build systems with explicitly declared per-directory build files Each target declares its dependencies, and undeclared ones are rejected Precise; dependencies are checked, not guessed More configuration to write and review for every target
Separate repositories with versioned packages Version ranges in package manifests Coupling is explicit at release boundaries No atomic cross-project changes; integration problems surface after publishing

Nx's inferred graph sits between these. It needs less configuration than explicitly declared builds, and it tracks transitive dependents that path filters miss. But it relies on static analysis, so it inherits the blind spots described above. Many teams combine it with some of these alternatives, such as scheduled full runs and path filters for files the graph can't see.

Debugging a wrong project graph: a checklist

When an edge is missing or a project looks wrong, work through these checks in order. Each one rules out a layer before you move to the next.

  1. Confirm the directory is a project. Run nx show projects and look for it. Nx only detects a directory through a project.json, through a package.json covered by your package manager's workspaces configuration, or through a plugin (Nx mental model). If the directory isn't listed, no import analysis will connect it to anything.

  2. Trace how the import resolves. An import can resolve through a paths alias in the root tsconfig or through a workspace package. If the alias points somewhere unexpected, or the package name doesn't match the consumer's dependency entry, Nx attaches the edge to a different project or to none.

  3. Find dependencies Nx can't infer. Static analysis does not detect shared config files, generated code, file paths read at runtime or service names resolved from environment variables. Declare these with implicitDependencies in the project configuration.

  4. Check plugins. Plugins registered in nx.json can add projects, rename them, or infer targets. Run nx show project <name> to see each setting and its source.

  5. Reset cached state. If the graph still doesn't match the working tree, run nx reset to clear the cached graph and stop the daemon, then regenerate the graph. If the reset fixes it, the problem was stale state. Your configuration was not the cause.

Treat manual dependencies like code. Add a short comment explaining why each one exists, and review the list when you remove or restructure a project. An implicit dependency that nobody revisits becomes a false edge. It widens affected runs and hides the real structure of the graph.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to nx project graph, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • The dependency graph answers "what does this touch?" in seconds instead of a manual audit.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • You can size a change's blast radius before merging, avoiding surprise cross-team firefighting.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.