| description | 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.
|
| metadata | {"version":"2.8.11","skiller":{"source":".agents/rules/turborepo.mdc"}} |
| name | turborepo |
Turborepo Skill
Build system for JavaScript/TypeScript monorepos. Turborepo caches task outputs and runs tasks in parallel based on dependency graph.
Default: Package Tasks; Root Orchestration Is Explicit
Put work in the package that owns it. Use a root task only when the work is
genuinely repository-wide orchestration.
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
package.json delegates package-owned work via turbo run <task>
Do not move package-owned logic into root package.json; that defeats
Turborepo's parallelization.
{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } }
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"lint": {},
"test": { "dependsOn": ["build"] }
}
}
{
"scripts": {
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test"
}
}
{
"scripts": {
"build": "cd apps/web && next build && cd ../api && tsc",
"lint": "eslint apps/ packages/",
"test": "vitest"
}
}
Root orchestration is valid in this repository for tooling/**, fixture and
scenario generation, package build/release composition, dependency pins,
skill/Intent synchronization, and final cross-workspace gates. Preserve the
existing package.json and turbo.json ownership pattern. A root script may
coordinate those owners, but work that belongs to one package stays in that
package.
Type checking is source-first. Do not add package builds merely to make types
resolve unless the package intentionally exposes only built artifacts. Prefer
fixing source entries or path configuration when that is the durable design.
Secondary Rule: turbo run vs turbo
Always use turbo run when the command is written into code:
{
"scripts": {
"build": "turbo run build"
}
}
- run: turbo run build --affected
The shorthand turbo <tasks> is ONLY for one-off terminal commands typed directly by humans or agents. Never write turbo build into package.json, CI, or scripts.
Quick Routing
- Dependencies, outputs, inputs, environment hashing, persistence, and cache
belong in
turbo.json task definitions.
- Package-specific exceptions belong in the owning package configuration.
- Use
--summarize or --dry to inspect hashes and graph selection.
- Use
--affected for changed packages plus dependents; use --filter for an
intentional package/directory/dependency/dependent subset.
- Use
turbo watch, persistent, interruptible, and with for dev/watch
lifecycles.
- Put reusable code in a workspace package with declared dependencies; do not
reach across package directories.
- Treat root orchestration as the explicit kitcn exception described above.
Critical 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.
{
"scripts": {
"build": "turbo build",
"dev": "turbo dev"
}
}
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev"
}
}
- run: turbo build --affected
- run: turbo run build --affected
Root Scripts Bypassing Turbo
Package-owned root scripts delegate to turbo run. Repository-wide kitcn
orchestration may invoke tooling/**, fixture/scenario commands, release
composition, or skill synchronization directly.
{
"scripts": {
"build": "bun build",
"dev": "bun dev"
}
}
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev"
}
}
Using && to Chain Turbo Tasks
Do not chain package-owned Turbo tasks with &&; model their graph in Turbo.
Root release or repository orchestration may sequence unlike tools when the
steps do not represent package task dependencies.
{
"scripts": {
"changeset:publish": "bun build && changeset publish"
}
}
{
"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.
{
"scripts": {
"prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build",
"build": "next build"
}
}
However, the fix depends on whether workspace dependencies are declared:
-
If dependencies ARE declared (e.g., "@repo/types": "workspace:*" in package.json), remove the prebuild script. Turbo's dependsOn: ["^build"] handles this automatically.
-
If dependencies are NOT declared, the prebuild exists because ^build won't trigger without a dependency relationship. The fix is to:
- Add the dependency to package.json:
"@repo/types": "workspace:*"
- Then remove the
prebuild script
{
"dependencies": {
"@repo/types": "workspace:*",
"@repo/utils": "workspace:*"
},
"scripts": {
"build": "next build"
}
}
{
"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. Be specific.
{
"globalDependencies": ["**/.env.*local"]
}
{
"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.
{
"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":
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.
turbo run lint --parallel
turbo run lint
Package-Specific Task Overrides in Root turbo.json
When multiple packages need different task configurations, use Package Configurations (turbo.json in each package) instead of cluttering root turbo.json with package#task overrides.
{
"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":
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
Keep package-specific overrides beside the package when possible.
Using ../ to Traverse Out of Package in inputs
Don't use relative paths like ../ to reference files outside the package. Use $TURBO_ROOT$ instead.
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", "../shared-config.json"]
}
}
}
{
"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:
- 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
{
"tasks": {
"build": {
"dependsOn": ["^build"]
}
}
}
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}
Common outputs by framework:
- Next.js:
[".next/**", "!.next/cache/**"]
- Vite/Rollup:
["dist/**"]
- tsc:
["dist/**"] or custom outDir
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:
{
"tasks": {
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"]
}
}
}
To determine correct outputs for TypeScript tasks:
- Check if
incremental or composite is enabled in tsconfig
- Check
tsBuildInfoFile for custom cache location (default: alongside outDir or in project root)
- If no incremental mode,
tsc --noEmit produces no files
^build vs build Confusion
{
"tasks": {
"build": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"]
},
"deploy": {
"dependsOn": ["web#build"]
}
}
}
Environment Variables Not Hashed
{
"tasks": {
"build": {
"outputs": ["dist/**"]
}
}
}
{
"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:
{
"tasks": {
"build": {
"env": ["API_URL"]
}
}
}
{
"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:
{
"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.ts
Accessing Files Across Package Boundaries
import { Button } from "../../packages/ui/src/button";
import { Button } from "@repo/ui/button";
Too Many Root Dependencies
{
"dependencies": {
"react": "^18",
"next": "^14"
}
}
{
"devDependencies": {
"turbo": "latest"
}
}
Common Task Configurations
Standard Build Pipeline
{
"$schema": "https://turborepo.dev/schema.v2.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:
{
"tasks": {
"dev": {
"dependsOn": ["^dev"],
"cache": false,
"persistent": false
}
}
}
{
"extends": ["//"],
"tasks": {
"dev": {
"persistent": true
}
}
}
Why this works:
- Packages (e.g.,
@acme/db, @acme/validators) have "dev": "tsc" — one-shot type generation that completes quickly
- Apps override with
persistent: true for actual dev servers (Next.js, etc.)
turbo watch re-runs the one-shot package dev scripts when source files change, keeping types in 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"]
}
}
}
Source Documentation
This skill is based on the official Turborepo documentation at: