pnpm --filter: run a script in one workspace package
- tool
- pnpm
- versions
- pnpm 8 – 10
- os
- any
pnpm --filter <selector> <command> runs a command only in the workspace packages that match the selector: pnpm --filter @acme/api run build builds one package, and pnpm --filter "@acme/web..." run build builds @acme/web and every workspace package it depends on, in dependency order. Without a filter, pnpm -r run build runs the script in all packages.
Example workspace
cat > package.json <<'EOF'
{ "name": "monorepo", "private": true }
EOF
cat > pnpm-workspace.yaml <<'EOF'
packages:
- "packages/*"
EOF
mkdir -p packages/api packages/web
cat > packages/api/package.json <<'EOF'
{ "name": "@acme/api", "version": "1.0.0", "scripts": { "build": "echo build api" } }
EOF
cat > packages/web/package.json <<'EOF'
{ "name": "@acme/web", "version": "1.0.0", "scripts": { "build": "echo build web" },
"dependencies": { "@acme/api": "workspace:*" } }
EOF
npx --yes pnpm@9 install > /dev/null 2>&1
echo "--- --filter @acme/api"
npx --yes pnpm@9 --filter @acme/api run build | grep -v '^>'
echo "--- --filter @acme/web..."
npx --yes pnpm@9 --filter "@acme/web..." run buildOutput
--- --filter @acme/api build api --- --filter @acme/web... Scope: 2 of 3 workspace projects packages/api build$ echo build api packages/api build: build api packages/api build: Done packages/web build$ echo build web packages/web build: build web packages/web build: Done
With one matching package pnpm prints the script output directly; with several it prefixes each line with the package path and runs them in topological order (dependencies first).
Selector syntax
| Selector | Matches |
|---|---|
| @acme/api | The package with that name |
| "@acme/*" | Every package whose name matches the glob |
| "@acme/web..." | The package and all its dependencies |
| "...@acme/api" | The package and all packages that depend on it |
| "@acme/web^..." | Only the dependencies, not the package itself |
| ./packages/api | The package in that directory (a path starts with . or /) |
| "{packages/**}" | Packages under a directory glob |
| "[origin/main]" | Packages changed since that git ref |
| "!@acme/docs" | Exclude a package (combine with other filters) |
When nothing matches
cat > package.json <<'EOF'
{ "name": "monorepo", "private": true }
EOF
cat > pnpm-workspace.yaml <<'EOF'
packages:
- "packages/*"
EOF
mkdir -p packages/api
cat > packages/api/package.json <<'EOF'
{ "name": "@acme/api", "version": "1.0.0", "scripts": { "build": "echo build api" } }
EOF
npx --yes pnpm@9 install > /dev/null 2>&1
npx --yes pnpm@9 --filter @acme/apii run build 2>&1 | sed 's/ in ".*"$//'
echo "exit=${PIPESTATUS[0]}"
npx --yes pnpm@9 --filter @acme/apii --fail-if-no-match run build 2>&1 | sed 's/ in ".*"$//'
echo "exit=${PIPESTATUS[0]}"Output
No projects matched the filters exit=0 No projects matched the filters exit=1
A misspelled selector is not an error by default, so a CI step can pass without building anything. Add --fail-if-no-match to CI commands, or set failIfNoMatch in the workspace settings.
Quote selectors that contain *, ..., ^, ! or brackets so the shell does not expand them. Repeat --filter to combine selectors: the result is the union.
Other commands
--filter works with most commands, not only run:
pnpm --filter @acme/web add zod # add a dependency to one package
pnpm --filter "./packages/**" test # run tests in every package under packages/
pnpm --filter "...[origin/main]" build # build changed packages and their dependentsOptions used with filters
| Option | Effect |
|---|---|
| -r, --recursive | Run in every workspace package (no filter needed) |
| --filter-prod SELECTOR | Like --filter, but ignores devDependencies when following the ... dependency selectors |
| --fail-if-no-match | Exit with an error when no package matches |
| --parallel | Run the script in all matched packages at once, ignoring dependency order (for watch scripts) |
| --workspace-concurrency=N | How many packages run at the same time (default 4) |
| --changed-files-ignore-pattern GLOB | With a [ref] selector, ignore changes in matching files (docs, READMEs) |
| --test-pattern GLOB | With a [ref] selector, treat matching files as tests so only the changed package is selected, not its dependents |
Typical CI use
Building only what a pull request touched keeps monorepo pipelines short: pnpm --filter "...[origin/main]" build selects every package changed since origin/main plus everything that depends on them, so a change in a shared library rebuilds its consumers too. The git ref must exist in the CI checkout, so fetch it first (a shallow clone often has no origin/main).
FAQ
Why does pnpm --filter run nothing and still succeed?
No package matched the selector, often a typo in the package name or a path without a leading ./. pnpm prints No projects matched the filters and exits 0; add --fail-if-no-match to make it fail.
Should I use the package name or the folder in --filter?
Either works. The name (from package.json) is stable when folders move; a path must start with . or / so pnpm treats it as a directory, for example --filter ./packages/api.
How do I run a script in the workspace root instead of a package?
Use -w (--workspace-root), for example pnpm -w run lint. --filter selects workspace packages, not the root.