turborepo
Original:🇺🇸 English
Not Translated
Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines, dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment variables, internal packages, monorepo structure/best practices, and boundaries. Use when user: configures tasks/workflows/pipelines, creates packages, sets up monorepo, shares code between apps, runs changed/affected packages, debugs cache, or has apps/packages directories.
2installs
Sourcesecondsky/claude-skills
Added on
NPX Install
npx skill4agent add secondsky/claude-skills turborepoSKILL.md Content
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
DO NOT create Root Tasks. ALWAYS create package tasks.
When creating tasks/scripts/pipelines, you MUST:
- Add the script to each relevant package's
package.json - Register the task in root
turbo.json - Root only delegates via
package.jsonturbo run <task>
DO NOT put task logic in root . This defeats Turborepo's parallelization.
package.jsonjson
// 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" } }json
// turbo.json - register tasks
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"lint": {},
"test": { "dependsOn": ["build"] }
}
}json
// Root package.json - ONLY delegates, no task logic
{
"scripts": {
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test"
}
}json
// DO NOT DO THIS - defeats parallelization
// Root package.json
{
"scripts": {
"build": "cd apps/web && next build && cd ../api && tsc",
"lint": "eslint apps/ packages/",
"test": "vitest"
}
}Root Tasks () are ONLY for tasks that truly cannot exist in packages (rare).
//#tasknameSecondary Rule: turbo run
vs turbo
turbo runturboAlways use when the command is written into code:
turbo runjson
// package.json - ALWAYS "turbo run"
{
"scripts": {
"build": "turbo run build"
}
}yaml
# CI workflows - ALWAYS "turbo run"
- run: turbo run build --affectedThe shorthand is ONLY for one-off terminal commands typed directly by humans or agents. Never write into package.json, CI, or scripts.
turbo <tasks>turbo buildQuick 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"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
turboturbo runturbo <task>json
// WRONG - using shorthand in package.json
{
"scripts": {
"build": "turbo build",
"dev": "turbo dev"
}
}
// CORRECT
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev"
}
}yaml
# WRONG - using shorthand in CI
- run: turbo build --affected
# CORRECT
- run: turbo run build --affectedRoot Scripts Bypassing Turbo
Root scripts MUST delegate to , not run tasks directly.
package.jsonturbo runjson
// 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.
&&json
// WRONG - turbo task not using turbo run
{
"scripts": {
"changeset:publish": "bun build && changeset publish"
}
}
// CORRECT
{
"scripts": {
"changeset:publish": "turbo run build && changeset publish"
}
}prebuild
Scripts That Manually Build Dependencies
prebuildScripts like that manually build other packages bypass Turborepo's dependency graph.
prebuildjson
// WRONG - manually building dependencies
{
"scripts": {
"prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build",
"build": "next build"
}
}However, the fix depends on whether workspace dependencies are declared:
-
If dependencies ARE declared (e.g.,in package.json), remove the
"@repo/types": "workspace:*"script. Turbo'sprebuildhandles this automatically.dependsOn: ["^build"] -
If dependencies are NOT declared, theexists because
prebuildwon't trigger without a dependency relationship. The fix is to:^build- Add the dependency to package.json:
"@repo/types": "workspace:*" - Then remove the script
prebuild
- Add the dependency to package.json:
json
// 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: only runs build in packages listed as dependencies. No dependency declaration = no automatic build ordering.
^buildOverly Broad globalDependencies
globalDependenciesglobalDependenciesjson
// 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/**"]
}
}
}Repetitive Task Configuration
Look for repeated configuration across tasks that can be collapsed. Turborepo supports shared configuration patterns.
json
// 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- affects ALL tasks, use for truly shared configglobalDependencies - Task-level /
env- use when only specific tasks need itinputs
NOT an Anti-Pattern: Large env
Arrays
envA large 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.
envUsing --parallel
Flag
--parallelThe flag bypasses Turborepo's dependency graph. If tasks need parallel execution, configure correctly instead.
--paralleldependsOnbash
# 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 ( in each package) instead of cluttering root with overrides.
turbo.jsonturbo.jsonpackage#taskjson
// 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 to inherit + extend arrays
$TURBO_EXTENDS$
When to use in root:
package#task- Single package needs a unique dependency (e.g., )
"deploy": { "dependsOn": ["web#build"] } - Temporary override while migrating
See for full details.
references/configuration/RULE.md#package-configurationsUsing ../
to Traverse Out of Package in inputs
../inputsDon't use relative paths like to reference files outside the package. Use instead.
../$TURBO_ROOT$json
// 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
outputsBefore flagging missing , check what the task actually produces:
outputs- Read the package's script (e.g., ,
"build": "tsc")"test": "vitest" - Determine if it writes files to disk or only outputs to stdout
- Only flag if the task produces files that should be cached
json
// 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: or custom
["dist/**"]outDir
TypeScript can still produce cache files:
--noEmitWhen in tsconfig.json, writes files even without emitting JS. Check the tsconfig before assuming no outputs:
incremental: truetsc --noEmit.tsbuildinfojson
// If tsconfig has incremental: true, tsc --noEmit produces cache files
{
"tasks": {
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"] // or wherever tsBuildInfoFile points
}
}
}To determine correct outputs for TypeScript tasks:
- Check if or
incrementalis enabled in tsconfigcomposite - Check for custom cache location (default: alongside
tsBuildInfoFileor in project root)outDir - If no incremental mode, produces no files
tsc --noEmit
^build
vs build
Confusion
^buildbuildjson
{
"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
json
// 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
.envTurbo does NOT load files - your framework does. But Turbo needs to know about changes:
.envjson
// 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
.envA 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.
.env// 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 to be explicit about what's shared, and document why.
globalEnvStrict Mode Filtering CI Variables
By default, Turborepo filters environment variables to only those in /. CI variables may be missing:
envglobalEnvjson
// If CI scripts need GITHUB_TOKEN but it's not in env:
{
"globalPassThroughEnv": ["GITHUB_TOKEN", "CI"],
"tasks": { ... }
}Or use (not recommended for production).
--env-mode=looseShared 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
typescript
// 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
json
// 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
json
{
"$schema": "https://turborepo.dev/schema.v2.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}Add a task if you have tasks that need parallel execution with cache invalidation (see below).
transitDev Task with ^dev
Pattern (for turbo watch
)
^devturbo watchA task with and in root turbo.json may look unusual but is correct for workflows:
devdependsOn: ["^dev"]persistent: falseturbo watchjson
// 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) have@acme/validators— one-shot type generation that completes quickly"dev": "tsc" - Apps override with for actual dev servers (Next.js, etc.)
persistent: true - re-runs the one-shot package
turbo watchscripts when source files change, keeping types in syncdev
Intended usage: Run (not ). Watch mode re-executes one-shot tasks on file changes while keeping persistent tasks running.
turbo watch devturbo run devAlternative pattern: Use a separate task name like or for one-shot dependency builds to make the intent clearer:
preparegeneratejson
{
"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 (no dependencies):
dependsOn: []- Allows parallel execution (fast)
- But cache is INCORRECT - changing dependency source won't invalidate cache
Transit Nodes solve both:
json
{
"tasks": {
"transit": { "dependsOn": ["^transit"] },
"my-task": { "dependsOn": ["transit"] }
}
}The task creates dependency relationships without matching any actual script, so tasks run in parallel with correct cache invalidation.
transitHow 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
json
{
"globalEnv": ["NODE_ENV"],
"globalDependencies": [".env"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["API_URL", "DATABASE_URL"]
}
}
}When to Load References
Load references/configuration/
when:
references/configuration/- RULE.md: Need overview of turbo.json structure
- tasks.md: Defining task configuration (dependsOn, outputs, inputs, env, cache)
- global-options.md: Setting up globalEnv, globalDependencies, cacheDir, daemon
- gotchas.md: Encountering unexpected configuration behavior
Load references/caching/
when:
references/caching/- RULE.md: Understanding how cache hashing works
- remote-cache.md: Setting up Vercel Remote Cache or self-hosted cache
- gotchas.md: Debugging cache misses or using --summarize/--dry flags
Load references/environment/
when:
references/environment/- RULE.md: Configuring environment variables (env, globalEnv, passThroughEnv)
- modes.md: Choosing strict vs loose mode, framework inference
- gotchas.md: Troubleshooting .env files or CI environment issues
Load references/filtering/
when:
references/filtering/- RULE.md: Need --filter syntax overview
- patterns.md: Building complex filter patterns for selective runs
Load references/ci/
when:
references/ci/- RULE.md: General CI principles and caching strategies
- github-actions.md: Complete GitHub Actions workflow setup
- vercel.md: Vercel deployment setup with turbo-ignore
- patterns.md: Implementing --affected flag patterns and optimization
Load references/cli/
when:
references/cli/- RULE.md: Basic turbo run command usage
- commands.md: Complete CLI reference including turbo-ignore and flags
Load references/best-practices/
when:
references/best-practices/- RULE.md: Monorepo structure and organization overview
- structure.md: Repository layout, workspace config, TypeScript/ESLint setup
- packages.md: Creating internal packages, JIT vs Compiled strategies, exports
- dependencies.md: Dependency management patterns, version sync
Load references/watch/
when:
references/watch/- RULE.md: Setting up turbo watch for development workflows
Load references/boundaries/
when:
references/boundaries/- RULE.md: Enforcing package isolation with experimental boundaries feature
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, cacheDir, daemon, 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: in the Turborepo repository
apps/docs/content/docs/ - Live: https://turborepo.dev/docs