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.
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"
}
}
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.
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.
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.
Related
- Root-Level vs Package-Level Scripts — how script scope and delegation work across a workspace.