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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
"cm"># Dashboard: Settings → API Keys → Create → copy mm_live_... key
"cm"># Dashboard: Mailboxes → New Mailbox
"cm"># Address: [email protected]
"cm"># Oversight mode: gated_sendStore 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.
"cm"># Trigger.dev dashboard: Environment → Secrets → Add secret
"cm"># Key: MULTIMAIL_API_KEY
"cm"># Value: mm_live_...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.
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();
},
});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.
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 → ApproveOnce 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.
"cm"># Dashboard: Mailboxes → [email protected] → Oversight mode → autonomous
"cm"># Tasks are unchanged — oversight mode is a mailbox setting, not a code settingEmail infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.