to inherit + extend arrays\n\n**When to use `package#task` in root:**\n\n- Single package needs a unique dependency (e.g., `\"deploy\": { \"dependsOn\": [\"web#build\"] }`)\n- Temporary override while migrating\n\nSee `references/configuration/RULE.md#package-configurations` for full details.\n\n### Using `../` to Traverse Out of Package in `inputs`\n\nDon't use relative paths like `../` to reference files outside the package. Use `$TURBO_ROOT
Build system for JavaScript/TypeScript monorepos. Turborepo caches task outputs and runs tasks in parallel based on dependency graph. **DO NOT create Root Tasks. ALWAYS create package tasks.** When creating tasks/scripts/pipelines, you MUST:
package.jsonturbo.jsonpackage.json only delegates via turbo run <task>package.json. This defeats Turborepo's parallelization.// DO THIS: Scripts in each package // apps/web/package.json { "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } } // apps/api/package.json { "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } // packages/ui/package.json { "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }
// turbo.json - register tasks { "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "lint": {}, "test": { "dependsOn": ["build"] } } }
// Root package.json - ONLY delegates, no task logic { "scripts": { "build": "turbo run build", "lint": "turbo run lint", "test": "turbo run test" } }
// 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" } }
//#taskname) are ONLY for tasks that truly cannot exist in packages (rare).turbo run vs turboturbo run when the command is written into code:// package.json - ALWAYS "turbo run" { "scripts": { "build": "turbo run build" } }
# CI workflows - ALWAYS "turbo run" - run: turbo run build --affected
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.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"` 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"` 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.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"` 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"` 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"` 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"` 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 `### "How should I structure my monorepo?"` Monorepo structure? ├─ Standard layout (apps/, packages/) → references/best-practices/RULE.md ├─ Package types (apps vs libraries) → references/best-practices/RULE.md#package-types ├─ Creating internal packages → references/best-practices/packages.md ├─ TypeScript configuration → references/best-practices/structure.md#typescript-configuration ├─ ESLint configuration → references/best-practices/structure.md#eslint-configuration ├─ Dependency management → references/best-practices/dependencies.md └─ Enforce package boundaries → references/boundaries/RULE.md `### "I want to enforce architectural boundaries"` Enforce boundaries? ├─ Check for violations → turbo boundaries ├─ Tag packages → references/boundaries/RULE.md#tags ├─ Restrict which packages can import others → references/boundaries/RULE.md#rule-types └─ Prevent cross-package file imports → references/boundaries/RULE.md
turbo Shorthand in Codeturbo run is recommended in package.json scripts and CI pipelines. The shorthand turbo <task> is intended for interactive terminal use.// WRONG - using shorthand in package.json { "scripts": { "build": "turbo build", "dev": "turbo dev" } } // CORRECT { "scripts": { "build": "turbo run build", "dev": "turbo run dev" } }
# WRONG - using shorthand in CI - run: turbo build --affected # CORRECT - run: turbo run build --affected
package.json scripts MUST delegate to turbo run, not run tasks directly.// WRONG - bypasses turbo entirely { "scripts": { "build": "bun build", "dev": "bun dev" } } // CORRECT - delegates to turbo { "scripts": { "build": "turbo run build", "dev": "turbo run dev" } }
&& to Chain Turbo Tasks&&. Let turbo orchestrate.// 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 Dependenciesprebuild that manually build other packages bypass Turborepo's dependency graph.// WRONG - manually building dependencies { "scripts": { "prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build", "build": "next build" } }
"@repo/types": "workspace:*" in package.json), remove the prebuild script. Turbo's dependsOn: ["^build"] handles this automatically.prebuild exists because ^build won't trigger without a dependency relationship. The fix is to:"@repo/types": "workspace:*"prebuild script// CORRECT - declare dependency, let turbo handle build order // package.json { "dependencies": { "@repo/types": "workspace:*", "@repo/utils": "workspace:*" }, "scripts": { "build": "next build" } } // turbo.json { "tasks": { "build": { "dependsOn": ["^build"] } } }
^build only runs build in packages listed as dependencies. No dependency declaration = no automatic build ordering.globalDependenciesglobalDependencies affects ALL tasks in ALL packages. Be specific.// WRONG - heavy hammer, affects all hashes { "globalDependencies": ["**/.env.*local"] } // BETTER - move to task-level inputs { "globalDependencies": [".env"], "tasks": { "build": { "inputs": ["$TURBO_DEFAULTquot;, ".env*"], "outputs": ["dist/**"] } } }
// WRONG - repetitive env and inputs across tasks { "tasks": { "build": { "env": ["API_URL", "DATABASE_URL"], "inputs": ["$TURBO_DEFAULTquot;, ".env*"] }, "test": { "env": ["API_URL", "DATABASE_URL"], "inputs": ["$TURBO_DEFAULTquot;, ".env*"] }, "dev": { "env": ["API_URL", "DATABASE_URL"], "inputs": ["$TURBO_DEFAULTquot;, ".env*"], "cache": false, "persistent": true } } } // BETTER - use globalEnv and globalDependencies for shared config { "globalEnv": ["API_URL", "DATABASE_URL"], "globalDependencies": [".env*"], "tasks": { "build": {}, "test": {}, "dev": { "cache": false, "persistent": true } } }
globalEnv / globalDependencies - affects ALL tasks, use for truly shared configenv / inputs - use when only specific tasks need itenv Arraysenv 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.--parallel Flag--parallel flag bypasses Turborepo's dependency graph. If tasks need parallel execution, configure dependsOn correctly instead.# WRONG - bypasses dependency graph turbo run lint --parallel # CORRECT - configure tasks to allow parallel execution # In turbo.json, set dependsOn appropriately (or use transit nodes) turbo run lint
turbo.json in each package) instead of cluttering root turbo.json with package#task overrides.// WRONG - root turbo.json with many package-specific overrides { "tasks": { "test": { "dependsOn": ["build"] }, "@repo/web#test": { "outputs": ["coverage/**"] }, "@repo/api#test": { "outputs": ["coverage/**"] }, "@repo/utils#test": { "outputs": [] }, "@repo/cli#test": { "outputs": [] }, "@repo/core#test": { "outputs": [] } } } // CORRECT - use Package Configurations // Root turbo.json - base config only { "tasks": { "test": { "dependsOn": ["build"] } } } // packages/web/turbo.json - package-specific override { "extends": ["//"], "tasks": { "test": { "outputs": ["coverage/**"] } } } // packages/api/turbo.json { "extends": ["//"], "tasks": { "test": { "outputs": ["coverage/**"] } } }
$TURBO_EXTENDS$ to inherit + extend arrayspackage#task in root:"deploy": { "dependsOn": ["web#build"] })references/configuration/RULE.md#package-configurations for full details.../ to Traverse Out of Package in inputs../ to reference files outside the package. Use $TURBO_ROOT$ instead.// WRONG - traversing out of package { "tasks": { "build": { "inputs": ["$TURBO_DEFAULTquot;, "../shared-config.json"] } } } // CORRECT - use $TURBO_ROOT$ for repo root { "tasks": { "build": { "inputs": ["$TURBO_DEFAULTquot;, "$TURBO_ROOT$/shared-config.json"] } } }
outputs for File-Producing Tasksoutputs, check what the task actually produces:"build": "tsc", "test": "vitest")// WRONG: build produces files but they're not cached { "tasks": { "build": { "dependsOn": ["^build"] } } } // CORRECT: build outputs are cached { "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] } } }
[".next/**", "!.next/cache/**"]["dist/**"]["dist/**"] or custom outDir--noEmit can still produce cache files:incremental: true in tsconfig.json, tsc --noEmit writes .tsbuildinfo files even without emitting JS. Check the tsconfig before assuming no outputs:// If tsconfig has incremental: true, tsc --noEmit produces cache files { "tasks": { "typecheck": { "outputs": ["node_modules/.cache/tsbuildinfo.json"] // or wherever tsBuildInfoFile points } } }
incremental or composite is enabled in tsconfigtsBuildInfoFile for custom cache location (default: alongside outDir or in project root)tsc --noEmit produces no files^build vs build Confusion{ "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"] } } } `### Environment Variables Not Hashed` // WRONG: API_URL changes won't cause rebuilds { "tasks": { "build": { "outputs": ["dist/**"] } } } // CORRECT: API_URL changes invalidate cache { "tasks": { "build": { "outputs": ["dist/**"], "env": ["API_URL", "API_KEY"] } } }
.env Files Not in Inputs.env files - your framework does. But Turbo needs to know about changes:// WRONG: .env changes don't invalidate cache { "tasks": { "build": { "env": ["API_URL"] } } } // CORRECT: .env file changes invalidate cache { "tasks": { "build": { "env": ["API_URL"], "inputs": ["$TURBO_DEFAULTquot;, ".env", ".env.*"] } } }
.env File in Monorepo.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.// WRONG - root .env affects all packages implicitly my-monorepo/ ├── .env # Which packages use this? ├── apps/ │ ├── web/ │ └── api/ └── packages/ // CORRECT - .env files in packages that need them my-monorepo/ ├── apps/ │ ├── web/ │ │ └── .env # Clear: web needs DATABASE_URL │ └── api/ │ └── .env # Clear: api needs API_KEY └── packages/
.env:globalEnv to be explicit about what's shared, and document why.env/globalEnv. CI variables may be missing:// If CI scripts need GITHUB_TOKEN but it's not in env: { "globalPassThroughEnv": ["GITHUB_TOKEN", "CI"], "tasks": { ... } }
--env-mode=loose (not recommended for production).// WRONG: Shared code inside an app apps/ web/ shared/ # This breaks monorepo principles! utils.ts // CORRECT: Extract to a package packages/ utils/ src/utils.ts `### Accessing Files Across Package Boundaries` // WRONG: Reaching into another package's internals import { Button } from "../../packages/ui/src/button"; // CORRECT: Install and import properly import { Button } from "@repo/ui/button"; `### Too Many Root Dependencies` // WRONG: App dependencies in root { "dependencies": { "react": "^18", "next": "^14" } } // CORRECT: Only repo tools in root { "devDependencies": { "turbo": "latest" } }
{ "$schema": "https://turborepo.dev/schema.v2.json", "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }, "dev": { "cache": false, "persistent": true } } }
transit task if you have tasks that need parallel execution with cache invalidation (see below).^dev Pattern (for turbo watch)dev task with dependsOn: ["^dev"] and persistent: false in root turbo.json may look unusual but is correct for turbo watch workflows:// Root turbo.json { "tasks": { "dev": { "dependsOn": ["^dev"], "cache": false, "persistent": false // Packages have one-shot dev scripts } } } // Package turbo.json (apps/web/turbo.json) { "extends": ["//"], "tasks": { "dev": { "persistent": true // Apps run long-running dev servers } } }
@acme/db, @acme/validators) have "dev": "tsc" — one-shot type generation that completes quicklypersistent: true for actual dev servers (Next.js, etc.)turbo watch re-runs the one-shot package dev scripts when source files change, keeping types in syncturbo watch dev (not turbo run dev). Watch mode re-executes one-shot tasks on file changes while keeping persistent tasks running.prepare or generate for one-shot dependency builds to make the intent clearer:{ "tasks": { "prepare": { "dependsOn": ["^prepare"], "outputs": ["dist/**"] }, "dev": { "dependsOn": ["prepare"], "cache": false, "persistent": true } } }
dependsOn: ["^taskname"]:dependsOn: [] (no dependencies):{ "tasks": { "transit": { "dependsOn": ["^transit"] }, "my-task": { "dependsOn": ["transit"] } } }
transit task creates dependency relationships without matching any actual script, so tasks run in parallel with correct cache invalidation.{ "globalEnv": ["NODE_ENV"], "globalDependencies": [".env"], "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"], "env": ["API_URL", "DATABASE_URL"] } } }
apps/docs/content/docs/ in the Turborepo repository