Email infrastructure for Mastra agents

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.

Built for Mastra developers

Oversight built for autonomous workflows

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.

Real mailboxes, not SMTP relay

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.

Tool-shaped API

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.

Graduated trust as workflow state

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.

Formally verified security model

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.


Get started in minutes

Define MultiMail tools for a Mastra agent
typescript
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.

Build a Mastra agent with email tools
typescript
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.

Mastra workflow with approval-gated email
typescript
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.

Inbound email webhook → Mastra workflow trigger
typescript
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.


Step by step

1

Create a MultiMail account and get an API key

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.

bash
export MULTIMAIL_API_KEY=$MULTIMAIL_API_KEY
2

Install Mastra and scaffold your project

Mastra 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.

bash
npx create-mastra@latest my-email-agent
cd my-email-agent
npm install
3

Define MultiMail tools

Create 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`.

bash
// 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();
  },
});
4

Attach tools to an agent

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.

bash
// 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 },
});
5

Configure a webhook for inbound mail

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.

bash
"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"]}'

Common questions

Does MultiMail have an official Mastra integration package?
No. You define MultiMail as Mastra tools by wrapping the REST API with `createTool` and Zod schemas. This is the idiomatic Mastra pattern for external services — it keeps the integration in your codebase and gives you full control over which endpoints you expose to the agent.
How does `gated_send` mode interact with a Mastra workflow?
When `send_email` is called on a mailbox in `gated_send` mode, MultiMail queues the message and returns `{ status: 'pending_approval', message_id: '...' }` immediately. Your workflow receives that response and can poll `list_pending`, wait for a webhook event (`email.approved` or `email.rejected`), or suspend the run until the approval arrives. The message does not leave MultiMail until a human approves it in the dashboard or via `POST /decide_email`.
Can I use MultiMail's MCP server with Mastra instead of the REST API?
Mastra is a tool-calling framework that integrates with MCP servers via its tool system. MultiMail's MCP server exposes 50 tools including `send_email`, `check_inbox`, and `decide_email`. If your Mastra setup already uses MCP tool discovery, you can connect to `https://mcp.multimail.dev/mcp` and get all MultiMail capabilities without writing individual tool wrappers.
How do I handle inbound email in a Mastra workflow?
Configure a MultiMail webhook pointing to your server. When MultiMail receives mail for your mailbox, it POSTs a JSON event with `type: 'email.received'` and the full message in `data`. Your server handler calls `mastra.getWorkflow('...').createRun()` to start a workflow run with the email payload as input. This is the standard pattern for event-triggered Mastra workflows.
Do I need to provision mailboxes through the MultiMail dashboard?
You can provision mailboxes programmatically via `POST https://api.multimail.dev/create_mailbox`. This is useful if your Mastra workflow needs to create a per-tenant or per-session mailbox at runtime. The `create_mailbox` call returns the mailbox address and lets you set the oversight mode at creation time.
What oversight mode should I use during development?
Use `gated_all` during development so every action — reads, sends, replies — requires explicit approval. This gives you a complete audit trail while you're still debugging tool behavior. Promote to `gated_send` when reads are reliable, and to `monitored` only when you've validated the full workflow in staging with real message content.

Explore more

The only agent email with a verifiable sender

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