Fixing 'npm run: script not found' in a Workspace
Running a script from the repo root fails with 'Missing script' even though the script exists in a package. This page explains why the root has no such script and how to route the command to the right package.
Exact symptoms and error messages
$ npm run build
npm error Missing script: "build"
npm error To see a list of scripts, run: npm run
Root cause analysis
npm run looks in the current package's scripts, and the repo root manifest often has no build — the script lives in each workspace package. Without a workspace-aware runner or a --workspace flag, npm never descends into the packages. This is the scope boundary described in Root-Level vs Package-Level Scripts and governed by the workspace configuration.
npm run is scoped to the current package's scripts, and at the repo root that manifest often defines only orchestration, not a build. The script you want lives in each workspace package, and without a --workspace flag or a workspace-aware runner, npm never descends into them — so it correctly reports the script missing from the manifest it actually looked at. The mismatch is between where you ran the command and where the script is defined.
This trips people migrating from a single-package repo, where npm run build at the root always worked because there was only one manifest. In a workspace, the root and the packages are distinct scopes, and commands have to say which scope they mean. Understanding that boundary — root scripts orchestrate, package scripts do the work — is the core of Root-Level vs Package-Level Scripts.
npm run looks only in the current package's scripts, and at the repository root that manifest often defines orchestration, not the build itself — the build script lives in each workspace package. Without a --workspace flag or a workspace-aware runner, npm never descends into the packages, so it correctly reports the script missing from the manifest it actually looked at. The mismatch is between where you ran the command and where the script is defined, not a genuine absence.
This most often trips teams migrating from a single-package repository, where npm run build at the root always worked because there was only one manifest. In a workspace the root and the packages are distinct script scopes, and a command must say which scope it means. Understanding that scope is explicit — root scripts orchestrate, package scripts implement — is what resolves the confusion, and it is the foundation of how scripts are organized across a workspace.
Resolution and configuration patch
Target the packages explicitly, or add a root delegating script:
# npm: run build in every workspace that defines it
npm run build --workspaces --if-present
# pnpm: run recursively
pnpm -r run build
// root package.json — a delegating script
{
"scripts": {
"build": "pnpm -r run build"
}
}
Target the packages explicitly, or add a delegating root script:
# Run build in every workspace that defines it
npm run build --workspaces --if-present
# pnpm equivalent
pnpm -r run build
// root package.json — one obvious entry point per task
{
"scripts": { "build": "pnpm -r run build", "test": "pnpm -r run test" }
}
The --if-present flag (npm) and pnpm's default recursive behavior skip packages that lack the script rather than erroring, so a heterogeneous workspace runs cleanly. A thin delegating root script gives contributors npm run build at the root while each package keeps its own implementation.
CLI validation and debug commands
# List scripts the current package actually defines
npm run
# Confirm which packages define build
pnpm -r exec node -e "console.log(require('./package.json').scripts?.build || 'none')"
Prevention and CI guardrails
- Add root-level delegating scripts for common tasks so
npm run buildworks from the root. - Use
--if-presentso packages without the script are skipped, not errored. - Prefer a task runner (
turbo run build) that resolves the graph automatically. - Document which scripts are root-level orchestration vs package-level.
- Add root delegating scripts for common tasks so the root command has an obvious meaning.
- Use
--if-presentso packages without the script are skipped, not errored. - Prefer a task runner (
turbo run build) that resolves the graph and adds caching automatically. - Document which scripts are root-level orchestration and which are package-level implementation.
Root delegating scripts versus per-package scripts
The cleanest resolution is to give the root a thin delegating script for each common task, so npm run build at the root does the obvious thing while the real work stays in the packages. The root script fans the command out; each package's script builds itself.
// root package.json
{
"scripts": {
"build": "pnpm -r run build",
"test": "pnpm -r run test",
"lint": "turbo run lint"
}
}
This keeps the two scopes distinct but connected: contributors get one root entry point per task, while each package remains responsible for its own implementation. The alternative — targeting a package explicitly with --workspace=pkg or --filter pkg — is right for one-off, package-specific commands, but for everyday tasks the delegating root script is what makes the repo approachable.
Using --if-present so a partial script set doesn't error
In a real workspace, not every package defines every script — a docs package has no build, a types-only package has no test. A fan-out command that errors on the first missing script is brittle, so tell the runner to skip packages that lack it.
# Skip packages without the script instead of failing
npm run build --workspaces --if-present
pnpm -r run test # pnpm skips missing scripts by default
--if-present (npm) and pnpm's default recursive behavior treat a missing script as 'nothing to do here' rather than an error, so the command completes across the packages that do define it. This is what lets a heterogeneous workspace — where packages legitimately differ in which scripts they have — run a single root task without every package needing a no-op stub for tasks it does not perform.
Root orchestration versus package implementation
The clean way to organize workspace scripts is a strict separation: root scripts orchestrate by fanning a task across packages, and package scripts implement by doing the actual work for one package. Keeping the two layers distinct means a root command like npm run build has one obvious meaning — build everything, in order — while each package remains responsible for how it builds itself. Contributors get a single entry point per task and never need to remember which packages define which scripts or in what sequence they must run.
The pattern that scales is to keep the root scripts thin and let a task runner handle ordering and caching. A root build that simply invokes the runner's build across the workspace gives contributors one command, while the runner derives the topological order so a package builds after its dependencies, parallelizes independent packages, and replays cached results. This is the separation of orchestration from implementation that keeps a growing monorepo approachable: the root answers 'what do I run to build the project', and each package answers 'how do I build myself', without either layer needing to know the other's details.
Running a script in one package or the changed set
Beyond running a task everywhere, a workspace often needs to run a script in exactly one package or only in the packages a change touched. Targeting one package is explicit — npm run build --workspace @acme/ui or pnpm --filter @acme/ui run build runs it in that package with the package's directory as the working directory. Running only the changed set uses a change-aware filter: pnpm --filter '...[origin/main]' run test runs the task in packages changed since the base plus their dependents, which is the affected set.
Choosing the right scope for each situation keeps commands both fast and correct. A one-off, package-specific task uses the single-package target; a CI run scopes to the affected set so its cost tracks the change rather than the repository; a whole-workspace task like a format check runs across everything. The precondition for the change-aware form is full git history so the base ref resolves and an accurate dependency graph so the dependent traversal is complete. Matching the command's scope to what the task actually needs — one package, the affected set, or everything — is what turns a workspace's scripts from a source of confusion into a precise, predictable interface.
Passing arguments to a workspace script
Once a script runs in the right package, the next common friction is passing arguments to it, which uses the double-dash convention to forward everything after it to the underlying command. npm run test --workspace @acme/ui -- --watch runs that package's test script with --watch appended; pnpm --filter @acme/ui run build -- --sourcemap does the same for pnpm. The double dash separates the arguments meant for npm/pnpm from the ones meant for the script.
When fanning a task across packages, forwarded arguments apply to every invocation, which is what you want for a flag like --coverage but not for a positional argument that only makes sense for one package. This is a reason to target a single package explicitly when the argument is package-specific, and to reserve the fan-out for flags that are meaningful everywhere. Understanding how arguments flow — after the double dash, to each invoked script — turns a workspace's scripts into a flexible interface where you can run one package's task with custom flags or the whole set with a shared flag, without a bespoke script per scenario.
Frequently Asked Questions
Why does --if-present matter?
In a workspace, not every package defines every script. --if-present skips packages that lack the script instead of failing the whole command, so a partial script set does not break the run.
Should build scripts live at the root or in packages?
Keep the real build in each package and a thin delegating script (or task-runner target) at the root. That preserves per-package scope while giving one root entry point.
Why does npm run build fail at the repo root?
Because npm run looks in the current package's scripts, and the root manifest often has no build — the script lives in each workspace package. Add a delegating root script or target the packages with --workspaces/--filter.
How do I run a task across packages that don't all define it?
Use --if-present with npm (npm run build --workspaces --if-present) or pnpm's recursive run, which skips missing scripts by default. Packages without the script are skipped rather than erroring the whole command.
Should the real build live at the root or in each package?
Keep the real build in each package and put a thin delegating script (or task-runner target) at the root. That preserves per-package scope while giving contributors one root entry point per task.
Why does npm run build fail at the repository root?
Because npm run looks only at the current package's scripts, and the root manifest usually has no build — it lives in each package. Add a delegating root script (pnpm -r run build) or target packages with --workspaces/--filter.
How do I run a task only where the script exists?
Use --if-present with npm (npm run build --workspaces --if-present) or pnpm's recursive run, which skips missing scripts by default. Packages without the script are skipped rather than failing the whole command.
How do I run a script in just one workspace package?
Target it explicitly: npm run build --workspace <name> or pnpm --filter <name> run build, which runs the script in that package with its directory as the working directory. Use a change-aware filter (...[origin/main]) to run only the affected set in CI.
How do I pass a flag to a script running in a workspace package?
Use the double-dash convention: npm run test --workspace <name> -- --watch forwards --watch to the package's test script. When fanning across packages, forwarded flags apply to every invocation, so target one package explicitly for package-specific arguments.
Why does pnpm -r run build skip some packages silently?
pnpm's recursive run skips packages that do not define the script, by design, so a heterogeneous workspace runs cleanly without a no-op stub in every package. If you expected a package to run, confirm it actually defines the script — the skip means it does not.
Related
- Root-Level vs Package-Level Scripts — how script scope and delegation work across a workspace.