Durable Email Delivery for Trigger.dev Workflows

Connect Trigger.dev tasks to MultiMail's REST API for retry-safe email sends with oversight controls, approval gating, and full audit logging across every background job.


Trigger.dev handles the hard parts of background job orchestration — retries, scheduling, durable execution, and event-driven triggers. MultiMail handles the hard parts of email for AI agents — deliverability, oversight controls, and audit logging. Together they give you a complete pipeline for automated email workflows that remain under human control.

The integration is REST-based. Trigger.dev tasks call MultiMail's API directly using fetch, so there is no additional SDK to install. Any task that needs to send, read, or process email makes an authenticated HTTP request to https://api.multimail.dev.

Trigger.dev's retry semantics pair well with MultiMail's idempotent send API. A task that fails mid-run can safely retry the send_email call — passing a stable idempotency_key ensures recipients never receive duplicates even when a job re-executes.

For teams running agentic workflows, MultiMail's oversight modes let you gate automated sends through a human-approval queue without changing your task logic. A job running against a gated_send mailbox queues the email for approval rather than delivering immediately. Your task code stays the same; only the mailbox policy changes.

Built for Trigger.dev developers

Retry-safe delivery

Trigger.dev retries failed tasks automatically. MultiMail's send_email endpoint is idempotent when you supply a stable idempotency_key, so retried tasks never produce duplicate sends to recipients.

Human-approval gating without code changes

Switch a mailbox from autonomous to gated_send mode and every automated send from that task enters an approval queue. Your Trigger.dev task code is unchanged — the policy lives in the mailbox configuration, not in the job logic.

Audit log for every automated action

MultiMail logs every send, read, reply, and decide_email call with the API key, timestamp, mailbox, and outcome. For compliance with SOC 2 or GDPR audit requirements, background job email activity is fully traceable.

Inbound processing via scheduled tasks

Use Trigger.dev's cron schedules to poll check_inbox on a fixed interval, then read_email for each new message. This gives you a durable, retryable inbound-processing pipeline without managing a separate webhook listener.

Oversight modes that match your trust level

Start new agentic workflows in gated_all mode so every action requires human approval. Promote to monitored or autonomous as confidence grows. MultiMail's graduated oversight maps directly to Trigger.dev workflows at any maturity stage.


Get started in minutes

Send a notification email after a background job completes
typescript
import { task } from '@trigger.dev/sdk/v3';

export const sendCompletionEmail = task({
  id: 'send-completion-email',
  retry: { maxAttempts: 3, backoffFactor: 2 },
  run: async (payload: {
    to: string;
    jobId: string;
    summary: string;
  }) => {
    const res = await fetch('https://api.multimail.dev/send_email', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: '[email protected]',
        to: payload.to,
        subject: `Job ${payload.jobId} completed`,
        text: payload.summary,
        idempotency_key: `job-complete-${payload.jobId}`,
      }),
    });

    if (!res.ok) {
      throw new Error(`send_email failed: ${res.status} ${res.statusText}`);
    }

    return res.json();
  },
});

A Trigger.dev task that sends a completion email via MultiMail's send_email endpoint. The task throws on non-2xx responses so Trigger.dev retries automatically. An idempotency_key derived from the job ID prevents duplicate sends on retry.

Scheduled inbox check with read and tag
typescript
import { schedules } from '@trigger.dev/sdk/v3';

const API = 'https://api.multimail.dev';
const headers = {
  Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
  'Content-Type': 'application/json',
};

export const processInbox = schedules.task({
  id: 'process-inbox-hourly',
  cron: '0 * * * *',
  run: async () => {
    const inboxRes = await fetch(
      `${API}/[email protected]&unread=true`,
      { headers },
    );
    if (!inboxRes.ok) throw new Error(`check_inbox: ${inboxRes.status}`);

    const { emails } = await inboxRes.json();

    for (const email of emails) {
      const readRes = await fetch(`${API}/read_email?id=${email.id}`, { headers });
      if (!readRes.ok) throw new Error(`read_email ${email.id}: ${readRes.status}`);

      const message = await readRes.json();

      await fetch(`${API}/tag_email`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ id: email.id, tags: ['processed'] }),
      });

      console.log('Processed:', message.subject);
    }

    return { processed: emails.length };
  },
});

A Trigger.dev cron task that polls check_inbox every hour, reads each new message, and tags it for downstream processing. Each read throws on failure so the scheduler retries the full run.

Gated approval workflow with wait and list_pending
typescript
import { task, wait } from '@trigger.dev/sdk/v3';

const BASE = 'https://api.multimail.dev';
const auth = () => ({
  Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
  'Content-Type': 'application/json',
});

export const sendGatedEmail = task({
  id: 'send-gated-email',
  run: async (payload: { to: string; subject: string; body: string }) => {
    "cm">// Mailbox is in gated_send mode — send_email queues rather than delivers
    const sendRes = await fetch(`${BASE}/send_email`, {
      method: 'POST',
      headers: auth(),
      body: JSON.stringify({
        from: '[email protected]',
        to: payload.to,
        subject: payload.subject,
        text: payload.body,
      }),
    });
    if (!sendRes.ok) throw new Error(`send_email: ${sendRes.status}`);
    const { message_id } = await sendRes.json();

    "cm">// Suspend for up to 24 hours while a human reviews
    await wait.for({ hours: 24 });

    const pendingRes = await fetch(`${BASE}/list_pending`, { headers: auth() });
    const { pending } = await pendingRes.json();
    const stillPending = pending.find((m: { id: string }) => m.id === message_id);

    return {
      message_id,
      status: stillPending ? 'awaiting_approval' : 'delivered_or_cancelled',
    };
  },
});

A two-phase Trigger.dev task that queues an email for human approval, suspends execution for up to 24 hours using wait.for, then checks list_pending to report whether the message was approved or is still waiting.

Reply to a thread from within a background task
typescript
import { task } from '@trigger.dev/sdk/v3';

export const replyToThread = task({
  id: 'reply-to-thread',
  run: async (payload: { thread_id: string; reply_text: string }) => {
    const API = 'https://api.multimail.dev';
    const headers = {
      Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      'Content-Type': 'application/json',
    };

    "cm">// Load the thread to get the latest message ID
    const threadRes = await fetch(`${API}/get_thread?id=${payload.thread_id}`, { headers });
    if (!threadRes.ok) throw new Error(`get_thread: ${threadRes.status}`);
    const { messages } = await threadRes.json();
    const latest = messages[messages.length - 1];

    "cm">// Reply in-thread
    const replyRes = await fetch(`${API}/reply_email`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        reply_to_id: latest.id,
        text: payload.reply_text,
      }),
    });
    if (!replyRes.ok) throw new Error(`reply_email: ${replyRes.status}`);

    return replyRes.json();
  },
});

Fetch a thread by ID, then continue the conversation via MultiMail's reply_email endpoint. Useful when a background job needs to respond to an existing email thread rather than start a new one.


Step by step

1

Create a MultiMail account and provision a mailbox

Sign up at multimail.dev, create a mailbox (e.g. [email protected]), and copy your API key from the dashboard. Set the mailbox oversight mode to gated_send during development so you can review sends before they reach recipients.

bash
"cm"># Dashboard: Settings → API Keys → Create → copy mm_live_... key
"cm"># Dashboard: Mailboxes → New Mailbox
"cm">#   Address: [email protected]
"cm">#   Oversight mode: gated_send
2

Add the API key as a Trigger.dev secret

Store your MultiMail API key as a secret in the Trigger.dev dashboard so it is available to all tasks at runtime without appearing in source code.

bash
"cm"># Trigger.dev dashboard: Environment → Secrets → Add secret
"cm"># Key:   MULTIMAIL_API_KEY
"cm"># Value: mm_live_...
3

Create a task that calls send_email

Write a Trigger.dev task that calls the MultiMail send_email endpoint. Include an idempotency_key derived from your job ID so retries don't produce duplicate sends.

bash
import { task } from &"cm">#039;@trigger.dev/sdk/v3';

export const notifyUser = task({
  id: &"cm">#039;notify-user',
  run: async (payload: { to: string; jobId: string; message: string }) => {
    const res = await fetch(&"cm">#039;https://api.multimail.dev/send_email', {
      method: &"cm">#039;POST',
      headers: {
        Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
        &"cm">#039;Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: &"cm">#039;[email protected]',
        to: payload.to,
        subject: &"cm">#039;Your job is done',
        text: payload.message,
        idempotency_key: `notify-${payload.jobId}`,
      }),
    });
    if (!res.ok) throw new Error(`send_email: ${res.status}`);
    return res.json();
  },
});
4

Deploy and test with a gated send

Deploy to Trigger.dev and trigger the task. Because the mailbox is in gated_send mode, the email will appear in your MultiMail approval queue rather than delivering immediately — verify the content before approving.

bash
npx trigger.dev@latest deploy

"cm"># Trigger manually for a test run
npx trigger.dev@latest run notifyUser \
  --payload &"cm">#039;{"to":"[email protected]","jobId":"test-001","message":"Done!"}'

"cm"># Approve from the MultiMail dashboard: Pending → Review → Approve
5

Switch to autonomous mode when ready

Once you've reviewed several sends and are confident in the output, change the mailbox oversight mode to monitored or autonomous from the MultiMail dashboard. No code changes are required — tasks continue calling send_email identically, but delivery now happens immediately.

bash
"cm"># Dashboard: Mailboxes → [email protected] → Oversight mode → autonomous
"cm"># Tasks are unchanged — oversight mode is a mailbox setting, not a code setting

Common questions

Do I need to install a MultiMail SDK to use it with Trigger.dev?
No. MultiMail is a REST API and works with any HTTP client. Trigger.dev tasks use the global fetch function available in Node.js 18+. Install nothing beyond the Trigger.dev SDK itself.
What happens if a task fails mid-run after calling send_email?
If you provide an idempotency_key in the send_email request, a retry will not produce a second delivery. MultiMail returns the original message ID on duplicate requests with the same key. Use a stable value derived from your Trigger.dev run ID or a hash of the payload.
Can I use Trigger.dev's scheduled tasks to poll for inbound email?
Yes. Use schedules.task with a cron expression to call check_inbox at a fixed interval, then read_email for each message ID returned. Because Trigger.dev retries failed runs automatically, a transient MultiMail API error won't leave messages unprocessed.
How does gated_send mode interact with long-running Trigger.dev tasks?
In gated_send mode, send_email queues the message rather than delivering it immediately and returns a message_id. Use wait.for to suspend the task for the expected approval window, then call list_pending to check whether the message was approved, cancelled, or is still awaiting review.
Does MultiMail handle email deliverability — SPF, DKIM, DMARC?
Yes. MultiMail manages SPF, DKIM, and DMARC alignment for all mailboxes. Custom domain mailboxes require DNS records provided in the dashboard; multimail.dev subdomains work without any DNS configuration.
Can I trace which Trigger.dev job sent which email for a compliance audit?
Yes. Embed a job identifier in the idempotency_key or a metadata field on each send_email call. MultiMail logs every API call with the key, mailbox, timestamp, and outcome. For SOC 2 or GDPR audit trails you can correlate Trigger.dev run IDs with MultiMail message IDs.
What oversight mode should I start with for a new agentic workflow?
Start with gated_send: reads are autonomous but sends require human approval. This lets you review what the agent sends before committing to autonomous delivery. Promote to monitored after validating output over several runs, then to autonomous when confidence is established.

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.