# Send email from CI/CD pipelines

Source: https://developer.nylas.com/docs/cookbook/cli/email-in-cicd/

Build and deploy notifications are application mail, not personal mail. When a pipeline finishes, the message comes from your release system, so routing it through a developer's connected inbox is the wrong model. This recipe sends those alerts with transactional send, which goes out from a verified domain instead of a user account.

You run the send as a single `curl` step inside your pipeline. The API key lives in your CI secret store, never in the repository, and the same step works for a green deploy or a red build.

## Why use transactional send for pipeline notifications?

Pipeline alerts are system messages triggered by an event, so they fit the transactional model rather than a user mailbox. Transactional send uses a `POST /v3/domains/{domain_name}/messages/send` route keyed on a verified domain, which means no OAuth grant and no per-developer authentication. One API key sends from your release domain.

This keeps the pipeline simple. A typical workflow needs just 1 `curl` step and 2 stored values: your API key and your verified domain. There is no token refresh to manage, which matters in a CI runner that starts cold and exits in under 60 seconds. For the domain verification steps, see [transactional send](/docs/cookbook/email/transactional-send/). If you would rather send from a real connected mailbox, use the grant route at `POST /v3/grants/{grant_id}/messages/send` instead.

## How do I store the Nylas API key in CI secrets?

Never hardcode the API key in a workflow file, because pipeline definitions are committed to the repository and visible to anyone with read access. Store the key as an encrypted CI secret and read it as an environment variable at runtime. Both major platforms encrypt secrets at rest and mask them in logs.

In GitHub, open Settings, then Secrets and variables, then Actions, and add a repository secret named `NYLAS_API_KEY`. Add `NYLAS_DOMAIN` for your verified domain as a repository *variable*, since it isn't sensitive, which is why the workflow below reads it as `vars.NYLAS_DOMAIN` rather than `secrets`. In GitLab, open Settings, then CI/CD, then Variables, and add both, with the Masked flag enabled on `NYLAS_API_KEY` so it never prints in job output. Reference the key as `${{ secrets.NYLAS_API_KEY }}` in GitHub or `$NYLAS_API_KEY` in GitLab. Rotate the key from the Nylas Dashboard if a runner is ever compromised.

## How do I send a deploy notification from GitHub Actions?

Add a job step that runs after your deploy step and reads the secrets from the environment. The step below sends a success notification, mapping `secrets.NYLAS_API_KEY` and `vars.NYLAS_DOMAIN` into environment variables that `curl` reads. This is the only place the send call appears in this recipe.

The job runs in about 5 seconds and exits after the single request returns.

```yaml [githubActionsSend-GitHub Actions]
- name: Send deploy notification
  if: success()
  env:
    NYLAS_API_KEY: ${{ secrets.NYLAS_API_KEY }}
    NYLAS_DOMAIN: ${{ vars.NYLAS_DOMAIN }}
  run: |
    curl --request POST \
      --url "https://api.us.nylas.com/v3/domains/${NYLAS_DOMAIN}/messages/send" \
      --header "Authorization: Bearer ${NYLAS_API_KEY}" \
      --header "Content-Type: application/json" \
      --data '{
        "to": [{ "name": "Release Team", "email": "releases@acme.com" }],
        "from": { "name": "ACME CI", "email": "ci@acme.com" },
        "subject": "Deploy succeeded: '"${GITHUB_REPOSITORY}"' @ '"${GITHUB_SHA}"'",
        "body": "Pipeline '"${GITHUB_RUN_ID}"' deployed successfully."
      }'
```

The `from` address must belong to the same verified domain in the path, so with `NYLAS_DOMAIN=acme.com` the sender has to be an `@acme.com` address or the send returns a `4xx`. The `if: success()` condition runs the step only when every earlier step passed, so the "succeeded" notice never fires on a broken deploy. For failure alerts, add a second step with `if: failure()` and a subject that says so. The GitLab example below shows the failure side.

## How do I send a build-failure alert from GitLab CI?

GitLab uses the same domain route, so you only change the variable syntax and the trigger rule. The job below runs only when an earlier stage fails, using the `on_failure` rule, and reuses the masked CI variables you stored earlier. The send takes roughly 1 HTTP round trip and adds under 10 seconds to the pipeline.

Define the job in `.gitlab-ci.yml` after your build and deploy stages.

```yaml [gitlabCiSend-GitLab CI]
notify_failure:
  stage: .post
  rules:
    - when: on_failure
  script:
    - |
      curl --request POST \
        --url "https://api.us.nylas.com/v3/domains/${NYLAS_DOMAIN}/messages/send" \
        --header "Authorization: Bearer ${NYLAS_API_KEY}" \
        --header "Content-Type: application/json" \
        --data "{
          \"to\": [{ \"name\": \"On-call\", \"email\": \"oncall@acme.com\" }],
          \"from\": { \"name\": \"ACME CI\", \"email\": \"ci@acme.com\" },
          \"subject\": \"Build failed: ${CI_PROJECT_PATH} pipeline ${CI_PIPELINE_ID}\",
          \"body\": \"Job ${CI_JOB_NAME} failed. See ${CI_PIPELINE_URL}.\"
        }"
```

Reuse the predefined CI variables like `CI_PIPELINE_URL` so the alert links straight back to the failed run.

## Things to know about sending from pipelines

The sending address in `from` must belong to a domain Nylas has verified, or the request returns a 4xx error before any mail leaves. Keep these alerts to 1 message per pipeline event, since transactional send targets system mail rather than bulk delivery. JSON payloads follow the same 3 MB limit as a grant send, which is more than enough for a status line.

Treat a non-200 response as a pipeline warning rather than a hard failure, so a notification outage never blocks a real deploy. Transactional send is in beta, so confirm delivery during rollout. If you run scheduled jobs outside CI, the [bash and cron recipe](/docs/cookbook/cli/send-email-bash-cron/) and the [PowerShell recipe](/docs/cookbook/cli/send-email-powershell/) cover the same send from a shell.

## What's next

- [Send transactional email from a domain](/docs/cookbook/email/transactional-send/) for domain verification and field reference
- [Send scheduled email with bash and cron](/docs/cookbook/cli/send-email-bash-cron/) for time-based jobs
- [Send email with PowerShell](/docs/cookbook/cli/send-email-powershell/) for Windows runners
- [Email API reference](/docs/reference/api/) for the full send schema