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.

The Two Execution Modes There are two distinct ways to "run a script across workspaces," and conflating them is the most common source of confus 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.
The Two Execution Modes — the core idea of this section at a glance.
  • 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

Numbered Patterns This is the baseline. Numbered Patterns This is the baseline.
Numbered Patterns — the core idea of this section at a glance.
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.
When Scripts Fail These ordering and lockfile concerns connect directly to Lockfile Management Strategies — a drifted lockfile changes the When Scripts Fail These ordering and lockfile concerns connect directly to Lockfile Management Strategies — a drifted lockfile changes the workspace graph and therefore the run o
When Scripts Fail — the core idea of this section at a glance.

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

Validation

Validation Validation in production JavaScript package workflows. Validation Validation in production JavaScript package workflows.
Validation — the core idea of this section at a glance.
# 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.
Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows. Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows.
Prevention & CI Guardrails — the core idea of this section at a glance.

Recursive runs and execution order

pnpm's recursive flag (-r or --recursive) runs a script across every workspace package that defines it, and understanding how it orders and parallelizes those runs is what makes it predictable. By default, pnpm runs the script in topological order for tasks that have inter-package dependencies — a package's script runs after the scripts of the packages it depends on — and parallelizes independent packages up to a concurrency limit. This means pnpm -r build builds packages in the right order without you sequencing them, because pnpm derives the order from the workspace graph.

Recursive order Topological order, parallel where independent, skip missing. pnpm -r build across packages topological order deps first skip missing no stub needed
pnpm -r derives the order from the graph and parallelizes independent packages.
# Run build across all packages, in dependency order
pnpm -r build
# Run in parallel where the graph allows, capped concurrency
pnpm -r --workspace-concurrency=4 test
# Skip packages that don't define the script (default behavior)
pnpm -r lint

pnpm skips packages that do not define the script rather than erroring, so a heterogeneous workspace runs cleanly without a no-op stub in every package. The topological ordering matters most for build-like tasks where one package's output feeds another; for independent tasks like linting, the order does not matter and parallelism dominates. Knowing that -r gives you graph-ordered, parallelized, skip-missing execution is what lets you use one command across the whole workspace and trust it to do the right thing, rather than scripting the sequencing by hand.

Filtering the recursive run to a subset

A recursive run does not have to cover every package; combining -r with a filter scopes it to a subset, which is how you run a script across just the affected packages or a particular area of the workspace. The change-aware filter is the most valuable: --filter '...[origin/main]' runs the script only in packages changed since the base plus their dependents, so a whole-workspace command becomes a scoped one that tracks the change.

Scoped recursion Filter selects the subset; -r runs across it. --filter '...[base]' affected subset recursive execution topological order cost tracks change scoped run
Combining a filter with recursion runs exactly the packages a task needs.
# Run test only in packages changed since main, plus dependents
pnpm --filter '...[origin/main]' test
# Run build in one package and its dependencies (prerequisites first)
pnpm --filter 'my-app...' build
# Run in a directory subtree
pnpm --filter './packages/ui/**' lint

The filter selects the packages; the recursive execution runs the script across them in topological order. This composition is what makes pnpm's script running suitable for real pipelines: a CI run scopes to the affected set so its cost tracks the change, a targeted build selects a package and its prerequisites, and a team's area is addressed by a path filter. The precondition for the change-aware form is full git history so the base resolves and an accurate dependency graph so the dependent traversal is complete — the same requirements that govern any affected-based selection. Between graph-ordered recursive execution and change-aware filtering, one command runs exactly the packages a task needs, in the right order, which is the foundation of a fast and correct workspace pipeline.

Combining recursive runs with caching

A recursive pnpm run executes the script across the selected packages, but on its own it recomputes every one on every invocation; layering a task runner's cache on top is what makes a large workspace's runs fast rather than merely scoped. A runner like Turborepo reads the same workspace graph pnpm does, so turbo run test --filter='...[origin/main]' runs the affected packages and replays any whose inputs are unchanged, on this machine or another through a remote cache.

Recursion plus cache Filter scopes; cache replays unchanged. filter affected scope the run cache replays unchanged packages cost tracks change fast at scale
Filtering narrows the set; caching short-circuits the survivors that didn't change.

The two mechanisms are complementary: the filter narrows the set to what a change can reach, and the cache short-circuits the packages within that set whose inputs did not actually change. Neither substitutes for the other — filtering without caching still recomputes every affected package, and caching without filtering still checks every package in the repo. Used together, a one-line change runs its package and dependents, and even among those, only the genuinely-changed ones are recomputed. This is the same composition that keeps monorepo CI fast, applied to everyday workspace scripts, so the cost of running a task tracks the size of the change rather than the size of the workspace no matter how many packages it grows to.

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.

Does pnpm -r run scripts in dependency order?

Yes — for tasks with inter-package dependencies, pnpm runs a package's script after the scripts of the packages it depends on, deriving the order from the workspace graph, and parallelizes independent packages up to a concurrency limit. It also skips packages that don't define the script.

How do I run a script only in changed packages with pnpm?

Combine the recursive run with a change-aware filter: pnpm --filter '...[origin/main]' <script> runs it in packages changed since the base plus their dependents. Fetch full history so the base resolves and keep the dependency graph accurate so the traversal is complete.

What does --workspace-concurrency control?

How many packages' scripts pnpm runs in parallel during a recursive run. Lowering it reduces resource contention (useful for memory-heavy builds); raising it increases parallelism where the graph and the machine allow. The topological order is still respected regardless.

How do I make a recursive pnpm run fast at scale?

Layer a task runner's cache on top of the filter. pnpm --filter '...[base]' (or turbo run --filter) scopes to the affected packages, and the cache replays those whose inputs are unchanged. Together the run's cost tracks the change rather than the workspace size.

Related

Root-Level vs Package-Level Scripts