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>(aliaspnpm --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 rootpackage.jsononly. A root script may itself callpnpm -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
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:--parallelwas used forbuild, discarding order. Fix: drop--parallel; let topological order apply. No projects matched the filters. The--filterpattern matched nothing — usually a typo in the package name or a[ref]selector with no changed packages. Verify withpnpm -r listfirst.- Run aborts midway with a non-zero exit. By default pnpm fails fast on the first errored package. Add
--no-bailto run the rest and collect all failures, useful fortest/lintsweeps. - A required script is missing in one package. A bare
pnpm --filter pkg run builderrors ifpkghas nobuild. Add--if-presentto 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
--parallelstrictly for long-running, order-independent tasks likedev. - Add
--if-presentto any workspace-widetest/lint/typecheckso optional packages never break the run. - Pin pnpm with the
packageManagerfield so local and CI runs compute the same graph and ordering. - Use
--workspace-concurrencymatched 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-bailin 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 — the orchestration-versus-execution boundary these commands sit on.
- Using pnpm --filter for Targeted Builds — selecting exactly which packages a run touches.
- Lockfile Management Strategies — keeping the workspace graph stable so run order stays deterministic.
- Workspace Configuration Deep Dive — defining the packages pnpm recurses over in the first place.