Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Compare the Nx approach

Turborepo Pipeline Configuration

A Turborepo pipeline is a declarative task graph: you describe how each task depends on others, what it consumes, and what it produces, and Turborepo computes a topological execution order, a content hash per task, and a caching boundary for free. Get the declaration right and incremental builds skip everything that has not changed; get it wrong and you face perpetual cache misses, MODULE_NOT_FOUND errors from out-of-order builds, or leaked secrets. This page covers the full turbo.json schema, the dependsOn graph semantics, deterministic inputs/outputs hashing, environment-variable scoping, and CI integration.

The pipeline definition is the contract every other piece of your Monorepo Architecture & Orchestration setup depends on. It is also what feeds the cache: the inputs and outputs you declare here are exactly what your Remote Caching Setup stores and keys on, so a sloppy glob undermines caching across every machine. Before committing to turbo.json syntax at all, weigh it against the alternatives in Choosing a Monorepo Task Runner; the rest of this page assumes you have settled on Turborepo.

Turborepo task graph A build task depends on upstream builds via caret-build, consumes declared inputs, and emits cached outputs that feed test and lint. inputs src, tsconfig ^build upstream deps build task hash = inputs + deps outputs dist (cached)
A build task hashes its inputs plus upstream dependency hashes, then emits cacheable outputs that downstream tasks consume.

The problem statement

Turborepo only goes as fast as your declarations are honest. Every task needs three answers: what must run before it (dependsOn), what changes its result (inputs and env), and what it leaves behind (outputs). Miss any one and you get a wrong answer — a stale build, a non-deterministic hash, or a cache that never hits. The sections below make each answer explicit.

Core turbo.json schema and initialization

Initialize the pipeline at the repository root and bind the schema so your editor validates structure and autocompletes fields.

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [".env"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "inputs": ["src/**/*.ts", "package.json", "tsconfig.json"],
      "env": ["NODE_ENV"]
    },
    "lint": {
      "dependsOn": [],
      "outputs": []
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.ts", "test/**/*.ts"],
      "outputs": ["coverage/**"]
    }
  }
}

Key directives:

  • $schema binds to the published Turborepo schema for editor validation.
  • tasks defines the execution graph. Turborepo v2 renamed pipeline to tasks; on v1 the same object is named pipeline.
  • globalDependencies lists files that invalidate the entire cache when they change. Use it sparingly — only for truly global config like a root .env.

Task dependency graphs (dependsOn)

Turborepo builds its execution order from explicit dependsOn arrays. Unlike Nx Workspace Architecture, which infers much of the graph from project targets, Turborepo asks you to state the edges directly.

Syntax Behavior Use case
"^build" Run build in all upstream workspace dependencies first Cross-package compilation chains
"build" Run build in the current workspace only Self-contained or sibling tasks
"$TURBO_DEFAULT$" Inherit the default task config when extending Reducing boilerplate in large repos
# Visualize the resolved execution order without running anything
turbo run build --dry=json | jq '.tasks[].taskId'

# Emit the dependency graph as a DOT file
turbo run build --graph=graph.dot

The caret matters. Omitting ^ on a task that depends on shared libraries lets downstream packages run before their dependencies compile, producing MODULE_NOT_FOUND errors or stale type definitions.

Persistent and interactive tasks

Not every task produces an artifact. A dev server runs forever; a watcher never exits. Turborepo needs to know this so it does not wait for the task to finish or try to cache its (nonexistent) output. Mark such tasks persistent and disable caching.

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

A persistent: true task cannot be a dependency of another task — Turborepo refuses to build a graph where a never-ending task blocks a downstream one, which catches the common mistake of listing dev in another task's dependsOn. Pair this with --continue in CI for batch tasks and reserve persistent tasks for local development entrypoints.

Cache hashing and deterministic outputs

A task's cache boundary is defined by inputs (what triggers a rebuild) and outputs (what gets stored). Misconfigured globs cause either cache bloat or perpetual misses.

# Scope execution to packages affected since the last commit
pnpm exec turbo run build --filter='...[HEAD^1]'

# Inspect per-task hashes and hit/miss state
turbo run build --log-order=stream --output-logs=hash-only

Glob rules:

  • outputs must capture only deterministic artifacts; always exclude volatile directories such as .next/cache, node_modules, and .turbo (use a !-prefixed glob).
  • inputs should be restricted to source files. Avoid **/*, which sweeps lockfiles, CI metadata, and editor cruft into the hash.
  • Pairing the pipeline with pnpm Workspace Filtering lets you invalidate and rebuild only the packages a change touches, cutting CI cost on incremental pull requests.

Environment variable security and passthrough

Turborepo does not inherit host shell variables into the hash by default — you declare them. This keeps cache keys stable and prevents secrets from silently entering an artifact.

Field Scope Cache impact Security posture
env Task-level Changes invalidate only that task Recommended for API keys, feature flags
globalEnv Repository-wide Changes invalidate every task Reserve for compiler flags (CC, CXX)
passThroughEnv Task-level Passes host vars without hashing them Never for secrets; breaks determinism
{
  "tasks": {
    "deploy": {
      "dependsOn": ["build"],
      "env": ["AWS_REGION", "DEPLOY_ENV"],
      "outputs": []
    }
  }
}

Never declare TURBO_TOKEN, NPM_TOKEN, or GITHUB_TOKEN in globalEnv; scope deployment credentials to the deploy task only. Strict environment scoping correlates directly with cache-hit stability, as quantified in Nx vs Turborepo Performance Benchmarks.

Per-package overrides and configuration inheritance

A single root turbo.json is the simplest layout, but real monorepos have packages with genuinely different build shapes — an app that emits .next/**, a library that emits dist/**, a docs site that emits build/**. Turborepo lets a package ship its own turbo.json that extends the root, overriding only the fields that differ. This keeps the root definition the shared baseline rather than a dumping ground of special cases.

// packages/web/turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "extends": ["//"],
  "tasks": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"]
    }
  }
}

The "extends": ["//"] line points at the root configuration; the build block here merges over the root's build, replacing its outputs while inheriting dependsOn and inputs. Use this sparingly — every override is a place where the build behaves differently per package, so prefer a consistent root definition and reach for overrides only when a package's artifact layout truly differs.

Output log modes

How much a task prints is itself a tuning knob, because in CI the log volume becomes ingestion cost and signal-to-noise. The --output-logs flag controls what a cached task replays and what a fresh task streams.

Mode Behavior When to use
full Replay all task output, cached or not Local debugging
hash-only Print only the task hash and status Verifying cache behavior
new-only Show output only for tasks that actually ran Default for readable CI
errors-only Show output only for failed tasks High-volume mainline CI
# Readable CI: only freshly-run tasks print, cached ones stay quiet
turbo run build --output-logs=new-only

Build orchestration and scripts

A clean pipeline pairs with clean scripts. The repository root typically exposes thin wrappers (turbo run build, turbo run test) that fan out to per-package scripts, rather than duplicating logic. Deciding what lives at the root versus inside each package is its own discipline — see Root-Level vs Package-Level Scripts for the division that keeps turbo.json readable.

// package.json (root)
{
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint"
  }
}

CI/CD pipeline integration and remote caching

Run pipelines with explicit concurrency, remote-cache authentication, and change-based filtering.

# .github/workflows/ci.yml
name: ci
on:
  push:
    branches: [main]
  pull_request:

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  # required for --filter to compute a diff
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - name: Build and test affected
        run: |
          pnpm exec turbo run build test \
            --concurrency=4 \
            --filter='...[origin/main]'
        timeout-minutes: 15

Production flags worth pinning:

Flag Purpose CI recommendation
--force Bypass the cache Debugging only; never in mainline CI
--filter Target a subset of workspaces '...[HEAD^1]' on PRs, '...[origin/main]' on main
--concurrency Bound parallel workers Set to the runner vCPU count to avoid OOM
--output-logs=errors-only Trim log volume Reduce ingestion cost in CI

How inputs, globalDependencies, and the lockfile compose

The single biggest source of confusion is which files actually feed a task's hash, because three different mechanisms contribute and they stack. A task's hash folds together: the hashed contents of every file matched by that task's inputs (or, if inputs is omitted, every committed file in the package); the contents of every file in globalDependencies; the resolved values of the variables in env and globalEnv; the hashes of the upstream tasks named by dependsOn; and the package manager lockfile, which Turborepo always factors in so a dependency bump invalidates correctly.

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["tsconfig.base.json", ".env"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", "!**/*.test.ts"],
      "outputs": ["dist/**"]
    }
  }
}

Two refinements are worth knowing. $TURBO_DEFAULT$ inside an inputs array means "the default set of committed files, plus the extra patterns I list," so you can exclude test files from a build's hash without re-enumerating the whole source tree. And anything in globalDependencies invalidates every task in the repo, which is why a root tsconfig.base.json belongs there but a package-specific config does not — putting too much in the global list quietly defeats incremental caching across the entire workspace.

Common pitfalls and mitigation

Mistake Impact Resolution
Omitting outputs arrays 0% hit rate; artifacts regenerated every run Declare exact globs (dist/**, build/**)
globalEnv for credentials Secrets exposed to all tasks; full invalidation on rotation Move to task-level env
Implicit shell env inheritance Non-deterministic builds across runners Declare every required var in turbo.json
Missing ^ in dependsOn Downstream runs before upstream compiles Use "^task" for workspace deps
Caching volatile dirs Payload bloat; slow uploads Exclude with a !-prefixed outputs glob

Frequently Asked Questions

What is the difference between env and globalEnv in turbo.json? env scopes a variable to one task, so its cache key changes only when that variable changes; globalEnv applies to every task and invalidates the whole cache when any listed variable changes. Use env for task-specific configuration and reserve globalEnv for truly global compiler flags.

How does Turborepo handle cross-package dependencies during execution? The ^ prefix in dependsOn (such as "^build") tells Turborepo to run the named task in every upstream workspace dependency before the current one, producing a strict topological order without manual script chaining or && operators.

Why does my task rebuild every time even though nothing changed? Either outputs is missing — so there is nothing to restore — or inputs is too broad and is hashing a file that changes on every run. Narrow inputs to source files and confirm outputs captures the real artifact directory.

Can I use Turborepo with non-JavaScript toolchains? Yes. Turborepo operates on filesystem outputs and declared environment variables, so it is language-agnostic. Point outputs and inputs at your toolchain's artifact and source paths (for example target/ for Rust) and exclude temp directories to keep the hash deterministic.

Related

Monorepo Architecture & Orchestration