
Turborepo
- 54.6k installs
- 30.8k repo stars
- Updated July 28, 2026
- vercel/turborepo
Turborepo is a CLI-based build system for JavaScript/TypeScript monorepos that caches outputs and runs tasks in parallel.
About
This skill provides guidance for Turborepo, a build system for JavaScript/TypeScript monorepos that caches task outputs and runs tasks in parallel. It covers task configuration, dependency management, filtering packages, environment variables, watch mode, and CI optimization. The skill emphasizes using package tasks over root tasks to leverage parallelization.
- Cache task outputs and run tasks in parallel based on dependency graphs
- Filter packages with --filter, --affected to optimize CI builds
- Configure package tasks via turbo.json for monorepo orchestration
Turborepo by the numbers
- 54,588 all-time installs (skills.sh)
- +2,842 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #8 of 1,453 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
What turborepo says it does
Turborepo caches task outputs and runs tasks in parallel based on dependency graph
npx skills add https://github.com/vercel/turborepo --skill turborepoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 54.6k |
|---|---|
| repo stars | ★ 30.8k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | vercel/turborepo ↗ |
How do you configure Turborepo task pipelines?
Manage JavaScript/TypeScript monorepos with parallel task execution, caching, and dependency-aware build orchestration.
Who is it for?
Developers maintaining JavaScript/TypeScript monorepos who need faster parallel builds and reliable remote caching.
Skip if: Single-package repos, Rust/Cargo workspaces, or teams with no apps/packages monorepo layout.
When should I use this skill?
User mentions turbo.json, turbo CLI, dependsOn, remote cache, --filter, --affected, or monorepo apps/packages directories.
What you get
turbo.json pipeline config, cache rules, filter commands, and monorepo package boundary setup
- turbo.json configuration
- Remote cache setup
- CI filter/affected commands
By the numbers
- Skill metadata version 2.9.17-canary.1
Files
Turborepo Skill
Build system for JavaScript/TypeScript monorepos. Turborepo caches task outputs and runs tasks in parallel based on dependency graph.
IMPORTANT: Package Tasks, Not Root Tasks
Prefer package tasks over Root Tasks.
When creating tasks/scripts/pipelines, you MUST default to package tasks:
1. Add the script to each relevant package's package.json 2. Register the task in root turbo.json 3. Root package.json only delegates via turbo run <task>
DO NOT put task logic in root package.json when it can live in packages. 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"
}
}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:
// package.json - ALWAYS "turbo run"
{
"scripts": {
"build": "turbo run build"
}
}# CI workflows - ALWAYS "turbo run"
- run: turbo run build --affectedThe 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"
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.
"I want to filter packages"
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.mdCritical Anti-Patterns
Using turbo Shorthand in Code
`turbo 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 --affectedRoot Scripts Bypassing Turbo
Root 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"
}
}Using && to Chain Turbo Tasks
Don't chain turbo tasks with &&. 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 Dependencies
Scripts like prebuild 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"
}
}However, the fix depends on whether workspace dependencies are declared:
1. If dependencies ARE declared (e.g., "@repo/types": "workspace:*" in package.json), remove the prebuild script. Turbo's dependsOn: ["^build"] handles this automatically.
2. 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:*" - Then remove the
prebuildscript
// 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"]
}
}
}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.
// 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:
// BEST - global.inputs with per-task exclusion
{
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": [".env"]
},
"tasks": {
"build": { "outputs": ["dist/**"] },
"lint": {
"inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/.env"]
}
}
}Repetitive Task Configuration
Look for repeated configuration across tasks that can be collapsed. Turborepo supports shared configuration patterns.
// WRONG - repetitive env and inputs across tasks
{
"tasks": {
"build": {
"env": ["API_URL", "DATABASE_URL"],
"inputs": ["$TURBO_DEFAULT$", ".env*"]
},
"test": {
"env": ["API_URL", "DATABASE_URL"],
"inputs": ["$TURBO_DEFAULT$", ".env*"]
},
"dev": {
"env": ["API_URL", "DATABASE_URL"],
"inputs": ["$TURBO_DEFAULT$", ".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
}
}
}When to use global vs task-level:
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.
# 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 lintPackage-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.
// 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/**"] }
}
}Benefits of Package Configurations:
- Keeps configuration close to the code it affects
- Root turbo.json stays clean and focused on base patterns
- Easier to understand what's special about each package
- Works with
$TURBO_EXTENDS$to inherit + extend arrays
When to use `package#task` in root:
- Single package needs a unique dependency (e.g.,
"deploy": { "dependsOn": ["web#build"] }) - Temporary override while migrating
See references/configuration/RULE.md#package-configurations for full details.
Using ../ to Traverse Out of Package in inputs
Don't use relative paths like ../ to reference files outside the package. Use $TURBO_ROOT$ instead.
// WRONG - traversing out of package
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", "../shared-config.json"]
}
}
}
// CORRECT - use $TURBO_ROOT$ for repo root
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"]
}
}
}Missing outputs for File-Producing Tasks
Before flagging missing `outputs`, check what the task actually produces:
1. Read the package's script (e.g., "build": "tsc", "test": "vitest") 2. Determine if it writes files to disk or only outputs to stdout 3. Only flag if the task produces files that should be cached
// WRONG: build produces files but they're not cached
{
"tasks": {
"build": {
"dependsOn": ["^build"]
}
}
}
// CORRECT: build outputs are cached
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}Common outputs by framework:
- Next.js:
[".next/**", "!.next/cache/**"] - Vite/Rollup:
["dist/**"] - tsc:
["dist/**"]or customoutDir
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:
// 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:
1. Check if incremental or composite is enabled in tsconfig 2. Check tsBuildInfoFile for custom cache location (default: alongside outDir or in project root) 3. If no incremental mode, 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
Turbo does NOT load .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_DEFAULT$", ".env", ".env.*"]
}
}
}Root .env File in Monorepo
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.
// 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/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:
// 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)
// WRONG: Shared code inside an app
apps/
web/
shared/ # This breaks monorepo principles!
utils.ts
// CORRECT: Extract to a package
packages/
utils/
src/utils.tsAccessing 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"
}
}Common Task Configurations
Standard Build Pipeline
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}Add a transit task if you have tasks that need parallel execution with cache invalidation (see below).
Dev Task with ^dev Pattern (for turbo watch)
A 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
}
}
}Why this works:
- Packages (e.g.,
@acme/db,@acme/validators) have"dev": "tsc"— one-shot type generation that completes quickly - Apps override with
persistent: truefor actual dev servers (Next.js, etc.) - `turbo watch` re-runs the one-shot package
devscripts when source files change, keeping types in sync
Intended usage: Run turbo watch dev (not turbo run dev). Watch mode re-executes one-shot tasks on file changes while keeping persistent tasks running.
Alternative pattern: Use a separate task name like 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
}
}
}Transit Nodes for Parallel Tasks with Cache Invalidation
Some tasks can run in parallel (don't need built output from dependencies) but must invalidate cache when dependency source code changes.
The problem with `dependsOn: ["^taskname"]`:
- Forces sequential execution (slow)
The problem with `dependsOn: []` (no dependencies):
- Allows parallel execution (fast)
- But cache is INCORRECT - changing dependency source won't invalidate cache
Transit Nodes solve both:
{
"tasks": {
"transit": { "dependsOn": ["^transit"] },
"my-task": { "dependsOn": ["transit"] }
}
}The transit task creates dependency relationships without matching any actual script, so tasks run in parallel with correct cache invalidation.
How to identify tasks that need this pattern: Look for tasks that read source files from dependencies but don't need their build outputs.
With Environment Variables
{
"globalEnv": ["NODE_ENV"],
"globalDependencies": [".env"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["API_URL", "DATABASE_URL"]
}
}
}With futureFlags.globalConfiguration, the same config moves global settings under global — and .env becomes a per-task input instead of a global hash input:
{
"futureFlags": { "globalConfiguration": true },
"global": {
"env": ["NODE_ENV"],
"inputs": [".env"]
},
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["API_URL", "DATABASE_URL"]
}
}
}Reference Index
Configuration
| File | Purpose |
|---|---|
| configuration/RULE.md | turbo.json overview, Package Configurations |
| configuration/tasks.md | dependsOn, outputs, inputs, env, cache, persistent |
| configuration/global-options.md | globalEnv, globalDependencies, global key, futureFlags, cacheDir, envMode |
| configuration/gotchas.md | Common configuration mistakes |
Caching
| File | Purpose |
|---|---|
| caching/RULE.md | How caching works, hash inputs |
| caching/remote-cache.md | Vercel Remote Cache, self-hosted, login/link |
| caching/gotchas.md | Debugging cache misses, --summarize, --dry |
Environment Variables
| File | Purpose |
|---|---|
| environment/RULE.md | env, globalEnv, passThroughEnv |
| environment/modes.md | Strict vs Loose mode, framework inference |
| environment/gotchas.md | .env files, CI issues |
Filtering
| File | Purpose |
|---|---|
| filtering/RULE.md | --filter syntax overview |
| filtering/patterns.md | Common filter patterns |
CI/CD
| File | Purpose |
|---|---|
| ci/RULE.md | General CI principles |
| ci/github-actions.md | Complete GitHub Actions setup |
| ci/vercel.md | Vercel deployment, turbo-ignore |
| ci/patterns.md | --affected, caching strategies |
CLI
| File | Purpose |
|---|---|
| cli/RULE.md | turbo run basics |
| cli/commands.md | turbo run flags, turbo-ignore, other commands |
Best Practices
| File | Purpose |
|---|---|
| best-practices/RULE.md | Monorepo best practices overview |
| best-practices/structure.md | Repository structure, workspace config, TypeScript/ESLint setup |
| best-practices/packages.md | Creating internal packages, JIT vs Compiled, exports |
| best-practices/dependencies.md | Dependency management, installing, version sync |
Watch Mode
| File | Purpose |
|---|---|
| watch/RULE.md | turbo watch, interruptible tasks, dev workflows |
Boundaries (Experimental)
| File | Purpose |
|---|---|
| boundaries/RULE.md | Enforce package isolation, tag-based dependency rules |
Source Documentation
This skill is based on the official Turborepo documentation at:
- Source:
apps/docs/content/docs/in the Turborepo repository - Live: https://turborepo.dev/docs
Load the Turborepo skill and help with monorepo task orchestration: creating workflows, configuring tasks, setting up pipelines, and optimizing builds.
Workflow
Step 1: Load turborepo skill
skill({ name: 'turborepo' })Step 2: Identify task type from user request
Analyze $ARGUMENTS to determine:
- Topic: configuration, caching, filtering, environment, CI, or CLI
- Task type: new setup, debugging, optimization, or implementation
Use decision trees in SKILL.md to select the relevant reference files.
Step 3: Read relevant reference files
Based on task type, read from references/<topic>/:
| Task | Files to Read |
|---|---|
| Configure turbo.json | configuration/RULE.md + configuration/tasks.md |
| Debug cache issues | caching/gotchas.md |
| Set up remote cache | caching/remote-cache.md |
| Filter packages | filtering/RULE.md + filtering/patterns.md |
| Environment problems | environment/gotchas.md + environment/modes.md |
| Set up CI | ci/RULE.md + ci/github-actions.md or ci/vercel.md |
| CLI usage | cli/commands.md |
Step 4: Execute task
Apply Turborepo-specific patterns from references to complete the user's request.
CRITICAL - When creating tasks/scripts/pipelines:
1. Prefer package tasks over Root Tasks. 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. 2. Add scripts to each relevant package's package.json (e.g., apps/web/package.json, packages/ui/package.json) 3. Register the task in root turbo.json 4. Root package.json only contains turbo run <task> - never actual task logic, unless defining a valid Root Task exception
Other things to verify:
outputsdefined for cacheable tasksdependsOnuses correct syntax (^taskvstask)- Environment variables in
envkey .envfiles ininputsif used- Use
turbo run(notturbo) in package.json and CI
Step 5: Summarize
=== Turborepo Task Complete ===
Topic: <configuration|caching|filtering|environment|ci|cli>
Files referenced: <reference files consulted>
<brief summary of what was done><user-request> $ARGUMENTS </user-request>
Dependency Management
Best practices for managing dependencies in a Turborepo monorepo.
Core Principle: Install Where Used
Dependencies belong in the package that uses them, not the root.
# Good: Install in specific package
pnpm add react --filter=@repo/ui
pnpm add next --filter=web
# Avoid: Installing in root
pnpm add react -w # Only for repo-level tools!Benefits of Local Installation
1. Clarity
Each package's package.json lists exactly what it needs:
// packages/ui/package.json
{
"dependencies": {
"react": "^18.0.0",
"class-variance-authority": "^0.7.0"
}
}2. Flexibility
Different packages can use different versions when needed:
// packages/legacy-ui/package.json
{ "dependencies": { "react": "^17.0.0" } }
// packages/ui/package.json
{ "dependencies": { "react": "^18.0.0" } }3. Better Caching
Installing in root changes workspace lockfile, invalidating all caches.
4. Pruning Support
turbo prune can remove unused dependencies for Docker images.
What Belongs in Root
Only repository-level tools:
// Root package.json
{
"devDependencies": {
"turbo": "latest",
"husky": "^8.0.0",
"lint-staged": "^15.0.0"
}
}NOT application dependencies:
- react, next, express
- lodash, axios, zod
- Testing libraries (unless truly repo-wide)
Installing Dependencies
Single Package
# pnpm
pnpm add lodash --filter=@repo/utils
# npm
npm install lodash --workspace=@repo/utils
# yarn
yarn workspace @repo/utils add lodash
# bun
cd packages/utils && bun add lodashMultiple Packages
# pnpm
pnpm add jest --save-dev --filter=web --filter=@repo/ui
# npm
npm install jest --save-dev --workspace=web --workspace=@repo/ui
# yarn (v2+)
yarn workspaces foreach -R --from '{web,@repo/ui}' add jest --devInternal Packages
# pnpm
pnpm add @repo/ui --filter=web
# This updates package.json:
{
"dependencies": {
"@repo/ui": "workspace:*"
}
}Keeping Versions in Sync
Option 1: Tooling
# syncpack - Check and fix version mismatches
npx syncpack list-mismatches
npx syncpack fix-mismatches
# manypkg - Similar functionality
npx @manypkg/cli check
npx @manypkg/cli fix
# sherif - Rust-based, very fast
npx sherifOption 2: Package Manager Commands
# pnpm - Update everywhere
pnpm up --recursive typescript@latest
# npm - Update in all workspaces
npm install typescript@latest --workspacesOption 3: pnpm Catalogs (pnpm 9.5+)
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
catalog:
react: ^18.2.0
typescript: ^5.3.0// Any package.json
{
"dependencies": {
"react": "catalog:" // Uses version from catalog
}
}Internal vs External Dependencies
Internal (Workspace)
// pnpm/bun
{ "@repo/ui": "workspace:*" }
// npm/yarn
{ "@repo/ui": "*" }Turborepo understands these relationships and orders builds accordingly.
External (npm Registry)
{ "lodash": "^4.17.21" }Standard semver versioning from npm.
Peer Dependencies
For library packages that expect the consumer to provide dependencies:
// packages/ui/package.json
{
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"react": "^18.0.0", // For development/testing
"react-dom": "^18.0.0"
}
}Common Issues
"Module not found"
1. Check the dependency is installed in the right package 2. Run pnpm install / npm install to update lockfile 3. Check exports are defined in the package
Version Conflicts
Packages can use different versions - this is a feature, not a bug. But if you need consistency:
1. Use tooling (syncpack, manypkg) 2. Use pnpm catalogs 3. Create a lint rule
Hoisting Issues
Some tools expect dependencies in specific locations. Use package manager config:
# .npmrc (pnpm)
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*Lockfile
Required for:
- Reproducible builds
- Turborepo dependency analysis
- Cache correctness
# Commit your lockfile!
git add pnpm-lock.yaml # or package-lock.json, yarn.lockCreating Internal Packages
How to create and structure internal packages in your monorepo.
Package Creation Checklist
1. Create directory in packages/ 2. Add package.json with name and exports 3. Add source code in src/ 4. Add tsconfig.json if using TypeScript 5. Install as dependency in consuming packages 6. Run package manager install to update lockfile
Package Compilation Strategies
Just-in-Time (JIT)
Export TypeScript directly. The consuming app's bundler compiles it.
// packages/ui/package.json
{
"name": "@repo/ui",
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
},
"scripts": {
"lint": "eslint .",
"check-types": "tsc --noEmit"
}
}When to use:
- Apps use modern bundlers (Turbopack, webpack, Vite)
- You want minimal configuration
- Build times are acceptable without caching
Limitations:
- No Turborepo cache for the package itself
- Consumer must support TypeScript compilation
- Can't use TypeScript
paths(use Node.js subpath imports instead)
Compiled
Package handles its own compilation.
// packages/ui/package.json
{
"name": "@repo/ui",
"exports": {
"./button": {
"types": "./src/button.tsx",
"default": "./dist/button.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
}// packages/ui/tsconfig.json
{
"extends": "@repo/typescript-config/library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}When to use:
- You want Turborepo to cache builds
- Package will be used by non-bundler tools
- You need maximum compatibility
Remember: Add dist/** to turbo.json outputs!
Defining Exports
Multiple Entrypoints
{
"exports": {
".": "./src/index.ts", // @repo/ui
"./button": "./src/button.tsx", // @repo/ui/button
"./card": "./src/card.tsx", // @repo/ui/card
"./hooks": "./src/hooks/index.ts" // @repo/ui/hooks
}
}Conditional Exports (Compiled)
{
"exports": {
"./button": {
"types": "./src/button.tsx",
"import": "./dist/button.mjs",
"require": "./dist/button.cjs",
"default": "./dist/button.js"
}
}
}Installing Internal Packages
Add to Consuming Package
// apps/web/package.json
{
"dependencies": {
"@repo/ui": "workspace:*" // pnpm/bun
// "@repo/ui": "*" // npm/yarn
}
}Run Install
pnpm install # Updates lockfile with new dependencyImport and Use
// apps/web/src/page.tsx
import { Button } from '@repo/ui/button';
export default function Page() {
return <Button>Click me</Button>;
}One Purpose Per Package
Good Examples
packages/
├── ui/ # Shared UI components
├── utils/ # General utilities
├── auth/ # Authentication logic
├── database/ # Database client/schemas
├── eslint-config/ # ESLint configuration
├── typescript-config/ # TypeScript configuration
└── api-client/ # Generated API clientAvoid Mega-Packages
// BAD: One package for everything
packages/
└── shared/
├── components/
├── utils/
├── hooks/
├── types/
└── api/
// GOOD: Separate by purpose
packages/
├── ui/ # Components
├── utils/ # Utilities
├── hooks/ # React hooks
├── types/ # Shared TypeScript types
└── api-client/ # API utilitiesConfig Packages
TypeScript Config
// packages/typescript-config/package.json
{
"name": "@repo/typescript-config",
"exports": {
"./base.json": "./base.json",
"./nextjs.json": "./nextjs.json",
"./library.json": "./library.json"
}
}ESLint Config
// packages/eslint-config/package.json
{
"name": "@repo/eslint-config",
"exports": {
"./base": "./base.js",
"./next": "./next.js"
},
"dependencies": {
"eslint": "^8.0.0",
"eslint-config-next": "latest"
}
}Common Mistakes
Forgetting to Export
// BAD: No exports defined
{
"name": "@repo/ui"
}
// GOOD: Clear exports
{
"name": "@repo/ui",
"exports": {
"./button": "./src/button.tsx"
}
}Wrong Workspace Syntax
// pnpm/bun
{ "@repo/ui": "workspace:*" } // Correct
// npm/yarn
{ "@repo/ui": "*" } // Correct
{ "@repo/ui": "workspace:*" } // Wrong for npm/yarn!Missing from turbo.json Outputs
// Package builds to dist/, but turbo.json doesn't know
{
"tasks": {
"build": {
"outputs": [".next/**"] // Missing dist/**!
}
}
}
// Correct
{
"tasks": {
"build": {
"outputs": [".next/**", "dist/**"]
}
}
}TypeScript Best Practices
Use Node.js Subpath Imports (Not paths)
TypeScript compilerOptions.paths breaks with JIT packages. Use Node.js subpath imports instead (TypeScript 5.4+).
JIT Package:
// packages/ui/package.json
{
"imports": {
"#*": "./src/*"
}
}// packages/ui/button.tsx
import { MY_STRING } from "#utils.ts"; // Uses .ts extensionCompiled Package:
// packages/ui/package.json
{
"imports": {
"#*": "./dist/*"
}
}// packages/ui/button.tsx
import { MY_STRING } from "#utils.js"; // Uses .js extensionUse tsc for Internal Packages
For internal packages, prefer tsc over bundlers. Bundlers can mangle code before it reaches your app's bundler, causing hard-to-debug issues.
Enable Go-to-Definition
For Compiled Packages, enable declaration maps:
// tsconfig.json
{
"compilerOptions": {
"declaration": true,
"declarationMap": true
}
}This creates .d.ts and .d.ts.map files for IDE navigation.
No Root tsconfig.json Needed
Each package should have its own tsconfig.json. A root one causes all tasks to miss cache when changed. Only use root tsconfig.json for non-package scripts.
Avoid TypeScript Project References
They add complexity and another caching layer. Turborepo handles dependencies better.
Monorepo Best Practices
Essential patterns for structuring and maintaining a healthy Turborepo monorepo.
Repository Structure
Standard Layout
my-monorepo/
├── apps/ # Application packages (deployable)
│ ├── web/
│ ├── docs/
│ └── api/
├── packages/ # Library packages (shared code)
│ ├── ui/
│ ├── utils/
│ └── config-*/ # Shared configs (eslint, typescript, etc.)
├── package.json # Root package.json (minimal deps)
├── turbo.json # Turborepo configuration
├── pnpm-workspace.yaml # (pnpm) or workspaces in package.json
└── pnpm-lock.yaml # Lockfile (required)Key Principles
1. `apps/` for deployables: Next.js sites, APIs, CLIs - things that get deployed 2. `packages/` for libraries: Shared code consumed by apps or other packages 3. One purpose per package: Each package should do one thing well 4. No nested packages: Don't put packages inside packages
Package Types
Application Packages (apps/)
- Deployable: These are the "endpoints" of your package graph
- Not installed by other packages: Apps shouldn't be dependencies of other packages
- No shared code: If code needs sharing, extract to
packages/
// apps/web/package.json
{
"name": "web",
"private": true,
"dependencies": {
"@repo/ui": "workspace:*",
"next": "latest"
}
}Library Packages (packages/)
- Shared code: Utilities, components, configs
- Namespaced names: Use
@repo/or@yourorg/prefix - Clear exports: Define what the package exposes
// packages/ui/package.json
{
"name": "@repo/ui",
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
}
}Package Compilation Strategies
Just-in-Time (Simplest)
Export TypeScript directly; let the app's bundler compile it.
{
"name": "@repo/ui",
"exports": {
"./button": "./src/button.tsx"
}
}Pros: Zero build config, instant changes Cons: Can't cache builds, requires app bundler support
Compiled (Recommended for Libraries)
Package compiles itself with tsc or bundler.
{
"name": "@repo/ui",
"exports": {
"./button": {
"types": "./src/button.tsx",
"default": "./dist/button.js"
}
},
"scripts": {
"build": "tsc"
}
}Pros: Cacheable by Turborepo, works everywhere Cons: More configuration
Dependency Management
Install Where Used
Install dependencies in the package that uses them, not the root.
# Good: Install in the package that needs it
pnpm add lodash --filter=@repo/utils
# Avoid: Installing everything at root
pnpm add lodash -w # Only for repo-level toolsRoot Dependencies
Only these belong in root package.json:
turbo- The build systemhusky,lint-staged- Git hooks- Repository-level tooling
Internal Dependencies
Use workspace protocol for internal packages:
// pnpm/bun
{ "@repo/ui": "workspace:*" }
// npm/yarn
{ "@repo/ui": "*" }Exports Best Practices
Use exports Field (Not main)
{
"exports": {
".": "./src/index.ts",
"./button": "./src/button.tsx",
"./utils": "./src/utils.ts"
}
}Avoid Barrel Files
Don't create index.ts files that re-export everything:
// BAD: packages/ui/src/index.ts
export * from './button';
export * from './card';
export * from './modal';
// ... imports everything even if you need one thing
// GOOD: Direct exports in package.json
{
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
}
}Namespace Your Packages
// Good
{ "name": "@repo/ui" }
{ "name": "@acme/utils" }
// Avoid (conflicts with npm registry)
{ "name": "ui" }
{ "name": "utils" }Common Anti-Patterns
Accessing Files Across Package Boundaries
// BAD: Reaching into another package
import { Button } from "../../packages/ui/src/button";
// GOOD: Install and import properly
import { Button } from "@repo/ui/button";Shared Code in Apps
// BAD
apps/
web/
shared/ # This should be a package!
utils.ts
// GOOD
packages/
utils/ # Proper shared package
src/utils.tsToo Many Root Dependencies
// BAD: Root has app dependencies
{
"dependencies": {
"react": "^18",
"next": "^14",
"lodash": "^4"
}
}
// GOOD: Root only has repo tools
{
"devDependencies": {
"turbo": "latest",
"husky": "latest"
}
}See Also
- structure.md - Detailed repository structure patterns
- packages.md - Creating and managing internal packages
- dependencies.md - Dependency management strategies
Repository Structure
Detailed guidance on structuring a Turborepo monorepo.
Workspace Configuration
pnpm (Recommended)
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"npm/yarn/bun
// package.json
{
"workspaces": ["apps/*", "packages/*"]
}Root package.json
{
"name": "my-monorepo",
"private": true,
"packageManager": "pnpm@9.0.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "latest"
}
}Key points:
private: true- Prevents accidental publishingpackageManager- Enforces consistent package manager version- Scripts only delegate to `turbo run` - No actual build logic here!
- Minimal devDependencies (just turbo and repo tools)
Always Prefer Package Tasks
Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.
// packages/web/package.json
{
"scripts": {
"build": "next build",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}
// packages/api/package.json
{
"scripts": {
"build": "tsc",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}Package tasks enable Turborepo to:
1. Parallelize - Run web#lint and api#lint simultaneously 2. Cache individually - Each package's task output is cached separately 3. Filter precisely - Run turbo run test --filter=web for just one package
Root Tasks are a fallback for tasks that truly cannot run per-package:
// AVOID unless necessary - sequential, not parallelized, can't filter
{
"scripts": {
"lint": "eslint apps/web && eslint apps/api && eslint packages/ui"
}
}Root turbo.json
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}With futureFlags.globalConfiguration, global settings move under a global key:
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": ["tsconfig.json"],
"env": ["CI"]
},
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}Directory Organization
Grouping Packages
You can group packages by adding more workspace paths:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "packages/config/*" # Grouped configs
- "packages/features/*" # Feature packagesThis allows:
packages/
├── ui/
├── utils/
├── config/
│ ├── eslint/
│ ├── typescript/
│ └── tailwind/
└── features/
├── auth/
└── payments/What NOT to Do
# BAD: Nested wildcards cause ambiguous behavior
packages:
- "packages/**" # Don't do this!Package Anatomy
Minimum Required Files
packages/ui/
├── package.json # Required: Makes it a package
├── src/ # Source code
│ └── button.tsx
└── tsconfig.json # TypeScript config (if using TS)package.json Requirements
{
"name": "@repo/ui", // Unique, namespaced name
"version": "0.0.0", // Version (can be 0.0.0 for internal)
"private": true, // Prevents accidental publishing
"exports": {
// Entry points
"./button": "./src/button.tsx"
}
}TypeScript Configuration
Shared Base Config
Create a shared TypeScript config package:
packages/
└── typescript-config/
├── package.json
├── base.json
├── nextjs.json
└── library.json// packages/typescript-config/base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ES2022"
}
}Extending in Packages
// packages/ui/tsconfig.json
{
"extends": "@repo/typescript-config/library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}No Root tsconfig.json
You likely don't need a tsconfig.json in the workspace root. Each package should have its own config extending from the shared config package.
ESLint Configuration
Shared Config Package
packages/
└── eslint-config/
├── package.json
├── base.js
├── next.js
└── library.js// packages/eslint-config/package.json
{
"name": "@repo/eslint-config",
"exports": {
"./base": "./base.js",
"./next": "./next.js",
"./library": "./library.js"
}
}Using in Packages
// apps/web/.eslintrc.js
module.exports = {
extends: ["@repo/eslint-config/next"]
};Lockfile
A lockfile is required for:
- Reproducible builds
- Turborepo to understand package dependencies
- Cache correctness
Without a lockfile, you'll see unpredictable behavior.
Boundaries
Experimental feature - See RFC
Full docs: https://turborepo.dev/docs/reference/boundaries
Boundaries enforce package isolation by detecting:
1. Imports of files outside the package's directory 2. Imports of packages not declared in package.json dependencies
Usage
turbo boundariesRun this to check for workspace violations across your monorepo.
Tags
Tags allow you to create rules for which packages can depend on each other.
Adding Tags to a Package
// packages/ui/turbo.json
{
"tags": ["internal"]
}Configuring Tag Rules
Rules go in root turbo.json:
// turbo.json
{
"boundaries": {
"tags": {
"public": {
"dependencies": {
"deny": ["internal"]
}
}
}
}
}This prevents public-tagged packages from importing internal-tagged packages.
Rule Types
Allow-list approach (only allow specific tags):
{
"boundaries": {
"tags": {
"public": {
"dependencies": {
"allow": ["public"]
}
}
}
}
}Deny-list approach (block specific tags):
{
"boundaries": {
"tags": {
"public": {
"dependencies": {
"deny": ["internal"]
}
}
}
}
}Restrict dependents (who can import this package):
{
"boundaries": {
"tags": {
"private": {
"dependents": {
"deny": ["public"]
}
}
}
}
}Using Package Names
Package names work in place of tags:
{
"boundaries": {
"tags": {
"private": {
"dependents": {
"deny": ["@repo/my-pkg"]
}
}
}
}
}Key Points
- Rules apply transitively (dependencies of dependencies)
- Helps enforce architectural boundaries at scale
- Catches violations before runtime/build errors
Debugging Cache Issues
Diagnostic Tools
--summarize
Generates a JSON file with all hash inputs. Compare two runs to find differences.
turbo build --summarize
# Creates .turbo/runs/<run-id>.jsonThe summary includes:
- Global hash and its inputs
- Per-task hashes and their inputs
- Environment variables that affected the hash
Comparing runs:
# Run twice, compare the summaries
diff .turbo/runs/<first-run>.json .turbo/runs/<second-run>.json--dry / --dry=json
See what would run without executing anything:
turbo build --dry
turbo build --dry=json # machine-readable outputShows cache status for each task without running them.
--force
Skip reading cache, re-execute all tasks:
turbo build --forceUseful to verify tasks actually work (not just cached results).
Unexpected Cache Misses
Symptom: Task runs when you expected a cache hit.
Environment Variable Changed
Check if an env var in the env key changed:
{
"tasks": {
"build": {
"env": ["API_URL", "NODE_ENV"]
}
}
}Different API_URL between runs = cache miss.
.env File Changed
.env files aren't tracked by default. Add to inputs:
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", ".env", ".env.local"]
}
}
}Or use globalDependencies for repo-wide env files:
{
"globalDependencies": [".env"]
}With futureFlags.globalConfiguration, use global.inputs instead. The key difference: global.inputs files are folded into each task's hash individually (not the global hash), so tasks can exclude specific files with negation globs.
{
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": [".env"]
}
}Lockfile Changed
Installing/updating packages changes the global hash.
Source Files Changed
Any file in the package (or in inputs) triggers a miss.
turbo.json Changed
Config changes invalidate the global hash.
Incorrect Cache Hits
Symptom: Cached output is stale/wrong.
Missing Environment Variable
Task uses an env var not listed in env:
// build.js
const apiUrl = process.env.API_URL; // not tracked!Fix: add to task config:
{
"tasks": {
"build": {
"env": ["API_URL"]
}
}
}Missing File in Inputs
Task reads a file outside default inputs:
{
"tasks": {
"build": {
"inputs": [
"$TURBO_DEFAULT$",
"../../shared-config.json" // file outside package
]
}
}
}Useful Flags
# Only show output for cache misses
turbo build --output-logs=new-only
# Show output for everything (debugging)
turbo build --output-logs=full
# See why tasks are running
turbo build --verbosity=2Debugging with globalConfiguration Enabled
When futureFlags.globalConfiguration is on, global.inputs files appear in per-task hash inputs (not the global hash). If you're getting unexpected cache misses:
1. Check --summarize output — global input files will show up in the task inputs section, not the global hash section 2. Verify tasks aren't accidentally excluding global inputs via negation globs in inputs 3. Remember that toggling the globalConfiguration flag itself invalidates all caches (the flag value is part of the global hash)
If you're getting unexpected cache hits after changing a global input file, the task may be excluding that file with a negation glob. Check the task's inputs for !$TURBO_ROOT$/... patterns.
Quick Checklist
Cache miss when expected hit:
1. Run with --summarize, compare with previous run 2. Check env vars with --dry=json 3. Look for lockfile/config changes in git
Cache hit when expected miss:
1. Verify env var is in env array 2. Verify file is in inputs array 3. Check if file is outside package directory
Remote Caching
Share cache artifacts across your team and CI pipelines.
Benefits
- Team members get cache hits from each other's work
- CI gets cache hits from local development (and vice versa)
- Dramatically faster CI runs after first build
- No more "works on my machine" rebuilds
Vercel Remote Cache
Free, zero-config when deploying on Vercel. For local dev and other CI:
Local Development Setup
# Authenticate with Vercel
npx turbo login
# Link repo to your Vercel team
npx turbo linkThis creates .turbo/config.json with your team info (gitignored by default).
CI Setup
Set these environment variables:
TURBO_TOKEN=<your-token>
TURBO_TEAM=<your-team-slug>Get your token from Vercel dashboard → Settings → Tokens.
GitHub Actions example:
- name: Build
run: npx turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Configuration in turbo.json
{
"remoteCache": {
"enabled": true,
"signature": false
}
}Options:
enabled: toggle remote cache (default: true when authenticated)signature: require artifact signing (default: false)
Artifact Signing
Verify cache artifacts haven't been tampered with:
# Set a secret key (use same key across all environments)
export TURBO_REMOTE_CACHE_SIGNATURE_KEY="your-secret-key"Enable in config:
{
"remoteCache": {
"signature": true
}
}Signed artifacts can only be restored if the signature matches.
Self-Hosted Options
Community implementations for running your own cache server:
- turbo-remote-cache (Node.js) - supports S3, GCS, Azure
- turborepo-remote-cache (Go) - lightweight, S3-compatible
- ducktape (Rust) - high-performance option
Configure with environment variables:
TURBO_API=https://your-cache-server.com
TURBO_TOKEN=your-auth-token
TURBO_TEAM=your-teamCache Behavior Control
# Disable remote cache for a run
turbo build --remote-cache-read-only # read but don't write
turbo build --no-cache # skip cache entirely
# Environment variable alternative
TURBO_REMOTE_ONLY=true # only use remote, skip localDebugging Remote Cache
# Verbose output shows cache operations
turbo build --verbosity=2
# Check if remote cache is configured
turbo configLook for:
- "Remote caching enabled" in output
- Upload/download messages during runs
- "cache hit, replaying output" with remote cache indicator
How Turborepo Caching Works
Turborepo's core principle: never do the same work twice.
The Cache Equation
fingerprint(inputs) → stored outputsIf inputs haven't changed, restore outputs from cache instead of re-running the task.
What Determines the Cache Key
Global Hash Inputs
These affect ALL tasks in the repo:
package-lock.json/yarn.lock/pnpm-lock.yaml- Files listed in
globalDependencies(orglobal.envwhen usingglobalConfiguration) - Environment variables in
globalEnv(orglobal.env) turbo.jsonconfiguration
{
"globalDependencies": [".env", "tsconfig.base.json"],
"globalEnv": ["CI", "NODE_ENV"]
}Task Hash Inputs
These affect specific tasks:
- All files in the package (unless filtered by
inputs) package.jsoncontents- Environment variables in task's
envkey - Task configuration (command, outputs, dependencies)
- Hashes of dependent tasks (
dependsOn) - Files from
global.inputs(when usingfutureFlags.globalConfiguration— see below)
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "package.json", "tsconfig.json"],
"env": ["API_URL"]
}
}
}How global.inputs Changes the Hash Equation
When futureFlags.globalConfiguration is enabled, global.inputs files are not part of the global hash. Instead, they are prepended to every task's inputs and folded into the task hash. This is a fundamental change from globalDependencies.
With `globalDependencies` (default):
task cache key = hash(global hash, task hash)
↑ includes globalDependencies file hashesChanging a globalDependencies file invalidates every task, regardless of task-level inputs. There is no way for a task to opt out.
With `global.inputs` (`futureFlags.globalConfiguration`):
task cache key = hash(global hash, task hash)
↑ includes global.inputs file hashes (merged with task inputs)global.inputs files are merged into each task's input globs. This means:
- Tasks can exclude specific global files with negation globs:
"inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/tsconfig.json"] - The global hash is smaller (it still includes lockfile, engines,
global.env, etc. — but not file hashes fromglobal.inputs) - The task hash correctly includes the global input file hashes alongside the task's own inputs
{
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": ["tsconfig.json", ".env"]
},
"tasks": {
"build": {
"outputs": ["dist/**"]
},
"lint": {
"inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/tsconfig.json"]
}
}
}In this example, changing tsconfig.json invalidates build (it's in the task's inputs) but not lint (which explicitly excludes it). With globalDependencies, both would have been invalidated.
What Gets Cached
1. File outputs - files/directories specified in outputs 2. Task logs - stdout/stderr for replay on cache hit
{
"tasks": {
"build": {
"outputs": ["dist/**", ".next/**"]
}
}
}Local Cache Location
.turbo/cache/
├── <hash1>.tar.zst # compressed outputs
├── <hash2>.tar.zst
└── ...Add .turbo to .gitignore.
Cache Restoration
On cache hit, Turborepo:
1. Extracts archived outputs to their original locations 2. Replays the logged stdout/stderr 3. Reports the task as cached (shows FULL TURBO in output)
Example Flow
# First run - executes build, caches result
turbo build
# → packages/ui: cache miss, executing...
# → packages/web: cache miss, executing...
# Second run - same inputs, restores from cache
turbo build
# → packages/ui: cache hit, replaying output
# → packages/web: cache hit, replaying output
# → FULL TURBOKey Points
- Cache is content-addressed (based on input hash, not timestamps)
- Empty
outputsarray means task runs but nothing is cached - Tasks without
outputskey cache nothing (use"outputs": []to be explicit) - Cache is invalidated when ANY input changes
GitHub Actions
Complete setup guide for Turborepo with GitHub Actions.
Basic Workflow Structure
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Build and Test
run: turbo run build test lintPackage Manager Setup
pnpm
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfileYarn
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "yarn"
- run: yarn install --frozen-lockfileBun
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: bun install --frozen-lockfileRemote Cache Setup
1. Create Vercel Access Token
1. Go to Vercel Dashboard 2. Create a new token with appropriate scope 3. Copy the token value
2. Add Secrets and Variables
In your GitHub repository settings:
Secrets (Settings > Secrets and variables > Actions > Secrets):
TURBO_TOKEN: Your Vercel access token
Variables (Settings > Secrets and variables > Actions > Variables):
TURBO_TEAM: Your Vercel team slug
3. Add to Workflow
jobs:
build:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Alternative: actions/cache
If you can't use remote cache, cache Turborepo's local cache directory:
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('**/turbo.json', '**/package-lock.json') }}
restore-keys: |
turbo-${{ runner.os }}-Note: This is less effective than remote cache since it's per-branch.
Complete Example
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: turbo run build --affected
- name: Test
run: turbo run test --affected
- name: Lint
run: turbo run lint --affectedCI Optimization Patterns
Strategies for efficient CI/CD with Turborepo.
PR vs Main Branch Builds
PR Builds: Only Affected
Test only what changed in the PR:
- name: Test (PR)
if: github.event_name == 'pull_request'
run: turbo run build test --affectedMain Branch: Full Build
Ensure complete validation on merge:
- name: Test (Main)
if: github.ref == 'refs/heads/main'
run: turbo run build testCustom Git Ranges with --filter
For advanced scenarios, use --filter with git refs:
# Changes since specific commit
turbo run test --filter="...[abc123]"
# Changes between refs
turbo run test --filter="...[main...HEAD]"
# Changes in last 3 commits
turbo run test --filter="...[HEAD~3]"Caching Strategies
Remote Cache (Recommended)
Best performance - shared across all CI runs and developers:
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}actions/cache Fallback
When remote cache isn't available:
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ github.ref }}-
turbo-${{ runner.os }}-Limitations:
- Cache is branch-scoped
- PRs restore from base branch cache
- Less efficient than remote cache
Matrix Builds
Test across Node versions:
strategy:
matrix:
node: [18, 20, 22]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: turbo run testParallelizing Across Jobs
Split tasks into separate jobs:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: turbo run lint --affected
test:
runs-on: ubuntu-latest
steps:
- run: turbo run test --affected
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- run: turbo run buildCache Considerations
When parallelizing:
- Each job has separate cache writes
- Remote cache handles this automatically
- With actions/cache, use unique keys per job to avoid conflicts
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.job }}-${{ github.sha }}Conditional Tasks
Skip expensive tasks on draft PRs:
- name: E2E Tests
if: github.event.pull_request.draft == false
run: turbo run test:e2e --affectedOr require label for full test:
- name: Full Test Suite
if: contains(github.event.pull_request.labels.*.name, 'full-test')
run: turbo run testCI/CD with Turborepo
General principles for running Turborepo in continuous integration environments.
Core Principles
Always Use turbo run in CI
Never use the `turbo <tasks>` shorthand in CI or scripts. Always use turbo run:
# CORRECT - Always use in CI, package.json, scripts
turbo run build test lint
# WRONG - Shorthand is only for one-off terminal commands
turbo build test lintThe shorthand turbo <tasks> is only for one-off invocations typed directly in terminal by humans or agents. Anywhere the command is written into code (CI, package.json, scripts), use turbo run.
Enable Remote Caching
Remote caching dramatically speeds up CI by sharing cached artifacts across runs.
Required environment variables:
TURBO_TOKEN=your_vercel_token
TURBO_TEAM=your_team_slugUse --affected for PR Builds
The --affected flag only runs tasks for packages changed since the base branch:
turbo run build test --affectedThis requires Git history to compute what changed.
Git History Requirements
Fetch Depth
--affected needs access to the merge base. Shallow clones break this.
# GitHub Actions
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Minimum for --affected
# Use 0 for full history if merge base is farWhy Shallow Clones Break --affected
Turborepo compares the current HEAD to the merge base with main. If that commit isn't fetched, --affected falls back to running everything.
For PRs with many commits, consider:
fetch-depth: 0 # Full historyEnvironment Variables Reference
| Variable | Purpose |
|---|---|
TURBO_TOKEN | Vercel access token for remote cache |
TURBO_TEAM | Your Vercel team slug |
TURBO_REMOTE_ONLY | Skip local cache, use remote only |
TURBO_LOG_ORDER | Set to grouped for cleaner CI logs |
See Also
- github-actions.md - GitHub Actions setup
- vercel.md - Vercel deployment
- patterns.md - CI optimization patterns
Vercel Deployment
Turborepo integrates seamlessly with Vercel for monorepo deployments.
Remote Cache
Remote caching is automatically enabled when deploying to Vercel. No configuration needed - Vercel detects Turborepo and enables caching.
This means:
- No
TURBO_TOKENorTURBO_TEAMsetup required on Vercel - Cache is shared across all deployments
- Preview and production builds benefit from cache
turbo-ignore
Skip unnecessary builds when a package hasn't changed using turbo-ignore.
Installation
npx turbo-ignoreOr install globally in your project:
pnpm add -D turbo-ignoreSetup in Vercel
1. Go to your project in Vercel Dashboard 2. Navigate to Settings > Git > Ignored Build Step 3. Select "Custom" and enter:
npx turbo-ignoreHow It Works
turbo-ignore checks if the current package (or its dependencies) changed since the last successful deployment:
1. Compares current commit to last deployed commit 2. Uses Turborepo's dependency graph 3. Returns exit code 0 (skip) if no changes 4. Returns exit code 1 (build) if changes detected
Options
# Check specific package
npx turbo-ignore web
# Use specific comparison ref
npx turbo-ignore --fallback=HEAD~1
# Verbose output
npx turbo-ignore --verboseEnvironment Variables
Set environment variables in Vercel Dashboard:
1. Go to Project Settings > Environment Variables 2. Add variables for each environment (Production, Preview, Development)
Common variables:
DATABASE_URLAPI_KEY- Package-specific config
Monorepo Root Directory
For monorepos, set the root directory in Vercel:
1. Project Settings > General > Root Directory 2. Set to the package path (e.g., apps/web)
Vercel automatically:
- Installs dependencies from monorepo root
- Runs build from the package directory
- Detects framework settings
Build Command
Vercel auto-detects turbo run build when turbo.json exists at root.
Override if needed:
turbo run build --filter=webOr for production-only optimizations:
turbo run build --filter=web --env-mode=strictturbo run Flags Reference
Full docs: https://turborepo.dev/docs/reference/run
Package Selection
--filter / -F
Select specific packages to run tasks in.
turbo build --filter=web
turbo build -F=@repo/ui -F=@repo/utils
turbo test --filter=./apps/*See filtering/ for complete syntax (globs, dependencies, git ranges).
Task Identifier Syntax (v2.2.4+)
Run specific package tasks directly:
turbo run web#build # Build web package
turbo run web#build docs#lint # Multiple specific tasks--affected
Run only in packages changed since the base branch.
turbo build --affected
turbo test --affected --filter=./apps/* # combine with filterHow it works:
- Default: compares
main...HEAD - In GitHub Actions: auto-detects
GITHUB_BASE_REF - Override base:
TURBO_SCM_BASE=development turbo build --affected - Override head:
TURBO_SCM_HEAD=your-branch turbo build --affected
Requires git history - shallow clones may fall back to running all tasks.
Execution Control
--dry / --dry=json
Preview what would run without executing.
turbo build --dry # human-readable
turbo build --dry=json # machine-readable--force
Ignore all cached artifacts, re-run everything.
turbo build --force--concurrency
Limit parallel task execution.
turbo build --concurrency=4 # max 4 tasks
turbo build --concurrency=50% # 50% of CPU cores--continue
Keep running other tasks when one fails.
turbo build test --continue--only
Run only the specified task, skip its dependencies.
turbo build --only # skip running dependsOn tasks--parallel (Discouraged)
Ignores task graph dependencies, runs all tasks simultaneously. Avoid using this flag—if tasks need to run in parallel, configure dependsOn correctly instead. Using --parallel bypasses Turborepo's dependency graph, which can cause race conditions and incorrect builds.
Cache Control
--cache
Fine-grained cache behavior control.
# Default: read/write both local and remote
turbo build --cache=local:rw,remote:rw
# Read-only local, no remote
turbo build --cache=local:r,remote:
# Disable local, read-only remote
turbo build --cache=local:,remote:r
# Disable all caching
turbo build --cache=local:,remote:Output & Debugging
--graph
Generate task graph visualization.
turbo build --graph # opens in browser
turbo build --graph=graph.svg # SVG file
turbo build --graph=graph.png # PNG file
turbo build --graph=graph.json # JSON data
turbo build --graph=graph.mermaid # Mermaid diagram--summarize
Generate JSON run summary for debugging.
turbo build --summarize
# creates .turbo/runs/<run-id>.json--output-logs
Control log output verbosity.
turbo build --output-logs=full # all logs (default)
turbo build --output-logs=new-only # only cache misses
turbo build --output-logs=errors-only # only failures
turbo build --output-logs=none # silent--profile
Generate Chrome tracing profile for performance analysis.
turbo build --profile=profile.json
# open chrome://tracing and load the file--verbosity / -v
Control turbo's own log level.
turbo build -v # verbose
turbo build -vv # more verbose
turbo build -vvv # maximum verbosityEnvironment
--env-mode
Control environment variable handling.
turbo build --env-mode=strict # only declared env vars (default)
turbo build --env-mode=loose # include all env vars in hashUI
--ui
Select output interface.
turbo build --ui=tui # interactive terminal UI (default in TTY)
turbo build --ui=stream # streaming logs (default in CI)---
turbo-ignore
Full docs: https://turborepo.dev/docs/reference/turbo-ignore
Skip CI work when nothing relevant changed. Useful for skipping container setup.
Basic Usage
# Check if build is needed for current package (uses Automatic Package Scoping)
npx turbo-ignore
# Check specific package
npx turbo-ignore web
# Check specific task
npx turbo-ignore --task=testExit Codes
0: No changes detected - skip CI work1: Changes detected - proceed with CI
CI Integration Example
# GitHub Actions
- name: Check for changes
id: turbo-ignore
run: npx turbo-ignore web
continue-on-error: true
- name: Build
if: steps.turbo-ignore.outcome == 'failure' # changes detected
run: pnpm buildComparison Depth
Default: compares to parent commit (HEAD^1).
# Compare to specific commit
npx turbo-ignore --fallback=abc123
# Compare to branch
npx turbo-ignore --fallback=main---
Other Commands
turbo boundaries
Check workspace violations (experimental).
turbo boundariesSee references/boundaries/ for configuration.
turbo watch
Re-run tasks on file changes.
turbo watch build testSee references/watch/ for details.
turbo prune
Create sparse checkout for Docker.
turbo prune web --dockerturbo link / unlink
Connect/disconnect Remote Cache.
turbo link # connect to Vercel Remote Cache
turbo unlink # disconnectturbo login / logout
Authenticate with Remote Cache provider.
turbo login # authenticate
turbo logout # log outturbo generate
Scaffold new packages.
turbo generateturbo run
The primary command for executing tasks across your monorepo.
Basic Usage
# Full form (use in CI, package.json, scripts)
turbo run <tasks>
# Shorthand (only for one-off terminal invocations)
turbo <tasks>When to Use turbo run vs turbo
Always use `turbo run` when the command is written into code:
package.jsonscripts- CI/CD workflows (GitHub Actions, etc.)
- Shell scripts
- Documentation
- Any static/committed configuration
Only use `turbo` (shorthand) for:
- One-off commands typed directly in terminal
- Ad-hoc invocations by humans or agents
// package.json - ALWAYS use "turbo run"
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
}
}# CI workflow - ALWAYS use "turbo run"
- run: turbo run build --affected
- run: turbo run test --affected# Terminal one-off - shorthand OK
turbo build --filter=webRunning Tasks
Tasks must be defined in turbo.json before running.
# Single task
turbo build
# Multiple tasks
turbo run build lint test
# See available tasks (run without arguments)
turbo runPassing Arguments to Scripts
Use -- to pass arguments through to the underlying package scripts:
turbo run build -- --sourcemap
turbo test -- --watch
turbo lint -- --fixEverything after -- goes directly to the task's script.
Package Selection
By default, turbo runs tasks in all packages. Use --filter to narrow scope:
turbo build --filter=web
turbo test --filter=./apps/*See filtering/ for complete filter syntax.
Quick Reference
| Goal | Command |
|---|---|
| Build everything | turbo build |
| Build one package | turbo build --filter=web |
| Multiple tasks | turbo build lint test |
| Pass args to script | turbo build -- --arg |
| Preview run | turbo build --dry |
| Force rebuild | turbo build --force |
Global Options Reference
Options that affect all tasks. Full docs: https://turborepo.dev/docs/reference/configuration
globalEnv
Environment variables affecting all task hashes.
{
"globalEnv": ["CI", "NODE_ENV", "VERCEL_*"]
}Use for variables that should invalidate all caches when changed.
globalDependencies
Files that affect all task hashes.
{
"globalDependencies": ["tsconfig.json", ".env", "pnpm-lock.yaml"]
}Lockfile is included by default. Add shared configs here.
globalPassThroughEnv
Variables available to tasks but not included in hash.
{
"globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"]
}Use for credentials that shouldn't affect cache keys.
cacheDir
Custom cache location. Default: node_modules/.cache/turbo.
{
"cacheDir": ".turbo/cache"
}daemon
Deprecated: The daemon is no longer used for turbo run and this option will be removed in version 3.0. The daemon is still used by turbo watch and the Turborepo LSP.
envMode
How unspecified env vars are handled. Default: "strict".
{
"envMode": "strict" // Only specified vars available
// or
"envMode": "loose" // All vars pass through
}Strict mode catches missing env declarations.
ui
Terminal UI mode. Default: "stream".
{
"ui": "tui" // Interactive terminal UI
// or
"ui": "stream" // Traditional streaming logs
}TUI provides better UX for parallel tasks.
remoteCache
Configure remote caching.
{
"remoteCache": {
"enabled": true,
"signature": true,
"timeout": 30,
"uploadTimeout": 60
}
}| Option | Default | Description |
|---|---|---|
enabled | true | Enable/disable remote caching |
signature | false | Sign artifacts with TURBO_REMOTE_CACHE_SIGNATURE_KEY |
preflight | false | Send OPTIONS request before cache requests |
timeout | 30 | Timeout in seconds for cache operations |
uploadTimeout | 60 | Timeout in seconds for uploads |
apiUrl | "https://vercel.com" | Remote cache API endpoint |
loginUrl | "https://vercel.com" | Login endpoint |
teamId | - | Team ID (must start with team_) |
teamSlug | - | Team slug for querystring |
See https://turborepo.dev/docs/core-concepts/remote-caching for setup.
concurrency
Default: "10"
Limit parallel task execution.
{
"concurrency": "4" // Max 4 tasks at once
// or
"concurrency": "50%" // 50% of available CPUs
}futureFlags
Enable experimental features that will become default in future versions.
{
"futureFlags": {
"errorsOnlyShowHash": true
}
}errorsOnlyShowHash
When using outputLogs: "errors-only", show task hashes on start/completion:
- Cache miss:
cache miss, executing <hash> (only logging errors) - Cache hit:
cache hit, replaying logs (no errors) <hash>
longerSignatureKey
Enforce a minimum key length of 32 bytes for TURBO_REMOTE_CACHE_SIGNATURE_KEY when remoteCache.signature is enabled. Short keys weaken HMAC-SHA256 signatures. Fails the run immediately if the key is too short.
globalConfiguration
Moves global configuration keys under a top-level global key for clarity and changes how global.inputs (formerly globalDependencies) affects task hashing.
When enabled:
- Global config keys move under
globalwith cleaner names global.inputsfiles are prepended to every task's inputs instead of being folded into the global hash — tasks can opt out of specific global inputs using negation globs
{
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": ["tsconfig.json", ".env"],
"env": ["CI", "NODE_ENV"],
"passThroughEnv": ["AWS_SECRET_KEY"],
"ui": "tui",
"envMode": "strict",
"cacheDir": ".turbo/cache",
"remoteCache": { "enabled": true },
"concurrency": "50%"
},
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}Key rename mapping:
| Old (top-level) | New (global.) |
|---|---|
globalDependencies | inputs |
globalEnv | env |
globalPassThroughEnv | passThroughEnv |
ui, envMode, cacheDir, daemon, concurrency, noUpdateNotifier, dangerouslyDisablePackageManagerCheck, remoteCache | Same names under global |
Behavior change for `global.inputs`:
With globalDependencies (old): files are hashed into the global hash, which is embedded in every task's cache key. Changing any of these files invalidates all tasks — there is no opt-out.
With global.inputs (new): files are treated as implicit task inputs prepended to each task's inputs globs. This means:
- Tasks can exclude specific global files:
"inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/tsconfig.json"] - The global hash no longer includes these file hashes (it still includes lockfile, engines, global env, etc.)
- Tasks with no explicit
inputsstill hash all package files plus the global inputs
See the gotchas doc for guidance on using $TURBO_DEFAULT$ with global.inputs.
noUpdateNotifier
Disable update notifications when new turbo versions are available.
{
"noUpdateNotifier": true
}dangerouslyDisablePackageManagerCheck
Bypass the packageManager field requirement. Use for incremental migration.
{
"dangerouslyDisablePackageManagerCheck": true
}Warning: Unstable lockfiles can cause unpredictable behavior.
Git Worktree Cache Sharing
When working in Git worktrees, Turborepo automatically shares local cache between the main worktree and linked worktrees.
How it works:
- Detects worktree configuration
- Redirects cache to main worktree's
.turbo/cache - Works alongside Remote Cache
Benefits:
- Cache hits across branches
- Reduced disk usage
- Faster branch switching
Disabled by: Setting explicit cacheDir in turbo.json.
Configuration Gotchas
Common mistakes and how to fix them.
#1 Root Scripts Not Using turbo run
Root package.json scripts for turbo tasks MUST use turbo run, not direct commands.
// WRONG - bypasses turbo, no parallelization or caching
{
"scripts": {
"build": "bun build",
"dev": "bun dev"
}
}
// CORRECT - delegates to turbo
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev"
}
}Why this matters: Running bun build or npm run build at root bypasses Turborepo entirely - no parallelization, no caching, no dependency graph awareness.
#2 Using && to Chain Turbo Tasks
Don't use && to chain tasks that turbo should orchestrate.
// WRONG - changeset:publish chains turbo task with non-turbo command
{
"scripts": {
"changeset:publish": "bun build && changeset publish"
}
}
// CORRECT - use turbo run, let turbo handle dependencies
{
"scripts": {
"changeset:publish": "turbo run build && changeset publish"
}
}If the second command (changeset publish) depends on build outputs, the turbo task should run through turbo to get caching and parallelization benefits.
#3 Overly Broad globalDependencies
globalDependencies affects hash for ALL tasks in ALL packages. Be specific.
// WRONG - affects all hashes
{
"globalDependencies": ["**/.env.*local"]
}
// CORRECT - move to specific tasks that need it
{
"globalDependencies": [".env"],
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**"]
}
}
}Why this matters: **/.env.*local matches .env files in ALL packages, causing unnecessary cache invalidation. Instead:
- Use
globalDependenciesonly for truly global files (root.env) - Use task-level
inputsfor package-specific .env files with$TURBO_DEFAULT$to preserve default behavior
With futureFlags.globalConfiguration, this is less of a concern because global.inputs acts as implicit task inputs — tasks can opt out of specific files with negation globs. But keeping the list focused is still good practice.
#4 Repetitive Task Configuration
Look for repeated configuration across tasks that can be collapsed.
// WRONG - repetitive env and inputs across tasks
{
"tasks": {
"build": {
"env": ["API_URL", "DATABASE_URL"],
"inputs": ["$TURBO_DEFAULT$", ".env*"]
},
"test": {
"env": ["API_URL", "DATABASE_URL"],
"inputs": ["$TURBO_DEFAULT$", ".env*"]
}
}
}
// BETTER - use globalEnv and globalDependencies
{
"globalEnv": ["API_URL", "DATABASE_URL"],
"globalDependencies": [".env*"],
"tasks": {
"build": {},
"test": {}
}
}When to use global vs task-level:
globalEnv/globalDependencies- affects ALL tasks, use for truly shared config- Task-level
env/inputs- use when only specific tasks need it
#5 Using ../ to Traverse Out of Package in inputs
Don't use relative paths like ../ to reference files outside the package. Use $TURBO_ROOT$ instead.
// WRONG - traversing out of package
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", "../shared-config.json"]
}
}
}
// CORRECT - use $TURBO_ROOT$ for repo root
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"]
}
}
}#6 MOST COMMON MISTAKE: Creating Root Tasks
Prefer package tasks over Root Tasks.
When you need to create a task (build, lint, test, typecheck, etc.), default to package tasks:
1. Add the script to each relevant package's package.json 2. Register the task in root turbo.json 3. Root package.json only contains turbo run <task>
// WRONG - DO NOT DO THIS
// Root package.json with task logic
{
"scripts": {
"build": "cd apps/web && next build && cd ../api && tsc",
"lint": "eslint apps/ packages/",
"test": "vitest"
}
}
// CORRECT - DO THIS
// 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" } }
// Root package.json - ONLY delegates
{ "scripts": { "build": "turbo run build", "lint": "turbo run lint", "test": "turbo run test" } }
// turbo.json - register tasks
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"lint": {},
"test": {}
}
}Why this matters:
- Package tasks run in parallel across all packages
- Each package's output is cached individually
- You can filter to specific packages:
turbo run test --filter=web
Root Tasks (//#taskname) defeat all these benefits when a task can live in packages. Only use them for tasks that truly cannot exist in any package, such as Vitest Projects' //#test, repo-wide release scripts, or tooling that does not invoke turbo itself.
#7 Tasks That Need Parallel Execution + Cache Invalidation
Some tasks can run in parallel (don't need built output from dependencies) but must still invalidate cache when dependency source code changes. Using dependsOn: ["^taskname"] forces sequential execution. Using no dependencies breaks cache invalidation.
Use Transit Nodes for these tasks:
// WRONG - forces sequential execution (SLOW)
"my-task": {
"dependsOn": ["^my-task"]
}
// ALSO WRONG - no dependency awareness (INCORRECT CACHING)
"my-task": {}
// CORRECT - use Transit Nodes for parallel + correct caching
{
"tasks": {
"transit": { "dependsOn": ["^transit"] },
"my-task": { "dependsOn": ["transit"] }
}
}Why Transit Nodes work:
transitcreates dependency relationships without matching any actual script- Tasks that depend on
transitgain dependency awareness - Since
transitcompletes instantly (no script), tasks run in parallel - Cache correctly invalidates when dependency source code changes
How to identify tasks that need this pattern: Look for tasks that read source files from dependencies but don't need their build outputs.
Missing outputs for File-Producing Tasks
Before flagging missing `outputs`, check what the task actually produces:
1. Read the package's script (e.g., "build": "tsc", "test": "vitest") 2. Determine if it writes files to disk or only outputs to stdout 3. Only flag if the task produces files that should be cached
// WRONG - build produces files but they're not cached
"build": {
"dependsOn": ["^build"]
}
// CORRECT - outputs are cached
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}No outputs key is fine for stdout-only tasks. For file-producing tasks, missing outputs means Turbo has nothing to cache.
Forgetting ^ in dependsOn
// WRONG - looks for "build" in SAME package (infinite loop or missing)
"build": {
"dependsOn": ["build"]
}
// CORRECT - runs dependencies' build first
"build": {
"dependsOn": ["^build"]
}The ^ means "in dependency packages", not "in this package".
Missing persistent on Dev Tasks
// WRONG - dependent tasks hang waiting for dev to "finish"
"dev": {
"cache": false
}
// CORRECT
"dev": {
"cache": false,
"persistent": true
}Package Config Missing extends
// WRONG - packages/web/turbo.json
{
"tasks": {
"build": { "outputs": [".next/**"] }
}
}
// CORRECT
{
"extends": ["//"],
"tasks": {
"build": { "outputs": [".next/**"] }
}
}Without "extends": ["//"], Package Configurations are invalid.
Root Tasks Need Special Syntax
To run a task defined only in root package.json:
# WRONG
turbo run format
# CORRECT
turbo run //#formatAnd in dependsOn:
"build": {
"dependsOn": ["//#codegen"] // Root package's codegen
}Overwriting Default Inputs
// WRONG - only watches test files, ignores source changes
"test": {
"inputs": ["tests/**"]
}
// CORRECT - extends defaults, adds test files
"test": {
"inputs": ["$TURBO_DEFAULT$", "tests/**"]
}Without $TURBO_DEFAULT$, you replace all default file watching.
Excluding global.inputs Without $TURBO_DEFAULT$
When using futureFlags.globalConfiguration, global.inputs values are prepended to every task's inputs. If you want to exclude a global input from a specific task, you must include $TURBO_DEFAULT$ to preserve default file hashing.
// WRONG - task hashes NO files at all (global input cancelled, no defaults)
"build": {
"inputs": ["!$TURBO_ROOT$/config.txt"]
}
// CORRECT - task hashes all package files, minus config.txt
"build": {
"inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/config.txt"]
}Without $TURBO_DEFAULT$, the only inclusion glob comes from global.inputs, which the negation cancels out. The task ends up with no inclusions and no default file hashing, so it hashes nothing. Changes to source files won't cause cache misses.
Caching Tasks with Side Effects
// WRONG - deploy might be skipped on cache hit
"deploy": {
"dependsOn": ["build"]
}
// CORRECT
"deploy": {
"dependsOn": ["build"],
"cache": false
}Always disable cache for deploy, publish, or mutation tasks.
turbo.json Configuration Overview
Configuration reference for Turborepo. Full docs: https://turborepo.dev/docs/reference/configuration
File Location
Root turbo.json lives at repo root, sibling to root package.json:
my-monorepo/
├── turbo.json # Root configuration
├── package.json
└── packages/
└── web/
├── turbo.json # Package Configuration (optional)
└── package.jsonAlways Prefer Package Tasks Over Root Tasks
Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.
Package tasks enable parallelization, individual caching, and filtering. Define scripts in each package's package.json:
// packages/web/package.json
{
"scripts": {
"build": "next build",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}
// packages/api/package.json
{
"scripts": {
"build": "tsc",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}// Root package.json - delegates to turbo
{
"scripts": {
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"typecheck": "turbo run typecheck"
}
}When you run turbo run lint, Turborepo finds all packages with a lint script and runs them in parallel.
Root Tasks are a fallback, not the default. Only use them for tasks that truly cannot run per-package (e.g., repo-level CI scripts, workspace-wide config generation).
// AVOID: Task logic in root defeats parallelization
{
"scripts": {
"lint": "eslint apps/web && eslint apps/api && eslint packages/ui"
}
}Basic Structure
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"globalEnv": ["CI"],
"globalDependencies": ["tsconfig.json"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}The $schema key enables IDE autocompletion and validation.
With futureFlags.globalConfiguration
When the globalConfiguration future flag is enabled, global options move under a global key with cleaner names:
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": ["tsconfig.json"],
"env": ["CI"],
"ui": "tui"
},
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}See the global options reference for the full rename mapping and behavior changes.
Configuration Sections
Global options - Settings affecting all tasks:
- Without flag:
globalEnv,globalDependencies,globalPassThroughEnv,cacheDir,daemon,envMode,ui,remoteCache - With
globalConfigurationflag: all of the above move under theglobalkey (see global options)
Task definitions - Per-task settings in tasks object:
dependsOn,outputs,inputs,envcache,persistent,interactive,outputLogs
Package Configurations
Use turbo.json in individual packages to override root settings:
// packages/web/turbo.json
{
"extends": ["//"],
"tasks": {
"build": {
"outputs": [".next/**", "!.next/cache/**"]
}
}
}The "extends": ["//"] is required - it references the root configuration.
When to use Package Configurations:
- Framework-specific outputs (Next.js, Vite, etc.)
- Package-specific env vars
- Different caching rules for specific packages
- Keeping framework config close to the framework code
Extending from Other Packages
You can extend from config packages instead of just root:
// packages/web/turbo.json
{
"extends": ["//", "@repo/turbo-config"]
}Adding to Inherited Arrays with $TURBO_EXTENDS$
By default, array fields in Package Configurations replace root values. Use $TURBO_EXTENDS$ to append instead:
// Root turbo.json
{
"tasks": {
"build": {
"outputs": ["dist/**"]
}
}
}// packages/web/turbo.json
{
"extends": ["//"],
"tasks": {
"build": {
// Inherits "dist/**" from root, adds ".next/**"
"outputs": ["$TURBO_EXTENDS$", ".next/**", "!.next/cache/**"]
}
}
}Without $TURBO_EXTENDS$, outputs would only be [".next/**", "!.next/cache/**"].
Works with:
dependsOnenvinputsoutputspassThroughEnvwith
Excluding Tasks from Packages
Use extends: false to exclude a task from a package:
// packages/ui/turbo.json
{
"extends": ["//"],
"tasks": {
"e2e": {
"extends": false // UI package doesn't have e2e tests
}
}
}turbo.jsonc for Comments
Use turbo.jsonc extension to add comments with IDE support:
// turbo.jsonc
{
"tasks": {
"build": {
// Next.js outputs
"outputs": [".next/**", "!.next/cache/**"]
}
}
}Task Configuration Reference
Full docs: https://turborepo.dev/docs/reference/configuration#tasks
dependsOn
Controls task execution order.
{
"tasks": {
"build": {
"dependsOn": [
"^build", // Dependencies' build tasks first
"codegen", // Same package's codegen task first
"shared#build" // Specific package's build task
]
}
}
}| Syntax | Meaning |
|---|---|
^task | Run task in all dependencies first |
task | Run task in same package first |
pkg#task | Run specific package's task first |
The ^ prefix is crucial - without it, you're referencing the same package.
Transit Nodes for Parallel Tasks
For tasks like lint and check-types that can run in parallel but need dependency-aware caching:
{
"tasks": {
"transit": { "dependsOn": ["^transit"] },
"lint": { "dependsOn": ["transit"] },
"check-types": { "dependsOn": ["transit"] }
}
}DO NOT use `dependsOn: ["^lint"]` - this forces sequential execution. DO NOT use `dependsOn: []` - this breaks cache invalidation.
The transit task creates dependency relationships without running anything (no matching script), so tasks run in parallel with correct caching.
outputs
Glob patterns for files to cache. If omitted, nothing is cached.
{
"tasks": {
"build": {
"outputs": ["dist/**", "build/**"]
}
}
}Framework examples:
// Next.js
"outputs": [".next/**", "!.next/cache/**"]
// Vite
"outputs": ["dist/**"]
// TypeScript (tsc)
"outputs": ["dist/**", "*.tsbuildinfo"]
// No file outputs (lint, typecheck)
"outputs": []Use ! prefix to exclude patterns from caching.
inputs
Files considered when calculating task hash. Defaults to all tracked files in package.
{
"tasks": {
"test": {
"inputs": ["src/**", "tests/**", "vitest.config.ts"]
}
}
}Special values:
| Value | Meaning |
|---|---|
$TURBO_DEFAULT$ | Include default inputs, then add/remove |
$TURBO_ROOT$/<path> | Reference files from repo root |
{
"tasks": {
"build": {
"inputs": [
"$TURBO_DEFAULT$",
"!README.md",
"$TURBO_ROOT$/tsconfig.base.json"
]
}
}
}Interaction with global.inputs
When futureFlags.globalConfiguration is enabled, files listed in global.inputs are prepended to every task's inputs. The combined list is then used to compute the task hash.
This is different from globalDependencies, where files were hashed into the global hash and could not be influenced by task-level inputs.
With `globalDependencies` (old behavior):
globalDependenciesfiles contribute to the global hash- Task
inputsonly control which package files are hashed - There is no way for a task to "opt out" of a
globalDependenciesfile
With `global.inputs` (new behavior):
global.inputsfiles are merged into each task'sinputsglobs- Task
inputsandglobal.inputsare combined, then the full list is hashed into the task hash - Tasks can exclude specific global files with negation globs
{
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": ["tsconfig.json", ".env"]
},
"tasks": {
"build": {},
"lint": {
"inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/.env"]
}
}
}In this example:
buildhashes all package files +tsconfig.json+.env(fromglobal.inputs)linthashes all package files +tsconfig.json, but excludes.envbecause of the negation glob
Tasks with no explicit inputs key still hash all package files (the default behavior) plus the global.inputs files.
env
Environment variables to include in task hash.
{
"tasks": {
"build": {
"env": [
"API_URL",
"NEXT_PUBLIC_*", // Wildcard matching
"!DEBUG" // Exclude from hash
]
}
}
}Variables listed here affect cache hits - changing the value invalidates cache.
cache
Enable/disable caching for a task. Default: true.
{
"tasks": {
"dev": { "cache": false },
"deploy": { "cache": false }
}
}Disable for: dev servers, deploy commands, tasks with side effects.
persistent
Mark long-running tasks that don't exit. Default: false.
{
"tasks": {
"dev": {
"cache": false,
"persistent": true
}
}
}Required for dev servers - without it, dependent tasks wait forever.
interactive
Allow task to receive stdin input. Default: false.
{
"tasks": {
"login": {
"cache": false,
"interactive": true
}
}
}outputLogs
Control when logs are shown. Options: full, hash-only, new-only, errors-only, none.
{
"tasks": {
"build": {
"outputLogs": "new-only" // Only show logs on cache miss
}
}
}with
Run tasks alongside this task. For long-running tasks that need runtime dependencies.
{
"tasks": {
"dev": {
"with": ["api#dev"],
"persistent": true,
"cache": false
}
}
}Unlike dependsOn, with runs tasks concurrently (not sequentially). Use for dev servers that need other services running.
interruptible
Allow turbo watch to restart the task on changes. Default: false.
{
"tasks": {
"dev": {
"persistent": true,
"interruptible": true,
"cache": false
}
}
}Use for dev servers that don't automatically detect dependency changes.
description
Human-readable description of the task.
{
"tasks": {
"build": {
"description": "Compiles the application for production deployment"
}
}
}For documentation only - doesn't affect execution or caching.
passThroughEnv
Environment variables available at runtime but NOT included in cache hash.
{
"tasks": {
"build": {
"passThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"]
}
}
}Warning: Changes to these vars won't cause cache misses. Use env if changes should invalidate cache.
extends (Package Configuration only)
Control task inheritance in Package Configurations.
// packages/ui/turbo.json
{
"extends": ["//"],
"tasks": {
"lint": {
"extends": false // Exclude from this package
}
}
}| Value | Behavior |
|---|---|
true (default) | Inherit from root turbo.json |
false | Exclude task from package, or define fresh without inheritance |
Environment Variable Gotchas
Common mistakes and how to fix them.
.env Files Must Be in inputs
Turbo does NOT read .env files. Your framework (Next.js, Vite, etc.) or dotenv loads them. But Turbo needs to know when they change.
Wrong:
{
"tasks": {
"build": {
"env": ["DATABASE_URL"]
}
}
}Right:
{
"tasks": {
"build": {
"env": ["DATABASE_URL"],
"inputs": ["$TURBO_DEFAULT$", ".env", ".env.local", ".env.production"]
}
}
}Strict Mode Filters CI Variables
In strict mode, CI provider variables (GITHUB_TOKEN, GITLAB_CI, etc.) are filtered unless explicitly listed.
Symptom: Task fails with "authentication required" or "permission denied" in CI.
Solution:
{
"globalPassThroughEnv": ["GITHUB_TOKEN", "GITLAB_CI", "CI"]
}passThroughEnv Doesn't Affect Hash
Variables in passThroughEnv are available at runtime but changes WON'T trigger rebuilds.
Dangerous example:
{
"tasks": {
"build": {
"passThroughEnv": ["API_URL"]
}
}
}If API_URL changes from staging to production, Turbo may serve a cached build pointing to the wrong API.
Use passThroughEnv only for:
- Auth tokens that don't affect output (SENTRY_AUTH_TOKEN)
- CI metadata (GITHUB_RUN_ID)
- Variables consumed after build (deploy credentials)
Runtime-Created Variables Are Invisible
Turbo captures env vars at startup. Variables created during execution aren't seen.
Won't work:
# In package.json scripts
"build": "export API_URL=$COMPUTED_VALUE && next build"Solution: Set vars before invoking turbo:
API_URL=$COMPUTED_VALUE turbo run buildDifferent .env Files for Different Environments
If you use .env.development and .env.production, both should be in inputs.
{
"tasks": {
"build": {
"inputs": [
"$TURBO_DEFAULT$",
".env",
".env.local",
".env.development",
".env.development.local",
".env.production",
".env.production.local"
]
}
}
}Complete Next.js Example
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"globalEnv": ["CI", "NODE_ENV", "VERCEL"],
"globalPassThroughEnv": ["GITHUB_TOKEN", "VERCEL_URL"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": ["DATABASE_URL", "NEXT_PUBLIC_*", "!NEXT_PUBLIC_ANALYTICS_ID"],
"passThroughEnv": ["SENTRY_AUTH_TOKEN"],
"inputs": [
"$TURBO_DEFAULT$",
".env",
".env.local",
".env.production",
".env.production.local"
],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}This config:
- Hashes DATABASEURL and NEXT_PUBLIC\* vars (except analytics)
- Passes through SENTRY_AUTH_TOKEN without hashing
- Includes all .env file variants in the hash
- Makes CI tokens available globally
With futureFlags.globalConfiguration
The same config using the global key. The .env files move to global.inputs, which means they get folded into each task's hash individually rather than the global hash. This lets tasks exclude specific .env files if needed.
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"futureFlags": { "globalConfiguration": true },
"global": {
"env": ["CI", "NODE_ENV", "VERCEL"],
"passThroughEnv": ["GITHUB_TOKEN", "VERCEL_URL"],
"inputs": [".env", ".env.local", ".env.production", ".env.production.local"]
},
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": ["DATABASE_URL", "NEXT_PUBLIC_*", "!NEXT_PUBLIC_ANALYTICS_ID"],
"passThroughEnv": ["SENTRY_AUTH_TOKEN"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}With this approach, a task that doesn't care about .env.production can exclude it:
"lint": {
"inputs": ["$TURBO_DEFAULT$", "!$TURBO_ROOT$/.env.production"]
}This wouldn't have been possible with globalDependencies, where .env.production would be baked into the global hash and affect every task unconditionally.
Environment Modes
Turborepo supports different modes for handling environment variables during task execution.
Strict Mode (Default)
Only explicitly configured variables are available to tasks.
Behavior:
- Tasks only see vars listed in
env,globalEnv,passThroughEnv, orglobalPassThroughEnv - Unlisted vars are filtered out
- Tasks fail if they require unlisted variables
Benefits:
- Guarantees cache correctness
- Prevents accidental dependencies on system vars
- Reproducible builds across machines
# Explicit (though it's the default)
turbo run build --env-mode=strictLoose Mode
All system environment variables are available to tasks.
turbo run build --env-mode=looseBehavior:
- Every system env var is passed through
- Only vars in
env/globalEnvaffect the hash - Other vars are available but NOT hashed
Risks:
- Cache may restore incorrect results if unhashed vars changed
- "Works on my machine" bugs
- CI vs local environment mismatches
Use case: Migrating legacy projects or debugging strict mode issues.
Framework Inference (Automatic)
Turborepo automatically detects frameworks and includes their conventional env vars.
Inferred Variables by Framework
| Framework | Pattern |
|---|---|
| Next.js | NEXT_PUBLIC_* |
| Vite | VITE_* |
| Create React App | REACT_APP_* |
| Gatsby | GATSBY_* |
| Nuxt | NUXT_*, NITRO_* |
| Expo | EXPO_PUBLIC_* |
| Astro | PUBLIC_* |
| SvelteKit | PUBLIC_* |
| Remix | REMIX_* |
| Redwood | REDWOOD_ENV_* |
| Sanity | SANITY_STUDIO_* |
| Solid | VITE_* |
Disabling Framework Inference
Globally via CLI:
turbo run build --framework-inference=falseOr exclude specific patterns in config:
{
"tasks": {
"build": {
"env": ["!NEXT_PUBLIC_*"]
}
}
}Why Disable?
- You want explicit control over all env vars
- Framework vars shouldn't bust the cache (e.g., analytics IDs)
- Debugging unexpected cache misses
Checking Environment Mode
Use --dry to see which vars affect each task:
turbo run build --dry=json | jq '.tasks[].environmentVariables'Environment Variables in Turborepo
Turborepo provides fine-grained control over which environment variables affect task hashing and runtime availability.
Configuration Keys
env - Task-Specific Variables
Variables that affect a specific task's hash. When these change, only that task rebuilds.
{
"tasks": {
"build": {
"env": ["DATABASE_URL", "API_KEY"]
}
}
}globalEnv - Variables Affecting All Tasks
Variables that affect EVERY task's hash. When these change, all tasks rebuild.
{
"globalEnv": ["CI", "NODE_ENV"]
}passThroughEnv - Runtime-Only Variables (Not Hashed)
Variables available at runtime but NOT included in hash. Use with caution - changes won't trigger rebuilds.
{
"tasks": {
"deploy": {
"passThroughEnv": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
}
}
}globalPassThroughEnv - Global Runtime Variables
Same as passThroughEnv but for all tasks.
{
"globalPassThroughEnv": ["GITHUB_TOKEN"]
}Wildcards and Negation
Wildcards
Match multiple variables with *:
{
"env": ["MY_API_*", "FEATURE_FLAG_*"]
}This matches MY_API_URL, MY_API_KEY, FEATURE_FLAG_DARK_MODE, etc.
Negation
Exclude variables (useful with framework inference):
{
"env": ["!NEXT_PUBLIC_ANALYTICS_ID"]
}With futureFlags.globalConfiguration
When the globalConfiguration future flag is enabled, global environment keys move under the global key with cleaner names:
| Old (top-level) | New (global.) |
|---|---|
globalEnv | env |
globalPassThroughEnv | passThroughEnv |
global.env and global.passThroughEnv behave identically to their top-level counterparts — they affect the global hash and all tasks, respectively. The rename is purely organizational.
{
"futureFlags": { "globalConfiguration": true },
"global": {
"env": ["CI", "NODE_ENV"],
"passThroughEnv": ["GITHUB_TOKEN", "NPM_TOKEN"]
},
"tasks": {
"build": {
"env": ["DATABASE_URL", "API_*"],
"passThroughEnv": ["SENTRY_AUTH_TOKEN"]
}
}
}Complete Example
{
"$schema": "https://v2-9-17-canary-1.turborepo.dev/schema.json",
"globalEnv": ["CI", "NODE_ENV"],
"globalPassThroughEnv": ["GITHUB_TOKEN", "NPM_TOKEN"],
"tasks": {
"build": {
"env": ["DATABASE_URL", "API_*"],
"passThroughEnv": ["SENTRY_AUTH_TOKEN"]
},
"test": {
"env": ["TEST_DATABASE_URL"]
}
}
}Common Filter Patterns
Practical examples for typical monorepo scenarios.
Single Package
Run task in one package:
turbo run build --filter=web
turbo run test --filter=@acme/apiPackage with Dependencies
Build a package and everything it depends on:
turbo run build --filter=web...Useful for: ensuring all dependencies are built before the target.
Package Dependents
Run in all packages that depend on a library:
turbo run test --filter=...uiUseful for: testing consumers after changing a shared package.
Dependents Only (Exclude Target)
Test packages that depend on ui, but not ui itself:
turbo run test --filter=...^uiChanged Packages
Run only in packages with file changes since last commit:
turbo run lint --filter=[HEAD^1]Since a specific branch point:
turbo run lint --filter=[main...HEAD]Changed + Dependents (PR Builds)
Run in changed packages AND packages that depend on them:
turbo run build test --filter=...[HEAD^1]Or use the shortcut:
turbo run build test --affectedDirectory-Based
Run in all apps:
turbo run build --filter=./apps/*Run in specific directories:
turbo run build --filter=./apps/web --filter=./apps/apiScope-Based
Run in all packages under a scope:
turbo run build --filter=@acme/*Exclusions
Run in all apps except admin:
turbo run build --filter=./apps/* --filter=!adminRun everywhere except specific packages:
turbo run lint --filter=!legacy-app --filter=!deprecated-pkgComplex Combinations
Apps that changed, plus their dependents:
turbo run build --filter=...[HEAD^1] --filter=./apps/*All packages except docs, but only if changed:
turbo run build --filter=[main...HEAD] --filter=!docsDebugging Filters
Use --dry to see what would run without executing:
turbo run build --filter=web... --dryUse --dry=json for machine-readable output:
turbo run build --filter=...[HEAD^1] --dry=jsonCI/CD Patterns
PR validation (most common):
turbo run build test lint --affectedDeploy only changed apps:
turbo run deploy --filter=./apps/* --filter=[main...HEAD]Full rebuild of specific app and deps:
turbo run build --filter=production-app...Turborepo Filter Syntax Reference
Running Only Changed Packages: --affected
The primary way to run only changed packages is `--affected`:
# Run build/test/lint only in changed packages and their dependents
turbo run build test lint --affectedThis compares your current branch to the default branch (usually main or master) and runs tasks in:
1. Packages with file changes 2. Packages that depend on changed packages (dependents)
Why Include Dependents?
If you change @repo/ui, packages that import @repo/ui (like apps/web) need to re-run their tasks to verify they still work with the changes.
Customizing --affected
# Use a different base branch
turbo run build --affected --affected-base=origin/develop
# Use a different head (current state)
turbo run build --affected --affected-head=HEAD~5Common CI Pattern
# .github/workflows/ci.yml
- run: turbo run build test lint --affectedThis is the most efficient CI setup - only run tasks for what actually changed.
---
Manual Git Comparison with --filter
For more control, use --filter with git comparison syntax:
# Changed packages + dependents (same as --affected)
turbo run build --filter=...[origin/main]
# Only changed packages (no dependents)
turbo run build --filter=[origin/main]
# Changed packages + dependencies (packages they import)
turbo run build --filter=[origin/main]...
# Changed since last commit
turbo run build --filter=...[HEAD^1]
# Changed between two commits
turbo run build --filter=[a1b2c3d...e4f5g6h]Comparison Syntax
| Syntax | Meaning |
|---|---|
[ref] | Packages changed since ref |
...[ref] | Changed packages + their dependents |
[ref]... | Changed packages + their dependencies |
...[ref]... | Dependencies, changed, AND dependents |
---
Other Filter Types
Filters select which packages to include in a turbo run invocation.
Basic Syntax
turbo run build --filter=<package-name>
turbo run build -F <package-name>Multiple filters combine as a union (packages matching ANY filter run).
By Package Name
--filter=web # exact match
--filter=@acme/* # scope glob
--filter=*-app # name globBy Directory
--filter=./apps/* # all packages in apps/
--filter=./packages/ui # specific directoryBy Dependencies/Dependents
| Syntax | Meaning |
|---|---|
pkg... | Package AND all its dependencies |
...pkg | Package AND all its dependents |
...pkg... | Dependencies, package, AND dependents |
^pkg... | Only dependencies (exclude pkg itself) |
...^pkg | Only dependents (exclude pkg itself) |
Negation
Exclude packages with !:
--filter=!web # exclude web
--filter=./apps/* --filter=!admin # apps except adminTask Identifiers
Run a specific task in a specific package:
turbo run web#build # only web's build task
turbo run web#build api#test # web build + api testCombining Filters
Multiple --filter flags create a union:
turbo run build --filter=web --filter=api # runs in both---
Quick Reference: Changed Packages
| Goal | Command |
|---|---|
| Changed + dependents (recommended) | turbo run build --affected |
| Custom base branch | turbo run build --affected --affected-base=origin/develop |
| Only changed (no dependents) | turbo run build --filter=[origin/main] |
| Changed + dependencies | turbo run build --filter=[origin/main]... |
| Since last commit | turbo run build --filter=...[HEAD^1] |
turbo watch
Full docs: https://turborepo.dev/docs/reference/watch
Re-run tasks automatically when code changes. Dependency-aware.
turbo watch [tasks]Basic Usage
# Watch and re-run build task when code changes
turbo watch build
# Watch multiple tasks
turbo watch build test lintTasks re-run in order configured in turbo.json when source files change.
With Persistent Tasks
Persistent tasks ("persistent": true) won't exit, so they can't be depended on. They work the same in turbo watch as turbo run.
Dependency-Aware Persistent Tasks
If your tool has built-in watching (like next dev), use its watcher:
{
"tasks": {
"dev": {
"persistent": true,
"cache": false
}
}
}Non-Dependency-Aware Tools
For tools that don't detect dependency changes, use interruptible:
{
"tasks": {
"dev": {
"persistent": true,
"interruptible": true,
"cache": false
}
}
}turbo watch will restart interruptible tasks when dependencies change.
Limitations
Caching
Caching is experimental with watch mode:
turbo watch your-tasks --experimental-write-cacheTask Outputs in Source Control
If tasks write files tracked by git, watch mode may loop infinitely. Watch mode uses file hashes to prevent this but it's not foolproof.
Recommendation: Remove task outputs from git.
vs turbo run
| Feature | turbo run | turbo watch |
|---|---|---|
| Runs once | Yes | No |
| Re-runs on change | No | Yes |
| Caching | Full | Experimental |
| Use case | CI, one-off | Development |
Common Patterns
Development Workflow
# Run dev servers and watch for build changes
turbo watch dev buildType Checking During Development
# Watch and re-run type checks
turbo watch check-typesRelated skills
Forks & variants (8)
Turborepo has 8 known copies in the catalog totaling 12.1k installs. They canonicalize to this original listing.
- antfu - 11.9k installs
- hairyf - 71 installs
- module-federation - 55 installs
- smithery.ai - 37 installs
- sanity-io - 26 installs
How it compares
Pick turborepo for JS/TS monorepo pipeline and cache tuning instead of generic npm workspace scripts without task graphs.
FAQ
What Turborepo topics does the turborepo skill cover?
The turborepo skill covers turbo.json pipelines, dependsOn graphs, local and remote caching, the turbo CLI with --filter and --affected, environment variables, internal packages, monorepo structure, and boundary debugging for JavaScript/TypeScript workspaces.
Which Turborepo version does the skill reference?
The turborepo skill metadata lists version 2.9.17-canary.1, aligning guidance with the current Turborepo monorepo build-system release line from vercel/turborepo.
Is Turborepo safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.