DYNAMIC CI · BETA

Skip the CI jobs you don't need.

Dynamic CI decides run or skip for every job on every push, from your own CI history. Your merge queue still runs every job, so every commit that lands is fully tested.

Works with GitHub Actions and Buildkite.

Checks
Every job runs on every push
7 jobs, 3 runners
lint
unit-tests
integration-tests
migrations-check
e2e-web
docker-build
perf-bench
0s1m2m3m4m5m6m7m8m
Smaller CI bill
Less time waiting
Automatic and configurable
Every skip is validated

Your CI runs every job on every push.

A push touches three files, but your pipeline runs every job anyway.

You pay for jobs that add no signal.

Docs changes run the integration suite, and jobs that haven't failed in months run on one-line fixes.

Every push restarts the wait.

Fix a typo after review and you wait on the slowest job again, not the one that mattered.

Stacked PRs pay for CI over and over.

Fix the bottom of a stack and every PR above it reruns its full pipeline, re-checking code that didn't change.

Agents multiply the waste.

Coding agents open more PRs, push more often, and lean on stacks, so your CI bill grows with them, not your headcount.

HOW IT DECIDES

One verdict per job, with reasoning.

On every push, Dynamic CI scores each job against what changed and what that job has done before, then returns run or skip. Follow one PR from first push to merge.

One PR, first push to merge
RunSkippedFailedPinned
Merge queue: nothing skipped
Job
PR opened
Typo fix pushed
PR stacked on top
Merge queue
Fix pushed
unit-tests
integration-tests
e2e-web
migrations-check
integration-tests skipped: nothing this PR touched has broken it, and it rarely fails.
SKIP VALIDATION

Every job passes six checks before it is skipped.

Your merge queue skips nothing.

The commit that lands is fully tested. The only jobs skipped in the queue are ones your own path filters exclude.

Any job can be required.

Set any job to always run and it never skips. Developers can also force a skipped job back on from the PR, with no job change.

A job that just failed always runs.

If it rejected a PR from the queue, it runs again, whatever the score says.

New jobs always run.

No history, no skip. A job needs a couple of days of data before Dynamic CI will skip it.

Your call always wins.

Pinned jobs always run. Any developer can force a job back on from the PR.

When in doubt, it runs.

Trunk slow or unreachable, a fork with no token, a job with no verdict: everything runs.

Queue skips nothing

The commit that lands is fully tested.

Required jobs run

No queue behind it? It always runs.

Failed jobs rerun

Rejected a PR? It runs again.

New jobs run

No history, no skip.

Your call wins

Pinned or forced jobs always run.

When in doubt, it runs

Trunk down, forks, no verdict.

And if a skip is ever wrong, you see it. Every false skip is flagged in your dashboard and fed into the next verdict.

COMPARED TO OTHER APPROACHES

Keep what you have and skip what it can’t.

Most tools trim work inside a job that has already started. Dynamic CI decides before the job starts, so when it says skip, nothing after that point runs.

Approach
What it skips
When it decides
What it sees
With Dynamic CI
Dynamic CI
Jobs
When the job is scheduled
Paths, job history, PR history, stack position, merge queue outcomes, your rules
Path filters
Jobs
When the job is scheduled
File paths
Replaces them. Your rules import as-is.
Build graphs
Targets
Inside the build
Dependency graph
Use both. They skip targets; Dynamic CI skips jobs.
Test selection
Tests
Inside the test step
Coverage or test history
Use both. It trims tests inside jobs that still run.
Faster runners
Nothing
Doesn't decide
Nothing; it just runs jobs faster
Stacks with it. Fewer jobs, on faster runners.
Dynamic CI
Skips
Jobs
Decides
When the job is scheduled
Sees
Paths, job history, PR history, stack position, merge queue outcomes, your rules
Path filters
Skips
Jobs
Decides
When the job is scheduled
Sees
File paths

Replaces them. Your rules import as-is.

Build graphs
Skips
Targets
Decides
Inside the build
Sees
Dependency graph

Use both. They skip targets; Dynamic CI skips jobs.

Test selection
Skips
Tests
Decides
Inside the test step
Sees
Coverage or test history

Use both. It trims tests inside jobs that still run.

Faster runners
Skips
Nothing
Decides
Doesn't decide
Sees
Nothing; it just runs jobs faster

Stacks with it. Fewer jobs, on faster runners.

DROP-IN SETUP

Add one job. Keep everything else.

No new build system, no test changes, no migration. Paste one job into your pipeline, add a short condition to each job you want Dynamic CI to decide on, and open a pull request. On Buildkite, it's one extra stage in your pipeline upload.

1

Paste one job.

Add the Dynamic CI job to your pipeline, then a short condition on each job you want it to decide on. Your repo starts in learning mode.

2

Watch it learn.

It scores every job and skips nothing, then shows what it would have saved on your own repo.

3

Turn it on, repo by repo or user by user.

Enforce a repo, or a few users first, in the Trunk app when the numbers convince you. Widen from there.

jobs:
dynamic-ci-filter:
name: Dynamic CI
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
unit-tests: ${{ steps.ci-filter.outputs.unit-tests }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: blob:none
- name: Dynamic CI
id: ci-filter
uses: trunk-io/dynamic-ci@v1
with:
token: ${{ secrets.TRUNK_API_TOKEN }}
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: [dynamic-ci-filter]
if: >-
!cancelled() &&
needs.dynamic-ci-filter.outputs.unit-tests != 'false'
steps:
- run: make test

FAQs

THE WAY CI SHOULD WORK

The fastest way to run CI jobs is to not run them at all.