Pair Temporal's durable execution model with MultiMail's graduated oversight to build email workflows that pause for approval, survive process crashes, and resume exactly where they left off.
Temporal is a durable workflow engine that persists every step of a long-running process. When an AI agent needs to send an email that requires human approval, Temporal can pause the workflow indefinitely—surviving restarts, deploys, and network failures—then resume the moment approval arrives.
MultiMail's `gated_send` and `gated_all` oversight modes map directly onto Temporal's timer and signal primitives. A workflow activity calls `send_email` via the MultiMail API, receives a pending approval ID, then waits on a Temporal signal that fires when the human approves or rejects. No polling loops, no external state storage, no lost approvals.
This pattern is especially useful for high-stakes outbound email in regulated industries: loan offer notifications, patient communications, contract delivery. Temporal guarantees at-least-once execution of each activity; MultiMail guarantees the human stays in the loop before anything leaves the system.
MultiMail's approval queue exposes webhook events when a human approves or rejects a pending send. These events map cleanly to Temporal workflow signals, letting you pause a workflow at the send step and resume it without any external coordination logic.
Human approval may take hours or days. Temporal workflows can pause for weeks without losing state. Combined with MultiMail's persistent approval queue, you never lose a pending send because a worker restarted.
Start with `gated_all` while building confidence, then promote a mailbox to `monitored` or `autonomous` via the MultiMail API. Temporal workflow logic doesn't need to change—oversight behavior is a configuration property on the mailbox, not embedded in workflow code.
Temporal retries failed activities automatically, but MultiMail's idempotency keys prevent duplicate email delivery. Pass a deterministic key derived from the workflow ID and activity attempt number to ensure exactly-once delivery semantics.
MultiMail's `get_thread` endpoint retrieves complete conversation history. Temporal activities can fetch this context at any step, letting an AI agent craft replies with full awareness of prior exchanges without re-fetching from upstream sources.
import { proxyActivities, defineSignal, condition, setHandler } from '@temporalio/workflow';
import type { EmailActivities } from './activities';
const { sendEmailForApproval, recordApprovalOutcome } = proxyActivities<EmailActivities>({
startToCloseTimeout: '30 seconds',
});
export const approvalSignal = defineSignal<[{ approved: boolean; approver: string }]>('approval');
export async function emailApprovalWorkflow(params: {
to: string;
subject: string;
body: string;
mailboxId: string;
}): Promise<{ sent: boolean; messageId: string | null }> {
const { pendingId, messageId } = await sendEmailForApproval(params);
let approved = false;
let approver = '';
let signalReceived = false;
setHandler(approvalSignal, ({ approved: a, approver: ap }) => {
approved = a;
approver = ap;
signalReceived = true;
});
"cm">// Wait up to 72 hours for human approval
await condition(() => signalReceived, '72 hours');
await recordApprovalOutcome({ pendingId, approved, approver, messageId });
return { sent: approved, messageId: approved ? messageId : null };
}A Temporal workflow that sends an email via MultiMail in `gated_send` mode, waits for the approval signal, and records the outcome. The workflow can pause for days awaiting human review without losing state.
import { Context } from '@temporalio/activity';
const MULTIMAIL_API = 'https://api.multimail.dev';
const MM_TOKEN = process.env.MULTIMAIL_API_KEY!;
export interface EmailActivities {
sendEmailForApproval(params: {
to: string;
subject: string;
body: string;
mailboxId: string;
}): Promise<{ pendingId: string; messageId: string }>;
recordApprovalOutcome(params: {
pendingId: string;
approved: boolean;
approver: string;
messageId: string;
}): Promise<void>;
}
export const emailActivities: EmailActivities = {
async sendEmailForApproval({ to, subject, body, mailboxId }) {
const info = Context.current().info;
"cm">// Deterministic idempotency key: workflow + activity attempt
const idempotencyKey = `${info.workflowExecution.workflowId}-${info.attempt}`;
const res = await fetch(`${MULTIMAIL_API}/send_email`, {
method: 'POST',
headers: {
Authorization: `Bearer ${MM_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
mailbox_id: mailboxId,
to,
subject,
body,
idempotency_key: idempotencyKey,
}),
});
if (!res.ok) throw new Error(`MultiMail send failed: ${res.status}`);
const data = await res.json() as { pending_id: string; message_id: string };
return { pendingId: data.pending_id, messageId: data.message_id };
},
async recordApprovalOutcome({ pendingId, approved, approver, messageId }) {
"cm">// Fetch the final state for audit logging
const res = await fetch(`${MULTIMAIL_API}/read_email?message_id=${messageId}`, {
headers: { Authorization: `Bearer ${MM_TOKEN}` },
});
const email = await res.json();
console.log({ pendingId, approved, approver, status: email.status });
},
};The activity implementations that call the MultiMail API. Activities are retried automatically by Temporal on failure; idempotency keys prevent duplicate sends on retry.
import { Client, Connection } from '@temporalio/client';
import { approvalSignal } from './workflows';
import { createHmac } from 'crypto';
const connection = await Connection.connect({ address: process.env.TEMPORAL_ADDRESS });
const client = new Client({ connection, namespace: process.env.TEMPORAL_NAMESPACE });
export async function handleMultimailWebhook(req: Request): Promise<Response> {
"cm">// Verify MultiMail webhook signature
const sig = req.headers.get('x-multimail-signature') ?? '';
const body = await req.text();
const expected = createHmac('sha256', process.env.MULTIMAIL_WEBHOOK_SECRET!)
.update(body)
.digest('hex');
if (sig !== expected) return new Response('Unauthorized', { status: 401 });
const event = JSON.parse(body) as {
type: 'approval.approved' | 'approval.rejected';
workflow_id: string;
approver_email: string;
pending_id: string;
};
if (event.type !== 'approval.approved' && event.type !== 'approval.rejected') {
return new Response('ignored', { status: 200 });
}
"cm">// workflow_id is set as a custom metadata field when the Temporal workflow starts
const handle = client.workflow.getHandle(event.workflow_id);
await handle.signal(approvalSignal, {
approved: event.type === 'approval.approved',
approver: event.approver_email,
});
return new Response('ok', { status: 200 });
}An HTTP endpoint that receives MultiMail approval webhooks and delivers them as Temporal workflow signals. Deploy this as a separate worker or API route.
import { Client } from '@temporalio/client';
import { emailApprovalWorkflow } from './workflows';
const MM_TOKEN = process.env.MULTIMAIL_API_KEY!;
export async function pollInboxAndDispatch(
client: Client,
mailboxId: string,
): Promise<void> {
const res = await fetch(
`https:"cm">//api.multimail.dev/check_inbox?mailbox_id=${mailboxId}&unread=true`,
{ headers: { Authorization: `Bearer ${MM_TOKEN}` } },
);
const { emails } = await res.json() as { emails: Array<{ id: string; from: string; subject: string }> };
for (const email of emails) {
const workflowId = `reply-${email.id}`;
"cm">// Idempotent start — skip if workflow already running
await client.workflow.start(emailApprovalWorkflow, {
taskQueue: 'email-agent',
workflowId,
args: [{
to: email.from,
subject: `Re: ${email.subject}`,
body: await generateReply(email.id),
mailboxId,
}],
});
}
}
async function generateReply(emailId: string): Promise<string> {
const res = await fetch(`https:"cm">//api.multimail.dev/read_email?message_id=${emailId}`, {
headers: { Authorization: `Bearer ${MM_TOKEN}` },
});
const { body } = await res.json() as { body: string };
"cm">// Replace with your LLM call
return `Thank you for your message. We will respond shortly.\n\nOriginal: ${body.slice(0, 200)}`;
}A Temporal activity that checks the MultiMail inbox and starts a new workflow for each unprocessed inbound email. Useful for intake pipelines where each arriving email kicks off a durable processing chain.
Install the Temporal TypeScript SDK and set up a worker that runs on the `email-agent` task queue. The worker registers your workflow and activity functions.
npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity
"cm"># Start a local Temporal server for development
npx @temporalio/create@latest --sample hello-world temporal-devCreate a mailbox via the MultiMail API with `gated_send` oversight so all outbound messages wait for human approval. Retrieve your API key from the MultiMail dashboard.
curl -X POST https://api.multimail.dev/create_mailbox \
-H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY' \
-H &"cm">#039;Content-Type: application/json' \
-d &"cm">#039;{
"address": "[email protected]",
"oversight_mode": "gated_send",
"display_name": "AI Agent"
}&"cm">#039;Configure MultiMail to deliver `approval.approved` and `approval.rejected` events to your webhook endpoint. This endpoint will signal your Temporal workflows when humans act on pending sends.
curl -X POST https://api.multimail.dev/webhooks \
-H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY' \
-H &"cm">#039;Content-Type: application/json' \
-d &"cm">#039;{
"url": "https://yourapp.com/webhooks/multimail",
"events": ["approval.approved", "approval.rejected"],
"secret": "your_webhook_signing_secret"
}&"cm">#039;Register your workflow and activities with the Temporal worker, deploy the webhook handler to your API, and start a Temporal workflow for each email that needs to be sent. Use the workflow ID as the correlation key between MultiMail events and Temporal executions.
import { Worker } from &"cm">#039;@temporalio/worker';
import { emailActivities } from &"cm">#039;./activities';
const worker = await Worker.create({
workflowsPath: require.resolve(&"cm">#039;./workflows'),
activities: emailActivities,
taskQueue: &"cm">#039;email-agent',
});
await worker.run();Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.