CircleCI’s path filtering is a surprisingly powerful way to shave minutes, or even hours, off your CI build times by only running jobs that are actually relevant to the code changes.

Imagine you have a monorepo with a frontend, a backend, and a shared library. You’ve set up your CircleCI config to build and test each of these independently. But when you push a change to just the frontend, you don’t want to run the backend tests or the shared library tests. That’s exactly what path filtering solves.

Here’s a simplified config.yml demonstrating path filtering. Notice the filters section under each job.

version: 2.1

jobs:
  build-frontend:
    docker:
      - image: cimg/node:18.17.0
    steps:
      - checkout
      - run: echo "Building frontend..."
      - run: npm install
      - run: npm run build:frontend
    filters:
      branches:
        only: /.*/ # Run on all branches
      paths:
        only:
          - frontend/**

  test-backend:
    docker:
      - image: cimg/go:1.20.5
    steps:
      - checkout
      - run: echo "Testing backend..."
      - run: go test ./...
    filters:
      branches:
        only: /.*/
      paths:
        only:
          - backend/**
          - shared/** # Also run if shared library changes

  test-shared-library:
    docker:
      - image: cimg/python:3.10
    steps:
      - checkout
      - run: echo "Testing shared library..."
      - run: pip install -r requirements.txt
      - run: pytest tests/shared/
    filters:
      branches:
        only: /.*/
      paths:
        only:
          - shared/**

workflows:
  version: 2
  build_and_test:
    jobs:
      - build-frontend
      - test-backend
      - test-shared-library

In this setup:

  • build-frontend will only run if files within the frontend/ directory are changed.
  • test-backend will run if files within backend/ or shared/ are changed. This is because the backend depends on the shared library.
  • test-shared-library will run if files within shared/ are changed.

The core concept is the filters block within a job definition. You can filter by branches, tags, or paths. For path filtering, you use the only or ignore keywords. only means "run this job if any of these paths match." ignore means "run this job unless any of these paths match." You can specify multiple paths, and they are treated as regular expressions.

Let’s break down the mental model: When a commit is pushed, CircleCI inspects the commit’s diff. For each job defined in your workflows, it checks if that job’s filters match the changes in the diff. If the filters pass (e.g., a changed file is within the only paths specified for a job), the job is queued to run. If the filters don’t pass, the job is skipped.

The paths filter uses glob patterns, similar to how you’d match files in your shell. frontend/** means any file or directory nested within frontend/. You can also use frontend/* to match only immediate children.

You can also combine only and ignore within the same paths filter. For example:

      paths:
        only:
          - src/**
        ignore:
          - src/tests/**

This would run a job if any file in src/ changes, unless the change is specifically within src/tests/. This is useful for jobs that test everything but you want to exclude specific test directories from triggering them if only those test files themselves change.

The most common mistake people make is not understanding how only and ignore interact, or how glob patterns work. For instance, shared/** will match shared/utils.go and shared/models/user.go. If you only wanted to match files directly under shared/ and not in subdirectories, you’d use shared/*. Another common oversight is forgetting to include dependencies; if your frontend depends on a shared component, changes to that component should trigger your frontend build job too, requiring a shared/** entry in the frontend job’s path filters.

The next concept you’ll likely explore is using filters with tags to deploy specific versions only when a tag is pushed.

Want structured learning?

Take the full Circleci course →