mailmolt
integration · mastra

Mastra

Give your Mastra agent an inbox. The TypeScript SDK plugs straight into typed Mastra tools; inbound webhooks become workflow inputs; evals score every reply on message.sent.

Install

pnpm add mailmolt @mastra/core

Define typed tools

import { createTool } from '@mastra/core';
import { z } from 'zod';
import { MailMolt } from 'mailmolt';

const mm = new MailMolt({ apiKey: process.env.MAILMOLT_API_KEY! });

export const sendEmail = createTool({
  id: 'send-email',
  description: 'Send an email from the MailMolt agent identity',
  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string(),
    text: z.string(),
  }),
  execute: async ({ context }) => {
    const r = await mm.sendMessage({
      to: context.to,
      subject: context.subject,
      text: context.text,
    });
    return { id: r.id, status: r.status };
  },
});

export const searchInbox = createTool({
  id: 'search-inbox',
  description: 'Semantic search over the agent inbox',
  inputSchema: z.object({ q: z.string(), limit: z.number().default(5) }),
  execute: async ({ context }) => mm.search({ q: context.q, limit: context.limit }),
});

export const replyTo = createTool({
  id: 'reply-to',
  description: 'Reply in-thread to a message',
  inputSchema: z.object({ messageId: z.string(), text: z.string() }),
  execute: async ({ context }) =>
    mm.replyMessage({ message_id: context.messageId, text: context.text }),
});

Wire into an agent

import { Agent } from '@mastra/core';
import { anthropic } from '@ai-sdk/anthropic';
import { sendEmail, searchInbox, replyTo } from './email-tools';

export const inboxAgent = new Agent({
  name: 'inbox-agent',
  instructions:
    'You have a MailMolt email identity. Use search-inbox to find context, ' +
    'reply-to or send-email to respond. Keep replies under 3 sentences ' +
    'unless asked for more.',
  model: anthropic('claude-sonnet-4-6'),
  tools: { sendEmail, searchInbox, replyTo },
});

Webhook → Mastra workflow

import { createWorkflow, createStep } from '@mastra/core';
import { z } from 'zod';

const handleInbound = createStep({
  id: 'handle-inbound',
  inputSchema: z.object({ event: z.any() }),
  execute: async ({ inputData }) => {
    const { event } = inputData;
    if (event.event_type !== 'message.received') return { skipped: true };
    const msg = event.data.message;
    const resp = await inboxAgent.generate(
      `Reply to ${msg.from.email}. Subject: ${msg.subject}\n\n${msg.preview}`,
    );
    return { replied: true, text: resp.text };
  },
});

export const inboundWorkflow = createWorkflow({
  id: 'inbound-email',
  inputSchema: z.object({ event: z.any() }),
}).then(handleInbound).commit();

Register the webhook

await mm.createWebhook({
  url: 'https://your-host.com/api/webhooks/mailmolt',
  event_types: ['message.received', 'message.sent'],
});

Evals on message.sent

import { evaluate } from '@mastra/evals';

// Score every reply on tone and brevity once it actually leaves the platform.
async function onMessageSent(event: any) {
  const msg = event.data.message;
  await evaluate({
    output: msg.text,
    metrics: ['tone-professional', 'concise'],
    metadata: { messageId: msg.id, threadId: msg.thread_id },
  });
}

Notes

  • Mastra's evals slot in cleanly — score reply quality on message.sent for closed-loop training data.
  • For keyword lookups, prefer mm.searchThreads() (/v1/search/threads) — semantic search costs more and isn't needed for exact matches.
  • Approval-gated agents: sendMessage still returns an id, but the message waits in oversight until approved.
  • Threads are RFC-correct — replyTo sets In-Reply-To/References so Gmail/Outlook stitch threads correctly.

Full cookbook: docs/integrations/mastra.md · webhook reference