Mastra gives you the primitives to build agent workflows. MultiMail adds the email delivery controls those workflows need — including graduated oversight when agents send customer-facing messages.
Mastra is a TypeScript framework that treats agents, tools, workflows, and memory as first-class application primitives. It's designed to make production AI systems feel like ordinary application code — typed, testable, and deployable alongside your existing stack.
Email is a natural fit for Mastra workflows: outreach sequences, notification pipelines, customer support drafts, and approval chains all involve sending or reading messages on behalf of a user or product. But Mastra itself doesn't provide email infrastructure — it expects you to wire in the services your workflow needs.
MultiMail fills that gap. You expose the MultiMail REST API as Mastra tools, and your agents call `send_email`, `check_inbox`, `read_email`, or `decide_email` the same way they call any other tool. If the workflow touches customer-facing messages, you configure oversight modes so humans approve sends before they leave.
Mastra workflows can run without a human in the loop. When those workflows draft emails, MultiMail's `gated_send` mode routes sends to an approval queue before delivery. Your workflow continues; the message waits. When the human approves via the MultiMail dashboard or API, the send completes and a webhook fires back to your workflow.
MultiMail provisions actual mailboxes — your own domain or `@multimail.dev`. Inbound email arrives as structured JSON via webhook, so your Mastra agent can call `check_inbox` or receive push events without parsing raw MIME. Threads, metadata, and attachment references are all first-class fields.
Every MultiMail operation maps cleanly to a Mastra tool: `send_email`, `reply_email`, `read_email`, `get_thread`, `tag_email`, `manage_contacts`. Each takes a JSON input and returns a JSON result, which is exactly what Mastra's tool executor expects. No adapters, no glue layers.
You can set oversight mode per mailbox or per API call. Start a new agent workflow in `gated_all` mode — every action requires approval — then promote to `gated_send` or `monitored` as confidence grows. The mode is a parameter you control in code, not a global setting you change in a dashboard.
MultiMail's authorization and oversight models are proven correct in Lean 4. That means the invariants you rely on — an agent cannot send without approval in `gated_send` mode, a `read_only` agent cannot call `send_email` — are machine-checked, not just documented.
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
const MM_API = 'https://api.multimail.dev';
const headers = {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json',
};
export const sendEmailTool = createTool({
id: 'send_email',
description: 'Send an email from a MultiMail mailbox. Respects the mailbox oversight mode — sends may be queued for approval.',
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
from_mailbox: z.string().default('[email protected]'),
}),
execute: async ({ context }) => {
const res = await fetch(`${MM_API}/send_email`, {
method: 'POST',
headers,
body: JSON.stringify(context),
});
return res.json();
},
});
export const checkInboxTool = createTool({
id: 'check_inbox',
description: 'List recent emails in a MultiMail mailbox.',
inputSchema: z.object({
mailbox: z.string().default('[email protected]'),
limit: z.number().int().min(1).max(50).default(10),
}),
execute: async ({ context }) => {
const params = new URLSearchParams({
mailbox: context.mailbox,
limit: String(context.limit),
});
const res = await fetch(`${MM_API}/check_inbox?${params}`, { headers });
return res.json();
},
});
export const readEmailTool = createTool({
id: 'read_email',
description: 'Fetch the full content of a single email by ID.',
inputSchema: z.object({ email_id: z.string() }),
execute: async ({ context }) => {
const res = await fetch(`${MM_API}/read_email/${context.email_id}`, { headers });
return res.json();
},
});Wrap the MultiMail REST API as Mastra tools using `createTool`. Each tool maps one-to-one with a MultiMail endpoint.
import { Agent } from '@mastra/core/agent';
import { openai } from '@ai-sdk/openai';
import { sendEmailTool, checkInboxTool, readEmailTool } from './multimail-tools';
export const supportAgent = new Agent({
name: 'SupportAgent',
instructions: `
You are a customer support agent. You can read the inbox, understand customer issues,
and draft or send replies. When a reply involves a refund, billing change, or account
action, send it — the mailbox is configured in gated_send mode so a human will
approve before delivery. For routine status updates, send directly.
`,
model: openai('gpt-4o'),
tools: {
send_email: sendEmailTool,
check_inbox: checkInboxTool,
read_email: readEmailTool,
},
});
"cm">// Run the agent
const result = await supportAgent.generate(
'Check the support inbox and draft a reply to any unanswered tickets from the last 24 hours.'
);
console.log(result.text);Attach the MultiMail tools to a Mastra Agent. The agent can now read mail, draft replies, and send messages — subject to whatever oversight mode the mailbox uses.
import { createWorkflow, createStep } from '@mastra/core/workflows';
import { z } from 'zod';
const MM_API = 'https://api.multimail.dev';
const headers = {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
'Content-Type': 'application/json',
};
const draftOutreach = createStep({
id: 'draft_outreach',
inputSchema: z.object({ prospect_email: z.string().email(), context: z.string() }),
outputSchema: z.object({ message_id: z.string(), status: z.string() }),
execute: async ({ inputData }) => {
"cm">// Send with oversight — mailbox is in gated_send mode
const res = await fetch(`${MM_API}/send_email`, {
method: 'POST',
headers,
body: JSON.stringify({
to: inputData.prospect_email,
from_mailbox: '[email protected]',
subject: 'Following up on your request',
body: inputData.context,
}),
});
const data = await res.json();
"cm">// status will be 'pending_approval' if oversight mode requires it
return { message_id: data.message_id, status: data.status };
},
});
const waitForApproval = createStep({
id: 'wait_for_approval',
inputSchema: z.object({ message_id: z.string(), status: z.string() }),
outputSchema: z.object({ approved: z.boolean(), message_id: z.string() }),
execute: async ({ inputData }) => {
if (inputData.status !== 'pending_approval') {
return { approved: true, message_id: inputData.message_id };
}
"cm">// Poll list_pending until the message is no longer queued
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 5000));
const res = await fetch(`${MM_API}/list_pending`, { headers });
const { pending } = await res.json();
const still_waiting = pending.some((m: { id: string }) => m.id === inputData.message_id);
if (!still_waiting) return { approved: true, message_id: inputData.message_id };
}
return { approved: false, message_id: inputData.message_id };
},
});
export const outreachWorkflow = createWorkflow({
id: 'gated_outreach',
inputSchema: z.object({ prospect_email: z.string().email(), context: z.string() }),
outputSchema: z.object({ approved: z.boolean(), message_id: z.string() }),
})
.then(draftOutreach)
.then(waitForApproval)
.commit();Use a Mastra workflow to orchestrate a multi-step email sequence. The `decide_email` step holds execution until the human approves or rejects the queued send.
import { Hono } from 'hono';
import { outreachWorkflow } from './workflows/outreach';
import { mastra } from './mastra';
const app = new Hono();
"cm">// MultiMail posts to this endpoint when a new email arrives
app.post('/webhooks/multimail/inbound', async (c) => {
const event = await c.req.json();
if (event.type !== 'email.received') {
return c.json({ ok: true });
}
const { from, subject, body_text, email_id } = event.data;
"cm">// Trigger a Mastra workflow to handle the inbound message
const run = await mastra.getWorkflow('support_triage').createRun();
await run.start({
inputData: {
from,
subject,
body: body_text,
email_id,
},
});
return c.json({ ok: true, run_id: run.runId });
});
export default app;Configure MultiMail to POST inbound email events to your server, then trigger a Mastra workflow for each new message.
Sign up at multimail.dev. Your API key starts with `mm_live_` for production or `mm_test_` for sandbox. Set it as an environment variable in your Mastra project.
export MULTIMAIL_API_KEY=$MULTIMAIL_API_KEYMastra has a CLI that generates a project with agents, tools, and workflows pre-wired. No separate SDK install is needed for MultiMail — you call the REST API directly from your tools.
npx create-mastra@latest my-email-agent
cd my-email-agent
npm installCreate a `src/tools/multimail.ts` file that wraps the MultiMail REST endpoints as Mastra tools with Zod schemas. Export the tools you need: `sendEmailTool`, `checkInboxTool`, `readEmailTool`, `decideEmailTool`.
// src/tools/multimail.ts
import { createTool } from &"cm">#039;@mastra/core/tools';
import { z } from &"cm">#039;zod';
const base = &"cm">#039;https://api.multimail.dev';
const h = {
Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
&"cm">#039;Content-Type': 'application/json',
};
export const sendEmailTool = createTool({
id: &"cm">#039;send_email',
description: &"cm">#039;Send an email via MultiMail. Subject to mailbox oversight mode.',
inputSchema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
from_mailbox: z.string(),
}),
execute: async ({ context }) => {
const res = await fetch(`${base}/send_email`, {
method: &"cm">#039;POST', headers: h, body: JSON.stringify(context),
});
return res.json();
},
});Import your MultiMail tools into a Mastra Agent definition. The agent's system prompt should explain what oversight mode to expect so it handles `pending_approval` responses correctly.
// src/agents/email-agent.ts
import { Agent } from &"cm">#039;@mastra/core/agent';
import { openai } from &"cm">#039;@ai-sdk/openai';
import { sendEmailTool, checkInboxTool, readEmailTool } from &"cm">#039;../tools/multimail';
export const emailAgent = new Agent({
name: &"cm">#039;EmailAgent',
instructions: &"cm">#039;You manage a shared inbox. Sends are gated — if status is pending_approval, inform the user that a human must approve before delivery.',
model: openai(&"cm">#039;gpt-4o'),
tools: { send_email: sendEmailTool, check_inbox: checkInboxTool, read_email: readEmailTool },
});In the MultiMail dashboard, set your inbound webhook URL to your server's `/webhooks/multimail/inbound` endpoint. MultiMail will POST structured JSON for each received message, which you can use to trigger Mastra workflow runs.
"cm"># Verify the webhook is reachable
curl -X POST https://api.multimail.dev/mailboxes/[email protected]/webhooks \
-H "Authorization: Bearer $MULTIMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d &"cm">#039;{"url": "https://your-app.com/webhooks/multimail/inbound", "events": ["email.received", "email.approved"]}'Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 50-tool MCP server. Formally verified in Lean 4.