skip to content

Turborepo (turbo) — CLI and pipeline configuration

Day-to-day Turborepo CLI commands and turbo.json config patterns — pipelines, filters, remote cache, prune, and CI integration.

7 min read 23 snippets deep dive

Turborepo (turbo) — CLI and pipeline configuration#

What it is#

Turborepo is Vercel’s monorepo task runner. It schedules package.json scripts across workspace packages with awareness of dependency topology, hashes every input to skip already-completed work, and ships local + remote caches so CI runners and teammates share build artefacts.

The CLI ships as the turbo npm package — a Rust binary distributed via npm. Pair it with pnpm workspaces (or npm/yarn/bun workspaces) for package management; turbo only handles task orchestration.

Install#

# In the monorepo root
npm install -D turbo
pnpm add -wD turbo            # -w for workspace root
yarn add -DW turbo
bun add -d turbo

# Optional global
npm install -g turbo

# Scaffold a fresh turborepo
npx create-turbo@latest

Output: turbo binary on PATH.

Day-to-day commands#

CommandWhat it does
turbo run buildRun the build script across every package, respecting dependsOn ordering.
turbo run build test lintMultiple tasks in one invocation; Turbo plans the DAG.
turbo run devRun dev scripts in parallel (with persistent: true config).
turbo run build --filter=@my/webOnly the named package.
turbo run build --filter=@my/web...Package and its deps (upstream).
turbo run build --filter=...@my/uiPackage and its dependents (downstream).
turbo run build --filter='[origin/main]'Only packages changed since main.
turbo run build --dryPrint the planned DAG without executing.
turbo run build --forceBypass cache; run everything.
turbo prune --scope=@my/web --dockerOutput a focused subtree for Docker builds.
turbo login / turbo linkSet up Vercel Remote Cache (per-developer).
turbo genRun a code generator (templates in turbo/generators/).
turbo daemon status / stop / restartManage the background file-watcher daemon.

Common scenarios#

Build pipeline with cache#

// turbo.json
{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "package.json", "tsconfig.json"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}
turbo run build

Output (cold):

@my/ui:build:  ✓ built dist/ in 1.2s
@my/api:build: ✓ built dist/ in 0.8s
@my/web:build: ✓ built .next/ in 4.1s

Tasks:  3 successful, 3 total
Cached: 0 cached, 3 total
Time:   5.2s

Output (warm):

Tasks:  3 successful, 3 total
Cached: 3 cached, 3 total
Time:   142ms >>> FULL TURBO

Parallel dev mode#

{
  "tasks": {
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
turbo run dev

Output:

• Packages in scope: api, web, ui
• Running dev in 3 packages
web:dev: ready on http://localhost:3000
api:dev: ready on http://localhost:4000

persistent: true tells Turbo the task is long-running. cache: false skips cache replay. Each package’s dev script (vite, next dev, tsx watch, etc.) starts in parallel.

Remote cache setup#

turbo login                   # one-time browser-based login
turbo link                    # link this repo to a Vercel team

Output:

>>> Opening browser to log in
>>> Success! Turborepo CLI authorized for alice@example.com
✔ Linked to vercel/my-repo

~/.turbo/config.json now holds the token. Subsequent turbo run calls check remote cache first.

In CI:

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: my-team

Self-hosted alternative — the community turborepo-remote-cache proxy supports S3, R2, MinIO.

Filter to changed packages in CI#

turbo run build test lint --filter='[origin/main]'

Output:

• Packages changed since origin/main: @my/web, @my/ui
• Running build, test, lint in 2 packages
 Tasks:    6 successful, 6 total
 Time:     3.1s

Only builds packages touched (directly or via dependencies) by the current branch’s diff against main. Pair with actions/checkout fetch-depth: 0.

Pruning for Docker#

turbo prune --scope=@my/web --docker

Output:

Generating pruned monorepo for @my/web in ./out
 - Added @my/web
 - Added @my/ui
 - Added @my/utils

Outputs an out/ directory:

  • out/json/ — minimal package.json per workspace package needed by @my/web
  • out/full/ — actual source files
  • out/pnpm-lock.yaml (or equivalent) — regenerated lockfile

Standard Dockerfile pattern:

FROM node:20-alpine AS pruner
WORKDIR /app
COPY . .
RUN npx turbo prune --scope=@my/web --docker

FROM node:20-alpine AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build --filter=@my/web

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=installer /app/apps/web/.next/standalone ./
CMD ["node", "server.js"]

Multi-task invocation#

turbo run build test lint

Output:

• Packages in scope: api, web, ui
• Running build, test, lint in 3 packages
 Tasks:    9 successful, 9 total
 Cached:   6 cached, 9 total
 Time:     2.7s

Turbo plans the combined DAG:

  • build tasks run first (respecting ^build)
  • test tasks wait for their package’s build
  • lint runs in parallel with everything

--concurrency=4 caps simultaneous work; default 10.

Generators#

npx turbo gen

Output:

? What generator would you like to run? › react-package
? Package name: new-pkg
✔ packages/new-pkg created

Interactive prompts driven by templates in turbo/generators/config.ts. Useful for “spin up a new internal package with the standard tsconfig + tsup setup”.

Useful flags#

FlagPurpose
--filter=<pattern>Scope to specific packages. Supports ... prefix/postfix for deps/dependents and [ref] for git diffs.
--concurrency=NCap parallel tasks. Default 10.
--forceSkip cache; rebuild everything.
--dry / --dry=jsonPlan without executing. JSON for tooling.
--graph / --graph=graph.pngExport task graph (DOT or PNG via graphviz).
--output-logs=errors-onlyShow only failed-task logs.
--output-logs=hash-onlyShow only cache-hit hashes — minimal output.
--no-daemonDisable the background watcher daemon (debugging).
--profile=profile.jsonEmit Chrome-tracing profile of the run.
--summarize (--summarize=output.json)Write run summary to disk for CI dashboards.
--continueContinue running siblings even after one fails (!fail-fast).

Configuration#

turbo.json schema (v2)#

{
  "$schema": "https://turborepo.com/schema.json",
  "globalDependencies": [".env", "tsconfig.base.json"],
  "globalEnv": ["NODE_ENV", "CI"],
  "globalPassThroughEnv": ["GITHUB_TOKEN"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "package.json", "tsconfig.json"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["NEXT_PUBLIC_API_URL"],
      "outputLogs": "new-only"
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**", "tests/**"],
      "outputs": ["coverage/**"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "deploy": {
      "dependsOn": ["build", "test"],
      "cache": false
    }
  }
}

Key fields:

  • dependsOn — array of task names. ^build means “build of every dependency package”; bare build means “build of this package”.
  • inputs — files that affect the cache hash. Narrow these to avoid spurious cache busts.
  • outputs — files/directories the task produces. Cached as the task output.
  • env — env vars that contribute to the cache hash AND are passed to the task.
  • passThroughEnv — env vars passed to the task but NOT hashed.
  • cachetrue (default) or false. Disable for dev / deploy tasks.
  • persistent — long-running tasks. Cannot have other tasks depend on them.
  • outputLogs"full", "hash-only", "new-only", "errors-only", "none".

Per-package turbo.json overrides#

A package can have its own turbo.json that extends the root:

// packages/web/turbo.json
{
  "extends": ["//"],
  "tasks": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**", "public/sw.js"]
    }
  }
}

Global env declarations#

Any env var your build reads must be declared, OR the cache will be wrong (stripped from the task env by default in v2).

{
  "globalEnv": ["NODE_ENV", "CI", "NEXT_PUBLIC_*"]
}

NEXT_PUBLIC_* wildcards match any var with that prefix.

Common pitfalls#

  1. Missing inputs declaration — tsconfig change doesn’t invalidate cache. Add tsconfig/biome/eslint configs to globalDependencies or per-task inputs.
  2. Undeclared env varprocess.env.MY_VAR is undefined because Turbo strips unknown env. Add to env or passThroughEnv.
  3. pipeline vs tasks (v1 vs v2)pipeline was renamed to tasks in v2. Run npx @turbo/codemod migrate.
  4. Remote cache misses on Node version — different Node versions produce different hashes. Pin Node in CI to match dev.
  5. dependsOn direction confusion^build (with caret) is upstream deps; bare build is “this package’s build first”. Reading the docs slowly the first time saves grief.
  6. outputs: [] for non-output tasks — tests/lint produce no artefacts you want to cache; explicitly set outputs: [] (Turbo will cache “task ran successfully” but no files).
  7. Cache poisoning from PR forks — disable remote cache writes for PRs from forks. Use TURBO_CACHE_DIR=local for fork builds.
  8. Daemon hangsturbo daemon restart clears stuck state. Common after node_modules rebuilds.

See also#