# call-templates (/docs/macros/call-templates)



`job-defaults` and `executors` cover normal jobs, but they deliberately skip the
plumbing of reusable-workflow **call jobs** — `uses:`, `with:`, `needs:`, and
`secrets:` must be restated on every job. In matrix-heavy repos that plumbing is
the single biggest source of copy-paste: e.g. `vercel/next.js` has \~8 nearly
byte-identical `test-*` call jobs that differ only in one `with.afterBuild` value.

`call-templates` defines a named call-job preset; a job `extends:` it and overrides
only the deltas. Actio merges the template at compile time and strips both
`call-templates` and `extends` from the output — the generated YAML is byte-identical
to the hand-written call jobs.

Each job inherits `uses`, `needs`, `secrets`, and the shared `with` keys; only the
per-job `afterBuild` differs:

<CodeCompare>
  ```yaml title=".actio.yml"
  name: Build and test
  on: [push]
  call-templates:
    test:
      uses: ./.github/workflows/test_reusable.yml
      needs: build
      with:
        testTimingsArtifact: test-timings
      secrets: inherit
  jobs:
    build:
      runs-on: ubuntu-latest
      steps:
        - run: echo build
    test-unit:
      extends: test
      with:
        afterBuild: pnpm test:unit
    test-integration:
      extends: test
      with:
        afterBuild: pnpm test:integration
  ```

  ```yaml title="generated .yml"
  jobs:
    build:
      runs-on: ubuntu-latest
      steps:
        - run: echo build
    test-unit:
      uses: ./.github/workflows/test_reusable.yml
      needs: build
      with:
        testTimingsArtifact: test-timings
        afterBuild: pnpm test:unit
      secrets: inherit
    test-integration:
      uses: ./.github/workflows/test_reusable.yml
      needs: build
      with:
        testTimingsArtifact: test-timings
        afterBuild: pnpm test:integration
      secrets: inherit
  ```
</CodeCompare>

## Merge semantics [#merge-semantics]

| Key       | Behaviour                                                                                                                             |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `with`    | **Shallow per-key** — `{ ...template, ...inline }`. Each `with.<key>` the job sets replaces the template's; unspecified keys inherit. |
| `needs`   | **Order-preserving union** — template deps plus inline deps, de-duplicated.                                                           |
| `secrets` | If either side is the string `inherit`, **inline wins** (replace). If both are maps, **shallow per-key** merge like `with`.           |
| `if`      | **AND-combined** — `template && inline`, consistent with `job-defaults`.                                                              |
| `uses`    | **Replace-on-presence** — inline value wins entirely; the template only applies when the job sets no inline `uses`.                   |

A v1 template may declare only the call quad: `uses`, `with`, `needs`,
`secrets`, and `if`. Runner keys (`runs-on`, `env`, `container`, …), `steps`,
and the broader job knobs (`permissions`, `concurrency`, `strategy`) are
rejected — keep `strategy` on the job so [`for-each`](/docs/syntax#for-each)
stays its sole owner, and set `permissions`/`concurrency` per job or via
[`job-defaults`](/docs/macros/job-defaults).

## Composing templates [#composing-templates]

`extends:` accepts a single name or a list. Templates are merged left-to-right
(later wins), and an inline value on the job always wins over all of them:

`needs` unions across both templates and the job; `with.shared` resolves to the
inline value while `kept` and `added` survive; the two `secrets` maps merge; and
both `if` gates combine:

<CodeCompare>
  ```yaml title=".actio.yml"
  call-templates:
    base:
      uses: ./.github/workflows/reuse.yml
      needs: build
      with:
        shared: base-shared
        kept: base-kept
      secrets:
        TOKEN: ${{ secrets.BASE }}
      if: ${{ github.event_name == 'push' }}
    extra:
      needs: lint
      with:
        shared: extra-shared
      secrets:
        EXTRA: ${{ secrets.EXTRA }}
  jobs:
    composed:
      extends: [base, extra]
      needs: deploy
      with:
        shared: inline-shared
        added: inline-added
      if: ${{ success() }}
  ```

  ```yaml title="generated .yml"
  jobs:
    composed:
      uses: ./.github/workflows/reuse.yml
      needs:
        - build
        - lint
        - deploy
      with:
        shared: inline-shared
        kept: base-kept
        added: inline-added
      secrets:
        TOKEN: ${{ secrets.BASE }}
        EXTRA: ${{ secrets.EXTRA }}
      if: github.event_name == 'push' && success()
  ```
</CodeCompare>

## Precedence with job-defaults [#precedence-with-job-defaults]

`call-templates` resolves **before** `job-defaults`, so a fully materialized call
job is what `job-defaults` sees. A template value (or inline job value) already
present blocks the default from overwriting it — precedence is
**inline > extends > job-defaults**. The `if` key AND-combines all three; for
keys a template can't carry (`permissions`, `concurrency`), `job-defaults` fills
them on the materialized call job. Keep `strategy` inline on the call job.

<Callout title="Call-job only">
  `extends:` is for reusable-workflow call jobs. A job that sets inline `steps:`
  cannot `extends:`, and if no template in the chain provides `uses:` (and the job
  sets none) Actio errors — the result would not be a call job. Templates carry
  the call plumbing; they never carry runner configuration.
</Callout>

<Callout title="Compile-time interpolation">
  `{{ params.* }}` inside a `call-templates` block resolves before the template is
  merged, so templates can be parameterized like any other Actio source.
</Callout>


## Sitemap

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