# if-changed (/docs/macros/if-changed)



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`](https://github.com/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:

<CodeCompare>
  ```yaml title=".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
  ```

  ```yaml title="generated .yml"
  jobs:
    actio_changes: # [!code highlight:13]
      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' # [!code highlight]
        - name: Always run
          run: ./test.sh
      needs: # [!code highlight:2]
        - actio_changes
  ```
</CodeCompare>

## Value [#value]

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

```yaml title=".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](https://github.com/dorny/paths-filter#filters-file) (`*`, `**`, negation
with `!`).

## Job-level gating [#job-level-gating]

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

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

## Shared setup, deduped flags [#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:

```yaml title=".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:` [#combining-with-if]

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

<CodeCompare>
  ```yaml title=".actio.yml"
  jobs:
    smoke:
      runs-on: ubuntu-latest
      if: github.ref == 'refs/heads/main'
      if-changed: "services/api/**"
      steps:
        - run: ./smoke.sh
  ```

  ```yaml title="generated .yml"
    smoke:
      runs-on: ubuntu-latest
      if: needs.actio_changes.outputs.filter_1 == 'true' && github.ref == 'refs/heads/main' # [!code highlight]
  ```
</CodeCompare>

## Diff base [#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 [#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.


## Sitemap

Browse the full documentation: [Markdown sitemap](https://austenstone.github.io/actio/sitemap.md) · [XML sitemap](https://austenstone.github.io/actio/sitemap.xml)