---
title: Scopes with Turborepo
description: Configure merge queue scopes using Turborepo's dependency graph awareness.
---

If you're using monorepo tools like Turborepo that have built-in
dependency graph analysis, you can use their affected project detection instead of file
patterns. This approach is often more accurate because these tools understand your project's
dependency relationships.

## Configuring Manual Scopes

To use the manual scopes mechanism, configure Mergify to expect scopes from your CI system:

```yaml
scopes:
  source:
    manual:

queue_rules:
  - name: default
    batch_size: 5
```

## Detecting Scopes with Turborepo

Use the `turbo run build --dry=json` command to determine affected projects
and upload them to Mergify.

### GitHub Actions

```yaml
name: Detect Scopes
on:
  pull_request:

jobs:
  detect-scopes:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
        with:
          # The queue-aware base commit must exist locally: turbo resolves
          # the base/head range through git.
          fetch-depth: 0

      - name: Get git refs
        id: refs
        uses: Mergifyio/gha-mergify-ci@@@GHA_MERGIFY_CI_VERSION@@
        with:
          action: scopes-git-refs

      - name: Get scopes
        id: scopes
        env:
          HEAD: ${{ steps.refs.outputs.head }}
          BASE: ${{ steps.refs.outputs.base }}
        run: |
          # scopes-git-refs reports an empty base on any event that is neither
          # a pull request nor a push. Diffing against an empty revision, or
          # letting the build tool fall back to its own default base, reports
          # scopes for the wrong range.
          [ -n "$BASE" ] || { echo "no base ref reported" >&2; exit 1; }

          # A failed turbo piped into jq still exits 0 and writes an empty
          # scope list, which the queue reads as "this pull request affects
          # nothing", so capture the dry run first.
          dry=$(npx turbo run build --dry=json --filter="[$BASE...$HEAD]") || exit 1
          scopes=$(printf '%s' "$dry" | jq -r '.packages | join(",")')
          echo "scopes=$scopes" >> "$GITHUB_OUTPUT"

      - name: Scopes upload
        uses: Mergifyio/gha-mergify-ci@@@GHA_MERGIFY_CI_VERSION@@
        with:
          action: scopes-upload
          token: ${{ secrets.MERGIFY_TOKEN }}
          scopes: ${{ steps.scopes.outputs.scopes }}
```

### Buildkite

Using the
[`mergifyio/mergify-ci`](https://github.com/Mergifyio/mergify-ci-buildkite-plugin)
Buildkite plugin, a first step resolves the merge-queue-aware base and head
SHAs and exposes them as meta-data, a second computes the affected projects with
`turbo run build --dry=json` into the `mergify-ci.scopes` meta-data, and a third
uploads them. The upload runs in its own step because the plugin replaces the
step's command, so a step that both detects and uploads never runs its
detection:

```yaml
steps:
  - label: ":mag: Get git refs"
    key: git-refs
    plugins:
      - mergifyio/mergify-ci#@@BUILDKITE_PLUGIN_VERSION@@:
          action: scopes-git-refs

  - label: ":mag: Detect scopes"
    key: detect-scopes
    depends_on: git-refs
    command: |
      # The base meta-data is only published when a base was resolved, so a
      # missing key means there is no base to diff against. Check the value
      # too: an empty one would send the build tool back to its own default
      # base and report scopes for the wrong range.
      BASE=$(buildkite-agent meta-data get "mergify-ci.base") || exit 1
      HEAD=$(buildkite-agent meta-data get "mergify-ci.head") || exit 1
      [ -n "$$BASE" ] || { echo "no base ref reported" >&2; exit 1; }
      # A failed turbo piped into jq still exits 0 and writes an empty scope
      # list, which the queue reads as "this pull request affects nothing",
      # so capture the dry run first.
      DRY=$(npx turbo run build --dry=json --filter="[$$BASE...$$HEAD]") || exit 1
      SCOPES=$(printf '%s' "$$DRY" | jq -r '.packages | join(",")')
      buildkite-agent meta-data set "mergify-ci.scopes" "$$SCOPES"

  - label: ":mag: Upload scopes"
    depends_on: detect-scopes
    plugins:
      - mergifyio/mergify-ci#@@BUILDKITE_PLUGIN_VERSION@@:
          action: scopes-upload
```

The plugin reads your [application key](/api/usage) from `MERGIFY_TOKEN` in
the agent's environment. Without it the step still runs, but it logs a warning
and sends nothing, and the build stays green, so a missing token looks like a
pipeline that reports no scopes.

<BuildkiteTokenNote />

### Any CI (Mergify CLI)

<ScopesDetection
  command={String.raw`# A failed turbo piped into jq still exits 0 and writes a well-formed, empty
# scopes.json, which the queue reads as "this pull request affects nothing".
DRY=$(npx turbo run build --dry=json --filter="[$BASE...$HEAD]") || exit 1
# error() rather than a bare .packages: if turbo ever stops reporting that key,
# jq would write {"scopes": null} and exit 0. This block runs in whatever shell
# your CI uses, so it cannot lean on set -e.
printf '%s' "$DRY" \
  | jq '{scopes: (.packages // error("turbo reported no packages"))}' > scopes.json || exit 1`}
/>
