|
---
name: turborepo
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.9.16-canary.1
---
# 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.
```json
// 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 (`//#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:**
```json
// package.json - ALWAYS "turbo run"
{
"scripts": {
"build": "turbo run build"
}
}
```
```yaml
# CI workflows - ALWAYS "turbo run"
- run: turbo run build --affected
```
**The shorthand `turbo <tasks>` is ONLY for one-off terminal commands** typed directly by humans or agents. Never write `turbo build` into package.json, CI, or scripts.
## Quick Decision Trees
### "I need to configure a task"
```
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.md
```
## 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.
```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 --affected
```
### Root Scripts Bypassing Turbo
Root `package.json` scripts MUST delegate to `turbo run`, not run tasks directly.
```json
// 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
Scripts like `prebuild` that manually build other packages bypass Turborepo's dependency graph.
```json
// WRONG - manually building dependencies
{
"scripts": {
"prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build",
"build": "next build"
}
}
```
**However, the fix depends on whether workspace dependencies are declared:**
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 `prebuild` script
```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:** `^build` only runs build in packages listed as dependencies. No dependency declaration = no automatic build ordering.
### Overly Broad `globalDependencies`
`globalDependencies` affects ALL tasks in ALL packages via the **global hash** — tasks cannot opt out of specific files, even with negation globs in `inputs`. Be specific.
```json
// WRONG - heavy hammer, affects all hashes
{
"globalDependencies": ["**/.env.*local"]
}
// BETTER - move to task-level inputs
{
"globalDependencies": [".env"],
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**"]
}
}
}
```
With `futureFlags.globalConfiguration`, this problem is reduced because `global.inputs` files are folded into each task's inputs (not the global hash). Tasks can exclude specific files:
```json
// 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.
```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` / `globalDependencies` - affects ALL tasks, use for truly shared config
- Task-level `env` / `inputs` - use when only specific tasks need it
### NOT an Anti-Pattern: Large `env` Arrays
A large `env` array (even 50+ variables) is **not** a problem. It usually means the user was thorough about declaring their build's environment dependencies. Do not flag this as an issue.
### Using `--parallel` Flag
The `--parallel` flag bypasses Turborepo's dependency graph. If tasks need parallel execution, configure `dependsOn` correctly instead.
```bash
# WRONG - bypasses dependency graph
turbo run lint --parallel
# CORRECT - configure tasks to allow parallel execution
# In turbo.json, set dependsOn appropriately (or use transit nodes)
turbo run lint
```
### 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.
```json
// 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.
```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
**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
```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: `["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:
```json
// If tsconfig has incremental: true, tsc --noEmit produces cache files
{
"tasks": {
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"] // or wherever tsBuildInfoFile points
}
}
}
```
To determine correct outputs for TypeScript tasks:
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
```json
{
"tasks": {
// ^build = run build in DEPENDENCIES first (other packages this one imports)
"build": {
"dependsOn": ["^build"]
},
// build (no ^) = run build in SAME PACKAGE first
"test": {
"dependsOn": ["build"]
},
// pkg#task = specific package's task
"deploy": {
"dependsOn": ["web#build"]
}
}
}
```
### 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
Turbo does NOT load `.env` files - your framework does. But Turbo needs to know about changes:
```json
// 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:
```json
// If CI scripts need GITHUB_TOKEN but it's not in env:
{
"globalPassThroughEnv": ["GITHUB_TOKEN", "CI"],
"tasks": { ... }
}
```
Or use `--env-mode=loose` (not recommended for production).
### Shared Code in Apps (Should Be a Package)
```
// 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
```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://v2-9-16-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**:
```json
// 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: 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:
```json
{
"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:**
```json
{
"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
```json
{
"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:
```json
{
"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](./references/configuration/RULE.md) | turbo.json overview, Package Configurations |
| [configuration/tasks.md](./references/configuration/tasks.md) | dependsOn, outputs, inputs, env, cache, persistent |
| [configuration/global-options.md](./references/configuration/global-options.md) | globalEnv, globalDependencies, global key, futureFlags, cacheDir, envMode |
| [configuration/gotchas.md](./references/configuration/gotchas.md) | Common configuration mistakes |
### Caching
| File | Purpose |
| --------------------------------------------------------------- | -------------------------------------------- |
| [caching/RULE.md](./references/caching/RULE.md) | How caching works, hash inputs |
| [caching/remote-cache.md](./references/caching/remote-cache.md) | Vercel Remote Cache, self-hosted, login/link |
| [caching/gotchas.md](./references/caching/gotchas.md) | Debugging cache misses, --summarize, --dry |
### Environment Variables
| File | Purpose |
| ------------------------------------------------------------- | ----------------------------------------- |
| [environment/RULE.md](./references/environment/RULE.md) | env, globalEnv, passThroughEnv |
| [environment/modes.md](./references/environment/modes.md) | Strict vs Loose mode, framework inference |
| [environment/gotchas.md](./references/environment/gotchas.md) | .env files, CI issues |
### Filtering
| File | Purpose |
| ----------------------------------------------------------- | ------------------------ |
| [filtering/RULE.md](./references/filtering/RULE.md) | --filter syntax overview |
| [filtering/patterns.md](./references/filtering/patterns.md) | Common filter patterns |
### CI/CD
| File | Purpose |
| --------------------------------------------------------- | ------------------------------- |
| [ci/RULE.md](./references/ci/RULE.md) | General CI principles |
| [ci/github-actions.md](./references/ci/github-actions.md) | Complete GitHub Actions setup |
| [ci/vercel.md](./references/ci/vercel.md) | Vercel deployment, turbo-ignore |
| [ci/patterns.md](./references/ci/patterns.md) | --affected, caching strategies |
### CLI
| File | Purpose |
| ----------------------------------------------- | --------------------------------------------- |
| [cli/RULE.md](./references/cli/RULE.md) | turbo run basics |
| [cli/commands.md](./references/cli/commands.md) | turbo run flags, turbo-ignore, other commands |
### Best Practices
| File | Purpose |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------- |
| [best-practices/RULE.md](./references/best-practices/RULE.md) | Monorepo best practices overview |
| [best-practices/structure.md](./references/best-practices/structure.md) | Repository structure, workspace config, TypeScript/ESLint setup |
| [best-practices/packages.md](./references/best-practices/packages.md) | Creating internal packages, JIT vs Compiled, exports |
| [best-practices/dependencies.md](./references/best-practices/dependencies.md) | Dependency management, installing, version sync |
### Watch Mode
| File | Purpose |
| ------------------------------------------- | ----------------------------------------------- |
| [watch/RULE.md](./references/watch/RULE.md) | turbo watch, interruptible tasks, dev workflows |
### Boundaries (Experimental)
| File | Purpose |
| ----------------------------------------------------- | ----------------------------------------------------- |
| [boundaries/RULE.md](./references/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
don't have the plugin yet? install it then click "run inline in claude" again.
restructured raw guide into implexa's six-part format with explicit decision logic, edge cases, environment setup guidance, and concrete success criteria while preserving original turborepo philosophy.
Turborepo is a build system for JavaScript/TypeScript monorepos that caches task outputs and runs tasks in parallel based on dependency graphs. use this skill when configuring tasks, creating packages, setting up monorepos, sharing code between apps, running changed/affected packages, debugging cache, or structuring apps/packages directories. turborepo lets you define what tasks depend on what, which outputs to cache, which environment variables matter, and how to parallelize work without redundant builds.
turbo.json at the repo root and package.json files in each package.package.json with scripts defined. root package.json should only delegate to turbo run, not contain task logic.dependsOn relationships, cache outputs, environment variables, global settings.--affected flag). requires git initialized and remote configured (typically origin/main as default branch).TURBO_TOKEN and TURBO_TEAM env vars (for Vercel) or custom remoteCache config in turbo.json.minimum viable turbo.json:
{
"$schema": "https://turborepo.org/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"lint": {},
"test": {
"dependsOn": ["build"]
}
}
}
enable remote cache (Vercel):
TURBO_TOKEN env var (from turbo.dev login)TURBO_TEAM env var (your team slug)enable remote cache (custom):
{
"remoteCache": {
"apiUrl": "https://my-cache.example.com",
"signature": true
}
}
git setup:
git to compare branches. requires git initialized and default branch configured (usually origin/main).--affected to work, git must know what changed relative to the default branch. if git is missing, --affected fails.input: desired monorepo layout (which packages are apps, which are libraries/shared code).
output: directory structure with each package containing package.json and source files.
apps/ directory for applications (Next.js, Remix, Express servers, etc.).packages/ directory for shared libraries (UI components, utilities, types, validators, etc.).package.json in each app and package with a name (e.g., "name": "@repo/web", "name": "@repo/ui").package.json (e.g., "build": "tsc", "lint": "eslint .", "test": "vitest").package.json. only add delegation scripts like "build": "turbo run build".example structure:
my-monorepo/
├── turbo.json
├── package.json (root - no task logic)
├── apps/
│ ├── web/
│ │ ├── package.json (scripts: build, dev, test, lint)
│ │ └── src/
│ ├── api/
│ │ ├── package.json (scripts: build, test, lint)
│ │ └── src/
├── packages/
│ ├── ui/
│ │ ├── package.json (scripts: build, lint)
│ │ └── src/
│ ├── types/
│ │ ├── package.json (scripts: build)
│ │ └── src/
input: which packages import from which other packages.
output: package.json dependency declarations and turbo.json dependsOn rules.
package.json, add workspace dependencies to other packages. example:{
"dependencies": {
"@repo/types": "workspace:*",
"@repo/ui": "workspace:*"
}
}
workspace:* protocol (pnpm, Yarn modern, bun) or just "*" (npm with workspaces field in root package.json).package.json. if a package imports from another but doesn't declare it, turbo's ^build won't order tasks correctly.example: if apps/web imports from @repo/types and @repo/ui, it must declare both:
{
"name": "@repo/web",
"dependencies": {
"@repo/types": "workspace:*",
"@repo/ui": "workspace:*"
}
}
input: which tasks exist in packages and what their dependencies are.
output: turbo.json with task definitions, dependsOn relationships, outputs, and environment variables.
tasks object with one entry per task type (build, lint, test, dev, etc.).dependsOn: ["^build"] to run builds in dependencies first.dependsOn: ["build"] or dependsOn: ["^build"] as needed.outputs key for any task that writes files to disk (build, generate, etc.). this tells turbo what to cache.env key if the task reads environment variables (e.g., API_URL, DATABASE_URL). turborepo hashes these to invalidate cache.inputs key if the task should re-run when specific files change (e.g., .env, turbo.json).cache: false and persistent: true for long-running tasks (dev servers, watchers).example turbo.json:
{
"$schema": "https://turborepo.org/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NODE_ENV"]
},
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
input: which environment variables each task needs to read.
output: env keys in task definitions and/or global globalEnv.
globalEnv at root level.env key in the task definition..env files, add inputs: ["$TURBO_DEFAULT$", ".env", ".env.*"] to invalidate cache when those files change. (turborepo does not load .env files; your framework does. but turbo needs to know about changes.).env files in monorepos. instead, create .env files in specific packages that need them.GITHUB_TOKEN), use globalPassThroughEnv to pass them without invalidating cache unnecessarily.example:
{
"globalEnv": ["NODE_ENV"],
"tasks": {
"build": {
"env": ["API_URL", "DATABASE_URL"],
"inputs": ["$TURBO_DEFAULT$", ".env", ".env.local"]
}
}
}
input: which packages to run tasks in (all, changed, specific names, etc.).
output: task execution with correct filtering and caching.
turbo run build.turbo run build --affected.turbo run build --filter=web.turbo run build --filter=...web.turbo run build --filter=web....turbo run (not shorthand turbo). in CI pipelines, always use turbo run.turbo build shorthand is acceptable but turbo run build is more explicit.examples:
# all packages
turbo run build
# changed packages + dependents
turbo run build --affected
# specific package
turbo run build --filter=@repo/web
# package + dependencies
turbo run build --filter=@repo/web...
# package + dependents
turbo run build --filter=...@repo/web
input: cache misses, unexpected rebuilds, or performance issues.
output: corrected turbo.json or resolved cache problems.
turbo run build --dry to see what turbo plans to run without executing.turbo run build --summarize to see cache hit/miss reasons.outputs key missing or wrong? (file-producing tasks must declare outputs)env missing a variable that affects the build?inputs missing a file that should invalidate cache (e.g., .env, package.json)?globalDependencies too broad, invalidating all tasks?env.turbo run build --force.cache: false in turbo.json.input: CI provider (GitHub Actions, GitLab CI, etc.) and whether to use remote cache.
output: CI workflow that runs turbo with --affected and remote cache.
turbo run build --affected to run only changed packages.TURBO_TOKEN and TURBO_TEAM (or custom remoteCache config).CI=true) are auto-detected by turbo and skip local caching..github/workflows/ci.yml:name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: turbo run build --affected
- run: turbo run lint --affected
- run: turbo run test --affected
turbo-ignore (turbo's built-in tool for detecting which packages changed).input: which packages should be allowed to import from which other packages (architectural rules).
output: boundary configuration in turbo.json and CI checks.
boundaries key to root turbo.json with rules.package.json (e.g., "turbo": { "tags": ["scope:ui", "type:library"] }).turbo boundaries in CI to check for violations.example:
{
"boundaries": {
"rules": [
{
"from": ["tag:scope:api"],
"to": ["tag:scope:ui"],
"allow": false
}
]
}
}
then add an outputs key listing the files or globs (e.g., "outputs": ["dist/**"]). without this, turbo caches nothing and re-runs the task every time.
else (task only outputs to stdout or is a check/linter), leave outputs empty or omit it.
then add an env key listing the variables (e.g., "env": ["API_URL", "DATABASE_URL"]). turborepo hashes these to invalidate cache when they change.
else (task is deterministic and doesn't read env), omit env.
then add dependsOn: ["^build"] to run builds in dependencies first.
else (task is standalone or only needs source), omit or use dependsOn: [].
then use turbo run build --affected. turborepo detects which files changed relative to the default branch and runs only affected packages plus their dependents.
else (run everything), use turbo run build with no filter.
then set cache: false and persistent: true. turborepo will not cache the output and will keep the process running.
else (cacheable task), omit these keys (defaults are cache: true, persistent: false).
then move variables to package-specific .env files and remove the root .env. if you must share variables, use globalEnv to be explicit.
else (each package has its own .env as needed), no change needed.
cd apps/web && npm run build && cd ../api && npm run build)then refactor to use turbo run with proper dependsOn relationships. turbo orchestrates the order; you don't need manual sequencing.
else (task delegates to turbo), no change needed.
turbo shorthand in package.json or CI scriptsthen change to turbo run <task>. the shorthand is only for interactive terminal commands.
else (using turbo run in scripts), no change needed.
then use Package Configurations (create a turbo.json in each package) instead of cluttering root turbo.json with package#task overrides.
else (all packages share the same task config), keep config in root turbo.json.
then use a Transit Node pattern: define an empty task (e.g., transit) with dependsOn: ["^transit"] and have other tasks depend on it. this creates dependency relationships without requiring built outputs.
else (standard sequential execution), use dependsOn: ["^build"] directly.
then add them to globalPassThroughEnv so they're passed through without invalidating cache unnecessarily.
else (no special CI variables), omit this key.
success looks like:
turbo.json is valid JSON with no syntax errors. it must contain at minimum a tasks object with task definitions.
all packages have package.json files with:
name field (e.g., "@repo/web")scripts field with task definitions (e.g., "build": "tsc", "lint": "eslint .")"dependencies": { "@repo/ui": "workspace:*" })root package.json contains only:
"workspaces": ["apps/*", "packages/*"])turbo run <task> (e.g., "build": "turbo run build")turbo.json task definitions include:
dependsOn for build tasks (e.g., ["^build"]) or empty array/omitted if standaloneoutputs for file-producing tasks (globs like ["dist/**"])env if the task reads environment variablesinputs if the task should re-run when specific files changecache: false and persistent: true for long-running tasks onlyenvironment variables are declared in env or globalEnv keys, not left implicit. .env files are in individual packages, not at the root.
filtering works correctly: turbo run build --affected detects only changed packages and their dependents. turbo run build --filter=web runs only the specified package.
cache is working: running the same task twice produces "cache hit" output (visible with --summarize). removing cache with --force and re-running completes successfully.
CI integration is set up with remote cache enabled (if using Vercel or custom backend) and --affected flag used to optimize CI runs.
no task logic in root package.json. all scripts delegate to turbo (e.g., "build": "turbo run build").
no relative paths (../) in turbo.json inputs/outputs. use $TURBO_ROOT$ instead.
the user knows the skill worked when:
turbo run build completes successfully and outputs "cache hit" on the second run without changing any code.
turbo run build --affected runs only the packages that changed plus their dependents, not the entire monorepo. visible in the output log showing filtered package list.
turbo run lint && turbo run test execute in parallel (if configured to do so) and complete faster than sequential runs.
environment variable changes (e.g., modifying API_URL in .env) cause the cache to invalidate and tasks to re-run. visible with --summarize showing cache miss reason is env change.
CI passes with --affected flag and completes faster than running all tasks. remote cache is populated (visible in Vercel dashboard or custom cache logs).
turbo boundaries check (if configured) catches invalid imports between packages and fails CI.
no "task not found" errors when running turbo run <taskname>. all packages have the script defined in their package.json.
dev servers and watchers don't hang when using cache: false and persistent: true. processes stay alive and respond to file changes.
--filter syntax works intuitively: --filter=web runs web, --filter=...web runs web and its dependents, --filter=web... runs web and its dependencies.
monorepo structure is clear: apps/ contains applications, packages/ contains libraries, no shared code inside an app, no circular imports.