---
title: "Update Turborepo Pipeline"
description: "Configure task dependencies with ^build and ^check-types, add type-checking to packages, understand the dependency graph, and see parallel execution."
canonical_url: "https://vercel.com/academy/production-monorepos/update-turborepo-pipeline"
md_url: "https://vercel.com/academy/production-monorepos/update-turborepo-pipeline.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-11-04T22:27:04.758Z"
content_type: "lesson"
course: "production-monorepos"
course_title: "Production Monorepos with Turborepo"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Update Turborepo Pipeline

# Update Turborepo pipeline

So far, `turbo build` has only ever run two tasks. packages/ui and packages/utils ship TypeScript source with no `build` script, and each app's bundler compiles them on the way through. That keeps setup light, but nothing checks a package's types until an app happens to import the broken file. As your monorepo grows, you need explicit task configuration to control build order, caching, and parallel execution.

We'll give the packages a `check-types` task, wire it into `turbo.json`, and look at how `dependsOn` decides what runs when. This gives you full control over task orchestration.

## Outcome

Understand and configure Turborepo's task pipeline with explicit dependencies.

## Fast track

1. Review current turbo.json configuration
2. Understand ^build and ^lint dependency syntax
3. Visualize the dependency graph
4. Test build and lint with dependency order

## Hands-on exercise 4.3

Configure Turborepo task dependencies for optimal build orchestration.

**Requirements:**

1. Review turbo.json task configuration
2. Understand `dependsOn: ["^build"]` syntax
3. Add a `check-types` script to packages (ui, utils)
4. Add a `check-types` task to turbo.json, then run `turbo check-types` and `turbo build` and observe execution order
5. Run `turbo lint` and see parallel execution
6. Understand when tasks run in parallel vs sequential

**Implementation hints:**

- `^build` means "dependencies' build tasks first"
- Tasks without dependencies run in parallel
- Packages that ship TypeScript source get `check-types`, not `build` ([Just-in-Time Packages](https://turborepo.dev/docs/core-concepts/internal-packages#just-in-time-packages))
- Use --dry flag to see execution plan

## Review current turbo.json

Open `turbo.json`:

```json title="turbo.json"
{
  "$schema": "https://turborepo.dev/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
```

Let's understand what each field means:

**build task:**

- `"dependsOn": ["^build"]` - Run build on all workspace dependencies first
- `"outputs": [".next/**", ...]` - Cache these directories after build

**lint task:**

- `"dependsOn": ["^lint"]` - Lint dependencies before linting this package

**dev task:**

- `"cache": false"` - Never cache dev (it's a watch mode)
- `"persistent": true"` - Keep running after completion

## Understanding dependency syntax

The `^` prefix means "workspace dependencies":

```json
{
  "build": {
    "dependsOn": ["^build"]
  }
}
```

**Translation:**
"Before building this package, first run build on all packages it depends on"

**Example flow for apps/snippet-manager:**

```
apps/snippet-manager depends on:
  - packages/ui
  - packages/utils

Run: turbo build --filter=@geniusgarage/snippet-manager

Execution order:
1. Look for build in packages/ui and packages/utils (neither has one, so nothing runs)
2. Build apps/snippet-manager
```

The packages still count. Turborepo keeps them in the graph as transit nodes, so a change in packages/utils changes snippet-manager's hash even though utils never runs a build.

**Without `^` prefix:**

```json
{
  "build": {
    "dependsOn": ["lint"]  // No ^ prefix
  }
}
```

This means: "Before building, run lint **in the same package**"

## Add check-types scripts to packages

Our first instinct might be to give each package a `build` script that runs `tsc --noEmit`. That works, but it puts a do-nothing step in front of every app build. packages/ui ships source that each app's bundler compiles, so there's nothing for it to build. What it needs is a type-check, and the Turborepo docs call that task `check-types`.

### Packages/ui/package.json

```json title="packages/ui/package.json" {3}
{
  "scripts": {
    "check-types": "tsc --noEmit",
    "lint": "eslint ."
  }
}
```

### Packages/utils/package.json

```json title="packages/utils/package.json" {3}
{
  "scripts": {
    "check-types": "tsc --noEmit",
    "lint": "eslint ."
  }
}
```

Same script. `tsc --noEmit` reads each package's `tsconfig.json` and reports type errors without writing any files.

### Add the task to turbo.json

```json title="turbo.json" {8-10}
{
  "$schema": "https://turborepo.dev/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "check-types": {
      "dependsOn": ["^check-types"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
```

`^check-types` means a package type-checks after the packages it depends on. There are no `outputs`, because type-checking writes nothing. Turborepo still caches the logs, so an unchanged package replays its result.

\*\*Note: Config Packages Don't Need check-types\*\*

Notice we're NOT adding `check-types` to `packages/typescript-config` or `packages/eslint-config`. These packages export static JSON and JavaScript files. They're pure configuration.

Only packages with TypeScript source need `check-types`.

## Try it

### 1. See the execution plan (dry run)

```bash
turbo check-types --dry
```

Output:

```
Tasks to Run
@geniusgarage/ui:check-types
@geniusgarage/utils:check-types
```

Neither package depends on the other, so `^check-types` has nothing to wait for. Both run in **parallel**.

### 2. Type-check, then build

```bash
turbo check-types
```

Output:

```
@geniusgarage/ui:check-types: cache miss, executing 1.402s
@geniusgarage/utils:check-types: cache miss, executing 1.123s

Tasks:    2 successful, 2 total
Cached:   0 cached, 2 total
Time:     1.534s
```

A new `tsconfig.tsbuildinfo` file shows up in packages/utils. That's expected: the shared `base.json` turns on `incremental`, so tsc saves its progress there to make the next check faster. The starter's `.gitignore` already skips these files.

Now build:

```bash
turbo build
```

Output:

```
@geniusgarage/web:build: cache miss, executing 5.123s
@geniusgarage/snippet-manager:build: cache miss, executing 4.891s

Tasks:    2 successful, 2 total
Cached:   0 cached, 2 total
Time:     5.234s
```

**Execution flow:**

1. `turbo check-types` runs ui and utils in **parallel** (type-checking only)
2. `turbo build` runs web and snippet-manager in **parallel** (full Next.js builds)
3. No package task runs during `turbo build`

Both apps missed the cache, and that's expected. We edited `package.json` in packages/ui and packages/utils, and those files feed into the apps' hashes.

### 3. Run build again (see caching)

```bash
turbo build
```

Output:

```
@geniusgarage/web:build: cache hit, replaying logs
@geniusgarage/snippet-manager:build: cache hit, replaying logs

Tasks:    2 successful, 2 total
Cached:   2 cached, 2 total
Time:     195ms >>> FULL TURBO
```

**5s → 195ms** because everything was cached!

### 4. Change a util and rebuild

Edit `packages/utils/src/index.ts`:

```ts title="packages/utils/src/index.ts" {2}
export function formatDate(date: Date): string {
  // Add a comment to invalidate cache
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  }).format(date)
}
```

Run build:

```bash
turbo build
```

Output:

```
@geniusgarage/web:build: cache hit, replaying logs
@geniusgarage/snippet-manager:build: cache miss, executing 4.891s

Tasks:    2 successful, 2 total
Cached:   1 cached, 2 total
Time:     4.987s
```

**Smart caching:**

- web **cached** (doesn't depend on utils)
- snippet-manager **rebuilt** (depends on utils, which changed)

Turborepo detected that `apps/snippet-manager` depends on `packages/utils`, so it rebuilt the app even though the app's code didn't change! packages/utils ran nothing during the build, but its files are still part of snippet-manager's hash. Run `turbo check-types` now and you'll see utils miss while ui replays from cache.

## How Turborepo orchestrates tasks

Your monorepo has this dependency structure:

```
apps/web depends on:
  └─ packages/ui
  └─ packages/typescript-config (devDependency - no build)
  └─ packages/eslint-config (devDependency - no build)

apps/snippet-manager depends on:
  └─ packages/ui
  └─ packages/utils
  └─ packages/typescript-config (devDependency - no build)
  └─ packages/eslint-config (devDependency - no build)

packages/utils depends on:
  └─ packages/typescript-config (devDependency - no build)
  └─ packages/eslint-config (devDependency - no build)

packages/ui depends on:
  └─ packages/typescript-config (devDependency - no build)
  └─ packages/eslint-config (devDependency - no build)
```

\*\*Note: Packages Without a Build Script\*\*

None of the packages have a `build` script, so none of them run anything during `turbo build`. They stay in the graph as transit nodes anyway. When an app depends on a package, including a config package like `typescript-config`, that package's files are part of the app's build hash.

**When you run `turbo build`:**

- apps/web and apps/snippet-manager build in **parallel**
- Each package they depend on runs nothing, but its hash feeds into theirs

**When you run `turbo check-types`:**

- packages/ui and packages/utils type-check in **parallel**
- If one of them depended on the other, `^check-types` would make it wait for that package's check first

Turborepo automatically figures out this order based on workspace dependencies and `^build` configuration!

## Common turbo.json patterns

### Run tests before build

```json
{
  "build": {
    "dependsOn": ["test", "^build"],
    "outputs": [".next/**"]
  }
}
```

This runs tests in the same package, then builds dependencies, then builds the package.

### Cache-only tasks

```json
{
  "test": {
    "cache": true,
    "outputs": ["coverage/**"]
  }
}
```

Tests are cached. Re-run only when source code changes.

### Never cache

```json
{
  "deploy": {
    "cache": false
  }
}
```

Deploy tasks should never be cached.

## Commit

Commit the new scripts and the `check-types` task:

```bash
git add .
git commit -m "chore: add check-types to ui and utils"
```

## Done-when

Verify Turborepo pipeline works:

- [ ] Reviewed turbo.json configuration
- [ ] Understood `^build` syntax means "dependencies first"
- [ ] Understood `^lint` syntax for lint dependencies
- [ ] Added `check-types` script to `packages/ui` (`tsc --noEmit`)
- [ ] Added `check-types` script to `packages/utils` (`tsc --noEmit`)
- [ ] Added a `check-types` task to turbo.json with `^check-types`
- [ ] Understood why config packages don't need `check-types` (static files)
- [ ] Ran `turbo check-types --dry` and saw both packages in the plan
- [ ] Ran `turbo check-types` and saw ui and utils type-check in parallel
- [ ] Ran `turbo build` and saw only the two apps build
- [ ] Ran `turbo build` again and saw full cache hit
- [ ] Changed packages/utils and saw selective rebuild
- [ ] Saw apps/snippet-manager rebuild (depends on changed utils)
- [ ] Understood how Turborepo determines task execution order
- [ ] Understood parallel execution (packages) vs sequential (dependencies)

## What's Next

Section 3 complete! You have:

- 3 shared packages (ui, config, utils)
- Centralized configuration
- Explicit task dependencies
- Smart caching and orchestration

**Section 4: Testing** - Add Vitest to packages/ui, write component tests, configure Turbo for tests, and see test caching in action. You'll prove that testing works seamlessly in monorepos with proper task orchestration.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
