What it is
Most CI runs are wasted. A docs-only change does not need the integration suite; a frontend typo does not need the backend tests. The usual fix is a path filter — hand-written globs that are coarse, go stale, and encode a guess about which files matter to which job. Dynamic CI answers the same question from evidence instead. Trunk already records every(file change set → job outcome) pair in your repository, so it can tell you which jobs
this diff has historically broken and which it never touches. The
trunk-io/dynamic-ci-filter action asks
for a verdict per job and emits it as a job output you gate on — a near drop-in
replacement for dorny/paths-filter.
How a verdict is reached
Each verdict combines independent signals, and every signal’s contribution is printed in the job logs and the job summary, so a skip is always auditable.
Signals are per-organization. Nothing is pooled across customers — your recommendations
are derived only from your own CI history.
Pass a comma-separated list to
ignore-signals to drop any of them from the tally.
Set it up
1
Get your organization API token
In the Trunk app, go to Settings → Manage Organization → Organization API
Token, and store it as a repository secret (for example
TRUNK_API_TOKEN).2
Add the filter job
One upstream job asks for verdicts for the whole workflow. Per-job outputs are set
at runtime, so re-export each one you intend to gate on by name.
3
Gate your jobs on the output
jobs:, and what
github.job reports — not the name: it displays under. A display name changes with a
job’s matrix values; the key does not.
This is pre-job mode, and it captures the most savings because a skipped job never boots
a runner. There is also a pre-step mode, where a job asks only about itself using the
job-keys input; it is simpler to adopt but the runner has already started by the time
the verdict arrives.
Always write != 'false'
A job with no verdict — a service outage, a job Trunk has not seen before, a job whose key
it has not resolved yet — emits no output at all. != 'false' correctly runs that job.
== 'true' would silently skip your entire test suite the first time anything went wrong.
That convention is what makes the action’s fail-open behavior work. A transport error, a
non-2xx response, a malformed response, or a request past the latency budget (30s by
default) all resolve to “run”, the step itself never fails, and every fail-open is logged
as a warning annotation and written to the job summary — so a real skip is always
distinguishable from a degraded one.
Merge queues
A merge queue validates the exact commit about to land, so a skip there could merge untested code. Trunk therefore never skips a job on a Trunk Merge Queue branch: a request on atrunk-merge/ branch short-circuits to run-everything before any
recommendation work happens.
You do not need to gate the queue yourself, and in particular you should not add
github.event_name != 'pull_request' — with draft merge-queue pull requests enabled,
queue batches arrive as pull_request events on trunk-merge/* branches, so the event
name cannot tell them apart from real PRs.
Other merge queues are not covered. GitHub’s native merge queue validates on
gh-readonly-queue/* branches via merge_group events, which are treated like any other
request, so gate those yourself: