Email infrastructure for GitHub Actions workflows

Route deployment notices, incident alerts, and release summaries through MultiMail's approval and identity layer — so automated CI events don't flood inboxes unchecked.


GitHub Actions workflows trigger on push, pull request, release, schedule, and dozens of other repository events. When those workflows need to send email — deployment notices, failure alerts, security scan results, release announcements — they typically use raw SMTP or a transactional service with no controls on who can send what to whom.

MultiMail gives GitHub Actions a managed email surface with oversight modes, identity verification, and per-workflow policy enforcement. A workflow step calls the MultiMail REST API directly using a stored secret. The API handles delivery, logging, and — if configured — human approval before any message leaves your domain.

CI pipelines can generate email volume fast. A failed deployment that retries five times produces five alert emails. MultiMail's `gated_send` mode lets you require a human to approve before batches of automated messages go out, while `monitored` mode delivers immediately but logs every send for audit. You pick the tradeoff per workflow, not globally.

Built for GitHub Actions developers

Approval gates on CI-triggered sends

Set a workflow's mailbox to `gated_send` or `gated_all` and no message leaves until a human approves it via the MultiMail dashboard or API. Stops alert storms and accidental mass-sends before they reach recipients.

Auditable send log for every workflow run

Every email sent through MultiMail is recorded with the sending identity, timestamp, recipient, and approval status. You can correlate email sends back to specific workflow runs using custom headers or subject prefixes.

Per-mailbox identity enforcement

Each workflow can send from a dedicated mailbox — `[email protected]` or your own domain — so recipients can distinguish deployment notices from incident alerts from release announcements by sender alone.

No SMTP credentials in CI secrets

MultiMail uses Bearer token auth (`MM_API_KEY` stored as a GitHub Actions secret). No SMTP host, port, username, or password to rotate. Tokens can be scoped per mailbox and revoked without touching workflow YAML.

Rate limiting and deduplication built in

Workflows that loop or retry can easily send duplicate alerts. MultiMail enforces per-mailbox send limits and supports idempotency keys so retried workflow steps don't produce duplicate emails.

Formally verified security model

MultiMail's oversight, identity, and authorization models are proven correct in Lean 4. When your CI pipeline sends an email, the policy enforcement isn't best-effort — it's mathematically verified.


Get started in minutes

Deployment notification on push to main
yaml
name: Deploy and notify

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy application
        run: ./scripts/deploy.sh

      - name: Send deployment notice
        if: success()
        env:
          MM_API_KEY: ${{ secrets.MM_API_KEY }}
        run: |
          curl -s -X POST https://api.multimail.dev/v1/send_email \
            -H "Authorization: Bearer $MM_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "from": "[email protected]",
              "to": ["[email protected]"],
              "subject": "Deployed: ${{ github.repository }} @ ${{ github.sha }}",
              "text": "Deployment triggered by ${{ github.actor }} completed successfully.\n\nCommit: ${{ github.sha }}\nBranch: ${{ github.ref_name }}\nWorkflow: ${{ github.run_id }}"
            }'

A workflow step that calls the MultiMail `send_email` endpoint after a successful deploy. Uses `curl` with a stored API key secret.

Failure alert with approval gate
yaml
name: CI with gated failure alerts

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run tests
        id: tests
        run: bun test

      - name: Queue failure alert (gated)
        if: failure()
        env:
          MM_API_KEY: ${{ secrets.MM_API_KEY }}
        run: |
          RESPONSE=$(curl -s -X POST https://api.multimail.dev/v1/send_email \
            -H "Authorization: Bearer $MM_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "from": "[email protected]",
              "to": ["[email protected]"],
              "subject": "[NEEDS APPROVAL] CI failed: ${{ github.repository }}",
              "text": "Tests failed on ${{ github.ref_name }}.\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\nThis message is pending human approval before delivery."
            }')
          echo "Queued message: $RESPONSE"

Send a failure alert only after human approval. The mailbox is configured in `gated_send` mode — the API call queues the message and returns a pending ID. The workflow does not block waiting for approval.

Release announcement via workflow_dispatch
yaml
name: Release announcement

on:
  workflow_dispatch:
    inputs:
      version:
        description: Release version (e.g. v1.4.0)
        required: true
      release_notes:
        description: One-paragraph release summary
        required: true

jobs:
  announce:
    runs-on: ubuntu-latest
    steps:
      - name: Send release announcement
        env:
          MM_API_KEY: ${{ secrets.MM_API_KEY }}
          VERSION: ${{ inputs.version }}
          NOTES: ${{ inputs.release_notes }}
        run: |
          curl -s -X POST https://api.multimail.dev/v1/send_email \
            -H "Authorization: Bearer $MM_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{
              \"from\": \"[email protected]\",
              \"to\": [\"[email protected]\"],
              \"subject\": \"Released: $VERSION\",
              \"text\": \"$NOTES\\n\\nFull changelog: https://github.com/${{ github.repository }}/releases/tag/$VERSION\"
            }"

      - name: Check pending approvals
        env:
          MM_API_KEY: ${{ secrets.MM_API_KEY }}
        run: |
          curl -s https://api.multimail.dev/v1/list_pending \
            -H "Authorization: Bearer $MM_API_KEY" | jq '.pending[] | {id, subject, status}'

A manually triggered workflow that sends a release announcement to a subscriber list. The `list_pending` endpoint can be polled to confirm approval before the workflow exits.

Security scan result delivered via Python SDK step
python
"cm"># .github/scripts/send_scan_report.py
import os
import json
import sys
from multimail import Multimail

client = Multimail(api_key=os.environ["MM_API_KEY"])

"cm"># Scan results passed as JSON via environment variable
results = json.loads(os.environ.get("SCAN_RESULTS", "{}"))
critical = results.get("critical", [])
high = results.get("high", [])

if not critical and not high:
    print("No critical or high findings — skipping alert")
    sys.exit(0)

body_lines = ["Security scan completed for " + os.environ["GITHUB_REPOSITORY"]]
body_lines.append("Run: https://github.com/" + os.environ["GITHUB_REPOSITORY"] + "/actions/runs/" + os.environ["GITHUB_RUN_ID"])
body_lines.append("")

if critical:
    body_lines.append(f"CRITICAL ({len(critical)}):")
    for finding in critical:
        body_lines.append(f"  - {finding}")

if high:
    body_lines.append(f"HIGH ({len(high)}):")
    for finding in high:
        body_lines.append(f"  - {finding}")

response = client.send_email(
    from_address="[email protected]",
    to=["[email protected]"],
    subject=f"[Security] {len(critical)} critical findings in {os.environ[&"cm">#039;GITHUB_REPOSITORY']}",
    text="\n".join(body_lines)
)

print(f"Sent: {response.message_id}")

A Python script run inside a workflow step that uses the multimail-sdk to send a security scan summary with structured results.


Step by step

1

Create a MultiMail account and mailbox

Sign up at multimail.dev and create a dedicated mailbox for CI notifications. Use a subdomain mailbox like `[email protected]` or configure your own domain. Set the oversight mode to `gated_send` for a safe starting point.

bash
"cm"># Via MultiMail API (or use the dashboard)
curl -X POST https://api.multimail.dev/v1/create_mailbox \
  -H "Authorization: Bearer $MM_API_KEY" \
  -H "Content-Type: application/json" \
  -d &"cm">#039;{"address": "[email protected]", "oversight_mode": "gated_send"}'
2

Store your API key as a GitHub Actions secret

In your GitHub repository, go to Settings → Secrets and variables → Actions → New repository secret. Name it `MM_API_KEY` and paste your `mm_live_...` token. For organizations, store it as an organization secret to share across multiple repositories.

bash
"cm"># Verify the secret is accessible in a test workflow step
- name: Verify MM_API_KEY
  env:
    MM_API_KEY: ${{ secrets.MM_API_KEY }}
  run: |
    curl -s https://api.multimail.dev/v1/list_pending \
      -H "Authorization: Bearer $MM_API_KEY" | jq &"cm">#039;.status'
3

Add a notify step to your existing workflow

Insert a `send_email` step after the job step you want to report on. Use `if: success()` or `if: failure()` to control when the notification fires. Include `github.sha`, `github.actor`, and `github.run_id` in the message body for traceability.

bash
- name: Notify on failure
  if: failure()
  env:
    MM_API_KEY: ${{ secrets.MM_API_KEY }}
  run: |
    curl -s -X POST https://api.multimail.dev/v1/send_email \
      -H "Authorization: Bearer $MM_API_KEY" \
      -H "Content-Type: application/json" \
      -d &"cm">#039;{
        "from": "[email protected]",
        "to": ["[email protected]"],
        "subject": "Build failed: ${{ github.repository }}",
        "text": "Workflow ${{ github.workflow }} failed.\nActor: ${{ github.actor }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
      }&"cm">#039;
4

Approve or adjust the oversight mode

If using `gated_send`, log into the MultiMail dashboard to approve the first test sends and confirm the message content looks correct. Once satisfied with the format, switch the mailbox to `monitored` or `autonomous` if human approval per-send is not required for this workflow.

bash
"cm"># Check pending messages from the mailbox
curl -s https://api.multimail.dev/v1/list_pending \
  -H "Authorization: Bearer $MM_API_KEY" | jq &"cm">#039;.pending[] | {id, subject, created_at, status}'

Common questions

Can one API key be used across multiple repositories?
Yes. Store the same `MM_API_KEY` as an organization-level secret in GitHub and reference it in any repository's workflows. However, if you want per-repository audit separation, create one mailbox per repository and use mailbox-scoped API keys so you can revoke access for a single repo without affecting others.
How do I prevent alert storms when a flaky test retries five times?
Set the mailbox to `gated_send` mode. All five alert emails will queue for approval, but only the ones a human approves will be delivered. Alternatively, use an idempotency key derived from the run ID in your `send_email` call — MultiMail will deduplicate sends with the same key within a time window.
Does the API call add significant latency to my workflow?
The `send_email` API call typically completes in under 300ms. In `gated_send` mode, the call returns immediately after queuing the message — it does not block until delivery. If you're calling from a script that waits for the response, add a timeout to avoid holding runner minutes unnecessarily.
Can I use this with GitHub's reusable workflows or composite actions?
Yes. Create a composite action in a shared repository that wraps the `curl` call and accepts inputs like `subject`, `body`, and `to`. Other workflows call it with `uses: yourorg/actions/notify@main` and pass their own `MM_API_KEY` secret. This lets you standardize email formatting across all repositories.
What if I need to send to different recipients depending on which branch failed?
Use GitHub Actions expression syntax to conditionally set the `to` field. For example, send failures on `main` to the oncall list and failures on feature branches to the PR author. The `github.actor` and `github.ref_name` context variables are available in any step's `env` block.
How does MultiMail handle CAN-SPAM and GDPR for automated CI emails?
CAN-SPAM applies to commercial emails, not transactional operational messages like deployment alerts sent to your own team. If you're sending release announcements to subscribers outside your organization, CAN-SPAM requires a physical address and unsubscribe mechanism; GDPR requires a lawful basis for processing recipient email addresses. MultiMail's audit log helps demonstrate processing records if required. Consult your legal team for subscriber-facing sends.

Explore more

The only agent email with a verifiable sender

Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.