Actio
Macros

if-changed

Gate a step or job on file changes with one shared paths-filter setup job.

A step- or job-level macro that runs a node only when files changed in the pull request or push match a glob pattern. Actio synthesizes one shared dorny/paths-filter setup job, computes every flag once, and folds the guard into each node's if:.

Compiles to a shared setup job plus an if: guard and needs: on the consuming job:

.actio.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build docs
        run: ./build-docs.sh
        if-changed:
          - "docs/**"
          - "*.md"
      - name: Always run
        run: ./test.sh
generated .yml
jobs:
  actio_changes: 
    runs-on: ubuntu-latest
    outputs:
      filter_1: ${{ steps.actio_filter.outputs.filter_1 }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: actio_filter
        with:
          filters: |-
            filter_1:
              - 'docs/**'
              - '*.md'
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build docs
        run: ./build-docs.sh
        if: needs.actio_changes.outputs.filter_1 == 'true'
      - name: Always run
        run: ./test.sh
    needs: 
      - actio_changes
.actio.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build docs
        run: ./build-docs.sh
        if-changed:
          - "docs/**"
          - "*.md"
      - name: Always run
        run: ./test.sh
generated .yml
jobs:
  actio_changes: 
    runs-on: ubuntu-latest
    outputs:
      filter_1: ${{ steps.actio_filter.outputs.filter_1 }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: actio_filter
        with:
          filters: |-
            filter_1:
              - 'docs/**'
              - '*.md'
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build docs
        run: ./build-docs.sh
        if: needs.actio_changes.outputs.filter_1 == 'true'
      - name: Always run
        run: ./test.sh
    needs: 
      - actio_changes

Value

if-changed takes a single glob string or an array of globs:

.actio.yml
if-changed: "services/api/**"        # one pattern
if-changed: ["docs/**", "*.md"]      # multiple (matches if ANY hit)

Globs use the standard paths-filter pattern syntax (*, **, negation with !).

Job-level gating

Put if-changed directly on a job to skip the whole job when nothing relevant changed:

.actio.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    if-changed: "services/api/**"
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh

Shared setup, deduped flags

Every if-changed in the workflow feeds the same actio_changes setup job. Identical glob groups collapse to a single filter flag, so two nodes watching services/api/** share one output instead of running paths-filter twice:

.actio.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    if-changed: "services/api/**"
    steps:
      - run: ./deploy.sh
  smoke:
    runs-on: ubuntu-latest
    if-changed: "services/api/**"
    steps:
      - run: ./smoke.sh

Both jobs reference needs.actio_changes.outputs.filter_1; the setup job runs paths-filter once.

Combining with if:

An existing if: is preserved and AND-combined with the change guard, so both conditions must hold:

.actio.yml
jobs:
  smoke:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    if-changed: "services/api/**"
    steps:
      - run: ./smoke.sh
generated .yml
  smoke:
    runs-on: ubuntu-latest
    if: needs.actio_changes.outputs.filter_1 == 'true' && github.ref == 'refs/heads/main'
.actio.yml
jobs:
  smoke:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    if-changed: "services/api/**"
    steps:
      - run: ./smoke.sh
generated .yml
  smoke:
    runs-on: ubuntu-latest
    if: needs.actio_changes.outputs.filter_1 == 'true' && github.ref == 'refs/heads/main'

Diff base

dorny/paths-filter auto-detects the comparison base from the event, so the same source works for both triggers with no extra config:

  • pull_request — diffs against the PR base branch (github.event.pull_request.base.sha).
  • push — diffs against the previous commit (github.event.before), with a first-push / force-push fallback to the default branch.

Notes

  • The setup job is named actio_changes and the filter step actio_filter. If a job named actio_changes already exists, Actio emits an error and leaves the workflow unchanged.
  • The if-changed key is stripped from the generated workflow.
  • A raw git diff mode (no marketplace action) is a possible future option; today the macro standardizes on dorny/paths-filter for robust base detection.

On this page