Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

Running Scripts Across Workspaces with pnpm

pnpm's recursive runner lets one command drive a script across every package in a workspace, but the defaults — parallelism, topological ordering, and how missing scripts are handled — decide whether that command builds your monorepo correctly or silently produces broken artifacts. This page covers the exact flags, the order they impose, and the failure modes to design around.

The Two Execution Modes

There are two distinct ways to "run a script across workspaces," and conflating them is the most common source of confusion.

  • Recursing with pnpm -r run <script> (alias pnpm --recursive) finds every workspace package that defines <script> and runs it, ordered by the dependency graph.
  • Running a root script with pnpm run <script> executes the script defined in the root package.json only. A root script may itself call pnpm -r run ..., which is the usual orchestration pattern.

Knowing which one you mean is foundational to the boundary discussion in Root-Level vs Package-Level Scripts: the root script orchestrates, the recursive run fans out to package-level work.

# Recurse: run "build" in each package that has it
pnpm -r run build

# Root script: run only the root package.json "build"
pnpm run build

Topological vs Parallel Order

By default, pnpm -r run executes in topological order — a package's script runs only after the scripts of every package it depends on have completed. This is correct for build, where @scope/ui must wait for @scope/core to emit its dist/ first.

# Topological by default: dependencies build before dependents
pnpm -r run build

--parallel discards ordering entirely and runs all matching scripts at once. Use it only for long-lived, order-independent tasks such as dev servers or watchers — never for build, where it causes dependents to compile against missing or stale outputs.

# No ordering — only safe for independent, long-running tasks
pnpm -r --parallel run dev

--workspace-concurrency caps how many package scripts run simultaneously while still respecting topological order. This is the right throttle for CI runners with limited cores; the default is the number of CPUs.

# Respect the DAG, but never run more than 4 at once
pnpm -r --workspace-concurrency=4 run build
Topological run order across workspaces A dependency graph where core builds first, then ui and utils in parallel, then the app last. wave 1 wave 2 wave 3 @scope/core no deps @scope/ui needs core @scope/utils needs core @scope/app needs ui, utils
pnpm -r run build executes in waves: core first, ui and utils together, app last.

Numbered Patterns

1. Build everything in dependency order

pnpm -r run build

This is the baseline. pnpm computes the dependency DAG from workspace links and runs build wave by wave. No package builds before its dependencies finish.

2. Skip packages that lack the script with --if-present

Without --if-present, recursing a script that some packages do not define is fine — pnpm simply skips packages without the script. The flag matters when you invoke a script by name in a single package context and want a missing script to be a no-op rather than an error.

# Will not error on packages missing "typecheck"
pnpm -r run typecheck --if-present

Use it on optional cross-cutting tasks (typecheck, lint) so an absent script never breaks the whole run.

3. Target a subset with --filter

--filter scopes the run to specific packages while still ordering them topologically. This is the precise targeting detailed in Using pnpm --filter for Targeted Builds.

# Just one package
pnpm --filter @scope/ui run build

# The package plus everything it depends on (... prefix)
pnpm --filter '@scope/app...' run build

# Everything changed since main, plus their dependents ([ref] selector)
pnpm --filter '...[origin/main]' run build

4. Run dev servers in parallel with live output

pnpm -r --parallel --stream run dev

--parallel lifts ordering for long-running watchers; --stream interleaves each package's output with a package-name prefix so you can tell which server logged what. Without --stream, pnpm buffers output and prints it per package as each finishes — useless for never-ending dev processes.

5. Throttle concurrency on constrained runners

pnpm -r --workspace-concurrency=2 run build

On a 2-core CI runner, capping concurrency prevents OOM and CPU thrashing while still honoring the DAG. Set it to the number of cores you actually want to commit.

6. Orchestrate from a root script

Define a root package.json script that recurses, so contributors run one memorable command:

{
  "scripts": {
    "build": "pnpm -r run build",
    "dev": "pnpm -r --parallel --stream run dev",
    "test": "pnpm -r run test --if-present"
  }
}

Now pnpm run build at the root fans out correctly, and CI calls the same entry point local developers use.

When Scripts Fail

  • Dependent built against stale output. Symptom: a package compiles using an old dist/ from a dependency. Cause: --parallel was used for build, discarding order. Fix: drop --parallel; let topological order apply.
  • No projects matched the filters. The --filter pattern matched nothing — usually a typo in the package name or a [ref] selector with no changed packages. Verify with pnpm -r list first.
  • Run aborts midway with a non-zero exit. By default pnpm fails fast on the first errored package. Add --no-bail to run the rest and collect all failures, useful for test/lint sweeps.
  • A required script is missing in one package. A bare pnpm --filter pkg run build errors if pkg has no build. Add --if-present to make it a no-op.

These ordering and lockfile concerns connect directly to Lockfile Management Strategies — a drifted lockfile changes the workspace graph and therefore the run order.

Validation

# Show the resolved workspace packages and their order inputs
pnpm -r list --depth -1

# Confirm the build wave order without running anything heavy
pnpm -r run build --workspace-concurrency=1
# Watch the order packages are reported in — it follows the DAG

# Verify a filtered selection matches what you expect before running
pnpm --filter '...[origin/main]' list --depth -1

Prevention & CI Guardrails

  • Default to topological order; reserve --parallel strictly for long-running, order-independent tasks like dev.
  • Add --if-present to any workspace-wide test/lint/typecheck so optional packages never break the run.
  • Pin pnpm with the packageManager field so local and CI runs compute the same graph and ordering.
  • Use --workspace-concurrency matched to runner cores in CI to avoid OOM, rather than relying on the CPU-count default.
  • Wrap fan-out commands behind root scripts so there is one canonical entry point shared by humans and CI.
  • Use --no-bail in non-blocking sweeps to surface every failure at once instead of stopping at the first.

Frequently Asked Questions

What is the difference between pnpm -r run build and pnpm run build? pnpm -r run build recurses and runs the build script in every workspace package that defines it, in topological order. pnpm run build runs only the root package.json build script — though that root script commonly calls pnpm -r run build itself.

Does --parallel make builds faster? Sometimes, but it removes topological ordering, so dependents can build against missing or stale dependency output. For build, use the default order (optionally throttled with --workspace-concurrency). Reserve --parallel for dev servers and watchers.

Why does my recursive run stop at the first failing package? pnpm fails fast by default. Add --no-bail to continue running the remaining packages and report all failures together — useful for test and lint sweeps where you want the full picture.

How do I keep CI output readable when running many packages at once? Add --stream so each package's output is interleaved with a package-name prefix as it is produced. Without it, pnpm buffers and prints output per package on completion, which is unhelpful for parallel or long-running tasks.

Related

Root-Level vs Package-Level Scripts