# Check email templates in CI

`bird email templates check` checks your email templates from any CI system and exits with a failure when a change would break them, so a pull request catches the problem before a recipient does. It reads only: nothing is sent and no draft changes.

## Before you start

- Install the [`bird` CLI](/docs/cli) with a pinned version, so a new release never changes your pipeline unannounced.
- Create an API key with read access to `email_management` and make it available to the job as `BIRD_API_KEY`.

## Check a template

```bash
bird email templates check welcome-email order-shipped
```

The command renders each template's draft in every language it has, since a translation can break on its own, and reports, per template and language:

- **Mail-client support**: the same compatibility findings [template preview](/docs/guides/email/templates) shows, such as CSS a client removes or ignores.
- **Size**: the HTML body against Gmail's clipping point of about 102 KB. Gmail hides everything past it behind a link. Above 90 KB the check warns, since merge values can push a message over.
- **Links and images**: every `http` and `https` URL in a link (`<a>` and `<area>`), an image source or `srcset`, a `background` attribute, or a CSS `url()` in a `style` attribute or `<style>` block, fetched from where the command runs. A `404`, `410`, or `5xx`, or a host that does not exist, is a problem. A `401`, `403`, or `429`, or a DNS lookup that times out, is a warning, because it usually means the runner was turned away or could not reach the host. Links to private or loopback addresses are never fetched. Pass `--skip-links` on a runner without outbound network access.

Links built from a template variable, a contact field, or the unsubscribe link are not fetched, because preview cannot fill them with the value a recipient gets. The report lists them under `unverified_links`, with the per-recipient part shown as `{variable}`, and `--annotations github` prints them as a notice so the pull request shows what was not checked.

The command prints a JSON report on stdout: `valid` for the whole run, then one entry per template and language with its `findings`. Each finding has a `severity`, an `area` (`compatibility`, `size`, or `links`), a `message`, and usually a `fix`. A `problem` fails the check; a `warning` is reported and never fails it.

## Check a file your build renders

If your build renders templates itself, for example with MJML or React Email, check the file it produces:

```bash
bird email templates check --html dist/welcome.html --template welcome-email --annotations github
```

The file is previewed as unsaved content against the template you name, so no draft changes; any template works. With `--annotations github`, compatibility findings land on the lines of the file they refer to, and size and link findings are annotated on the file as a whole.

## Exit codes

The exit code is what your CI job branches on:

| Exit code | Meaning                                                                        |
| --------- | ------------------------------------------------------------------------------ |
| `0`       | No problems. Warnings may still be reported.                                   |
| `7`       | The check ran and found at least one problem. The report names each one.       |
| `3`       | A template you named does not exist in this workspace.                         |
| `4`       | The API key is missing, invalid, or lacks read access to `email_management`.   |
| `6`       | Rate limited or a temporary server error. Retry after the `retry_after` delay. |

Retrying on `7` without a change fails the same way, so only `6` is worth retrying.

## Run it in GitHub Actions

With `--annotations github`, the command prints findings as GitHub Actions annotations instead of the JSON report, so they appear in the pull request's checks. This workflow checks the templates your build renders on every pull request that changes them:

```yaml
name: Email templates
on:
  pull_request:
    paths: ["emails/**"]
jobs:
  check:
    # Pull requests from forks get no repository secrets, so the check could only exit 4.
    if: ${{ !github.event.pull_request.head.repo.fork }}
    runs-on: ubuntu-latest
    env:
      BIRD_API_KEY: ${{ secrets.BIRD_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - name: Install the bird CLI
        run: curl -fsSL https://cli.bird.com/install.sh | sh -s -- --version "${{ vars.BIRD_CLI_VERSION }}"
      - name: Build the templates
        run: npm ci && npm run build:emails
      - name: Check the templates
        run: |
          status=0
          for file in dist/emails/*.html; do
            bird email templates check --html "$file" --template welcome-email --annotations github || status=$?
          done
          exit "$status"
```

Set `BIRD_CLI_VERSION` as a repository variable to the release you tested, and the `BIRD_API_KEY` secret to a key with read access to `email_management`. GitHub gives pull requests from forks no repository secrets, so the workflow skips them. Check a change from a fork before merging it by pushing the same change to a branch in your repository. In any other CI system, run the same commands and fail the job on a non-zero exit code.

## Troubleshooting

- **Exit `3`.** The template is not in the workspace the API key belongs to. List the workspace's templates with `bird email templates list`.
- **Exit `4`.** The key lacks read access to `email_management`, or the secret is not reaching the job.
- **Every link fails with a warning.** The runner has no outbound network access, or a firewall blocks it. Run with `--skip-links` there, and check links from a runner that can reach the internet.

## Next steps

- [Email templates](/docs/guides/email/templates): author, preview, and publish templates in every language
- [`bird email templates check`](/docs/cli/reference/email-templates-check): every flag the command takes
- [The `bird` CLI](/docs/cli): install a pinned version and authenticate in CI

## Related resources

- [Getting started with email](/learn/email/getting-started-with-email) (video)
- [Email](/email-api) (product)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first email](/docs/get-started/send-your-first-email) (docs)

[Get an implementation brief](/learn/workspace?topic=email)
