// Root package.json - ONLY delegates, no task logic{ "scripts": { "build": "turbo run build", "lint": "turbo run lint", "test": "turbo run test" }}
json
// DO NOT DO THIS - defeats parallelization// Root package.json{ "scripts": { "build": "cd apps/web && next build && cd ../api && tsc", "lint": "eslint apps/ packages/", "test": "vitest" }}
Root Tasks (//#taskname) are ONLY for tasks that truly cannot exist in packages, such as Vitest Projects' //#test, repo-wide release scripts, or tooling that does not invoke turbo itself.
Secondary Rule: turbo run vs turbo
Always use turbo run when the command is written into code:
# CI workflows - ALWAYS "turbo run"- run: turbo run build --affected
The shorthand turbo <tasks> is ONLY for one-off terminal commands typed directly by humans or agents. Never write turbo build into package.json, CI, or scripts.
Quick Decision Trees
"I need to configure a task"
text
Configure a task?├─ Define task dependencies → references/configuration/tasks.md├─ Lint/check-types (parallel + caching) → Use Transit Nodes pattern (see below)├─ Specify build outputs → references/configuration/tasks.md#outputs├─ Handle environment variables → references/environment/RULE.md├─ Set up dev/watch tasks → references/configuration/tasks.md#persistent├─ Package-specific config → references/configuration/RULE.md#package-configurations└─ Global settings (cacheDir, daemon) → references/configuration/global-options.md
"My cache isn't working"
text
Cache problems?├─ Tasks run but outputs not restored → Missing `outputs` key├─ Cache misses unexpectedly → references/caching/gotchas.md├─ Need to debug hash inputs → Use --summarize or --dry├─ Want to skip cache entirely → Use --force or cache: false├─ Remote cache not working → references/caching/remote-cache.md└─ Environment causing misses → references/environment/gotchas.md
"I want to run only changed packages"
text
Run only what changed?├─ Changed packages + dependents (RECOMMENDED) → turbo run build --affected├─ Custom base branch → --affected --affected-base=origin/develop├─ Manual git comparison → --filter=...[origin/main]└─ See all filter options → references/filtering/RULE.md
--affected is the primary way to run only changed packages. It automatically compares against the default branch and includes dependents.
"I want to filter packages"
text
Filter packages?├─ Only changed packages → --affected (see above)├─ By package name → --filter=web├─ By directory → --filter=./apps/*├─ Package + dependencies → --filter=web...├─ Package + dependents → --filter=...web└─ Complex combinations → references/filtering/patterns.md
"Environment variables aren't working"
text
Environment issues?├─ Vars not available at runtime → Strict mode filtering (default)├─ Cache hits with wrong env → Var not in `env` key├─ .env changes not causing rebuilds → .env not in `inputs`├─ CI variables missing → references/environment/gotchas.md└─ Framework vars (NEXT_PUBLIC_*) → Auto-included via inference
"I need to set up CI"
text
CI setup?├─ GitHub Actions → references/ci/github-actions.md├─ Vercel deployment → references/ci/vercel.md├─ Remote cache in CI → references/caching/remote-cache.md├─ Only build changed packages → --affected flag├─ Skip unnecessary builds → turbo-ignore (references/cli/commands.md)└─ Skip container setup when no changes → turbo-ignore
"I want to watch for changes during development"
text
Watch mode?├─ Re-run tasks on change → turbo watch (references/watch/RULE.md)├─ Dev servers with dependencies → Use `with` key (references/configuration/tasks.md#with)├─ Restart dev server on dep change → Use `interruptible: true`└─ Persistent dev tasks → Use `persistent: true`
"I need to create/structure a package"
text
Package creation/structure?├─ Create an internal package → references/best-practices/packages.md├─ Repository structure → references/best-practices/structure.md├─ Dependency management → references/best-practices/dependencies.md├─ Best practices overview → references/best-practices/RULE.md├─ JIT vs Compiled packages → references/best-practices/packages.md#compilation-strategies└─ Sharing code between apps → references/best-practices/RULE.md#package-types
Don't chain turbo tasks with &&. Let turbo orchestrate.
json
// WRONG - turbo task not using turbo run{ "scripts": { "changeset:publish": "bun build && changeset publish" }}// CORRECT{ "scripts": { "changeset:publish": "turbo run build && changeset publish" }}
prebuild Scripts That Manually Build Dependencies
Scripts like prebuild that manually build other packages bypass Turborepo's dependency graph.
json
// WRONG - manually building dependencies{ "scripts": { "prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build", "build": "next build" }}
However, the fix depends on whether workspace dependencies are declared:
If dependencies ARE declared (e.g., "@repo/types": "workspace:*" in package.json), remove the prebuild script. Turbo's dependsOn: ["^build"] handles this automatically.
If dependencies are NOT declared, the prebuild exists because ^build won't trigger without a dependency relationship. The fix is to:
Add the dependency to package.json: "@repo/types": "workspace:*"
Key insight:^build only runs build in packages listed as dependencies. No dependency declaration = no automatic build ordering.
Overly Broad globalDependencies
globalDependencies affects ALL tasks in ALL packages via the global hash — tasks cannot opt out of specific files, even with negation globs in inputs. Be specific.
json
// WRONG - heavy hammer, affects all hashes{ "globalDependencies": ["**/.env.*local"]}// BETTER - move to task-level inputs{ "globalDependencies": [".env"], "tasks": { "build": { "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] } }}
With futureFlags.globalConfiguration, this problem is reduced because global.inputs files are folded into each task's inputs (not the global hash). Tasks can exclude specific files:
globalEnv / globalDependencies - affects ALL tasks, use for truly shared config
Task-level env / inputs - use when only specific tasks need it
NOT an Anti-Pattern: Large env Arrays
A large env array (even 50+ variables) is not a problem. It usually means the user was thorough about declaring their build's environment dependencies. Do not flag this as an issue.
Using --parallel Flag
The --parallel flag bypasses Turborepo's dependency graph. If tasks need parallel execution, configure dependsOn correctly instead.
bash
# WRONG - bypasses dependency graphturbo run lint --parallel# CORRECT - configure tasks to allow parallel execution# In turbo.json, set dependsOn appropriately (or use transit nodes)turbo run lint
Package-Specific Task Overrides in Root turbo.json
When multiple packages need different task configurations, use Package Configurations (turbo.json in each package) instead of cluttering root turbo.json with package#task overrides.
TypeScript --noEmit can still produce cache files:
When incremental: true in tsconfig.json, tsc --noEmit writes .tsbuildinfo files even without emitting JS. Check the tsconfig before assuming no outputs:
json
// If tsconfig has incremental: true, tsc --noEmit produces cache files{ "tasks": { "typecheck": { "outputs": ["node_modules/.cache/tsbuildinfo.json"] // or wherever tsBuildInfoFile points } }}
To determine correct outputs for TypeScript tasks:
Check if incremental or composite is enabled in tsconfig
Check tsBuildInfoFile for custom cache location (default: alongside outDir or in project root)
If no incremental mode, tsc --noEmit produces no files
^build vs build Confusion
json
{ "tasks": { // ^build = run build in DEPENDENCIES first (other packages this one imports) "build": { "dependsOn": ["^build"] }, // build (no ^) = run build in SAME PACKAGE first "test": { "dependsOn": ["build"] }, // pkg#task = specific package's task "deploy": { "dependsOn": ["web#build"] } }}
A .env file at the repo root is an anti-pattern — even for small monorepos or starter templates. It creates implicit coupling between packages and makes it unclear which packages depend on which variables.
text
// WRONG - root .env affects all packages implicitlymy-monorepo/├── .env # Which packages use this?├── apps/│ ├── web/│ └── api/└── packages/// CORRECT - .env files in packages that need themmy-monorepo/├── apps/│ ├── web/│ │ └── .env # Clear: web needs DATABASE_URL│ └── api/│ └── .env # Clear: api needs API_KEY└── packages/
Problems with root .env:
Unclear which packages consume which variables
All packages get all variables (even ones they don't need)
Cache invalidation is coarse-grained (root .env change invalidates everything)
Security risk: packages may accidentally access sensitive vars meant for others
Bad habits start small — starter templates should model correct patterns
If you must share variables, use globalEnv to be explicit about what's shared, and document why.
Strict Mode Filtering CI Variables
By default, Turborepo filters environment variables to only those in env/globalEnv. CI variables may be missing:
json
// If CI scripts need GITHUB_TOKEN but it's not in env:{ "globalPassThroughEnv": ["GITHUB_TOKEN", "CI"], "tasks": { ... }}
Or use --env-mode=loose (not recommended for production).
Shared Code in Apps (Should Be a Package)
text
// WRONG: Shared code inside an appapps/ web/ shared/ # This breaks monorepo principles! utils.ts// CORRECT: Extract to a packagepackages/ utils/ src/utils.ts
Accessing Files Across Package Boundaries
typescript
// WRONG: Reaching into another package's internalsimport { Button } from "../../packages/ui/src/button";// CORRECT: Install and import properlyimport { Button } from "@repo/ui/button";
Too Many Root Dependencies
json
// WRONG: App dependencies in root{ "dependencies": { "react": "^18", "next": "^14" }}// CORRECT: Only repo tools in root{ "devDependencies": { "turbo": "latest" }}
With futureFlags.globalConfiguration, the same config moves global settings under global — and .env becomes a per-task input instead of a global hash input: