Email actions grounded in your Notion workspace

AI agents read Notion pages and databases to build context, then send or route email through MultiMail with the right oversight mode applied.


Notion serves as the knowledge layer for many AI agent workflows — task lists, CRM records, project wikis, and routing policies all live there. When an agent needs to act on that knowledge by sending email, it needs a delivery API that can handle the gap between "I read the context" and "I sent something I shouldn't have."

MultiMail sits at the delivery end of that pipeline. An agent queries a Notion database to find open action items, reads the relevant pages for context, then calls MultiMail's send_email or decide_email endpoints with the appropriate oversight mode applied. Notion informs the draft; MultiMail decides whether and how it ships.

This pattern appears in outreach automation, customer success tooling, and internal ops agents. The agent pulls structured data — assignee, deadline, status — from a Notion database, constructs a tailored message, and hands off to MultiMail, which can gate the send on human approval, run monitored, or operate fully autonomously depending on how much trust the workflow has earned.

Built for Notion developers

Context-aware sending with approval gates

Notion provides the signal for what to write. MultiMail's gated_send mode ensures that context-informed drafts still require human sign-off before delivery. The two layers are independent: adjust the Notion query logic without touching the oversight policy, or tighten oversight without changing what the agent reads.

Notion database properties map directly to email fields

Structured Notion properties — assignee email, due date, priority tag — map cleanly to MultiMail API parameters. An agent can read a database row and call send_email without an intermediate transformation step.

Oversight modes that match agent maturity

Start with gated_all while your agent is learning the workflow. Promote to gated_send once sends look correct. Run monitored or autonomous once you trust the output. oversight_mode is a per-request parameter, not a system-wide config change.

decide_email closes the routing loop

When an agent reads a Notion routing table to determine how to handle inbound email, MultiMail's decide_email endpoint executes that decision — archive, tag, forward, or reply — without requiring a separate delivery call.

Bidirectional webhook-to-Notion sync

MultiMail fires webhooks on inbound email, approval events, and delivery status. Agents can use these to update Notion database rows — logging message IDs, changing status properties, or creating follow-up tasks — so your workspace stays in sync with your email queue.


Get started in minutes

Read a Notion CRM database and send outreach email
javascript
import { Client } from '@notionhq/client';

const notion = new Client({ auth: process.env.NOTION_TOKEN });

async function sendOutreachFromNotion(databaseId) {
  const response = await notion.databases.query({
    database_id: databaseId,
    filter: {
      property: 'Status',
      select: { equals: 'Ready to Contact' }
    }
  });

  for (const page of response.results) {
    const name = page.properties.Name.title[0]?.plain_text;
    const email = page.properties.Email.email;
    const notes = page.properties.Notes.rich_text[0]?.plain_text ?? '';

    if (!email) continue;

    const res = await fetch('https://api.multimail.dev/v1/send_email', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        from: '[email protected]',
        to: email,
        subject: `Following up — ${name}`,
        text: `Hi ${name},\n\n${notes}\n\nLet me know if you have questions.`,
        oversight_mode: 'gated_send',
        metadata: { notion_page_id: page.id }
      })
    });

    const { message_id, status } = await res.json();
    console.log(`${message_id}: ${status}`);
    "cm">// status = 'pending_approval' — approval webhook fires when human decides
  }
}

Query a Notion database for contacts with a specific status, then send a tailored email via MultiMail for each result. Uses gated_send so each send requires human approval before delivery.

Read a Notion page and draft a context-grounded status update
javascript
import { Client } from '@notionhq/client';

const notion = new Client({ auth: process.env.NOTION_TOKEN });

async function draftStatusUpdate(pageId, recipientEmail) {
  const [pageResp, blocksResp] = await Promise.all([
    notion.pages.retrieve({ page_id: pageId }),
    notion.blocks.children.list({ block_id: pageId })
  ]);

  const title = pageResp.properties.title?.title[0]?.plain_text ?? 'Project Update';
  const body = blocksResp.results
    .filter(b => b.type === 'paragraph')
    .map(b => b.paragraph.rich_text.map(t => t.plain_text).join(''))
    .filter(Boolean)
    .join('\n\n');

  const res = await fetch('https://api.multimail.dev/v1/send_email', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      from: '[email protected]',
      to: recipientEmail,
      subject: `Status: ${title}`,
      text: `Project: ${title}\n\n${body}`,
      oversight_mode: 'gated_all',
      metadata: { notion_page_id: pageId }
    })
  });

  const { message_id, status } = await res.json();
  console.log(`${message_id}: ${status}`);
}

Fetch a Notion project page, extract paragraph blocks as body text, then submit a status update email under gated_all mode. Reviewers see the exact Notion content before approving.

Route inbound email using a Notion routing table
javascript
import { Client } from '@notionhq/client';

const notion = new Client({ auth: process.env.NOTION_TOKEN });

"cm">// Called from your MultiMail inbound webhook handler
export async function handleInbound(webhookPayload) {
  const { email_id, from } = webhookPayload;
  const senderDomain = from.split('@')[1];

  const routing = await notion.databases.query({
    database_id: process.env.ROUTING_DATABASE_ID,
    filter: {
      property: 'Domain Pattern',
      rich_text: { contains: senderDomain }
    }
  });

  let decision = 'archive';
  let assignTo = null;

  if (routing.results.length > 0) {
    const rule = routing.results[0];
    decision = rule.properties.Action.select?.name ?? 'archive';
    assignTo = rule.properties.AssignTo.email ?? null;
  }

  await fetch('https://api.multimail.dev/v1/decide_email', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email_id,
      decision,
      assign_to: assignTo,
      reason: `Matched Notion routing rule for domain ${senderDomain}`
    })
  });
}

On inbound email, query a Notion database that maps sender domains to routing actions, then call MultiMail's decide_email endpoint to execute the result. The Notion table is a human-editable policy your agent reads at runtime.

Update Notion on approval and tag email
javascript
import { Client } from '@notionhq/client';

const notion = new Client({ auth: process.env.NOTION_TOKEN });

export async function onApprovalEvent(event) {
  if (event.type !== 'message.approved') return;

  const { message_id, metadata } = event;

  "cm">// Tag the email in MultiMail
  await fetch('https://api.multimail.dev/v1/tag_email', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email_id: message_id,
      tags: ['sent-approved', 'notion-tracked']
    })
  });

  "cm">// Write outcome back to Notion if we have a page ID in metadata
  if (metadata?.notion_page_id) {
    await notion.pages.update({
      page_id: metadata.notion_page_id,
      properties: {
        Status: { select: { name: 'Email Sent' } },
        MessageId: { rich_text: [{ text: { content: message_id } }] },
        SentAt: { date: { start: new Date().toISOString() } }
      }
    });
  }
}

When MultiMail fires a message.approved webhook, tag the email and write the outcome back to the originating Notion page. Keeps your workspace in sync with what actually shipped.


Step by step

1

Install the Notion SDK and set credentials

Install the official Notion JavaScript client and create an internal integration at developers.notion.com. Share the relevant databases with that integration, then set your tokens as environment variables.

bash
npm install @notionhq/client

export NOTION_TOKEN=secret_...
export MULTIMAIL_API_KEY=mm_live_...
2

Create a dedicated mailbox for your agent

Give your agent a stable send address using MultiMail's create_mailbox endpoint. Set oversight_mode to gated_send for first-run safety — you can relax it once you've reviewed a batch of drafts.

bash
curl -X POST https://api.multimail.dev/v1/create_mailbox \
  -H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY...' \
  -H &"cm">#039;Content-Type: application/json' \
  -d &"cm">#039;{
    "address": "[email protected]",
    "display_name": "Notion Workflow Agent",
    "oversight_mode": "gated_send"
  }&"cm">#039;
3

Query Notion and send your first gated message

Pull a record from your Notion database and pass the relevant fields to MultiMail's send_email endpoint. The message will land in pending_approval state until a human approves it.

bash
import { Client } from &"cm">#039;@notionhq/client';

const notion = new Client({ auth: process.env.NOTION_TOKEN });

const { results } = await notion.databases.query({
  database_id: &"cm">#039;YOUR_DATABASE_ID',
  filter: { property: &"cm">#039;Status', select: { equals: 'Action Required' } }
});

const page = results[0];
const email = page.properties.Email.email;
const name = page.properties.Name.title[0]?.plain_text;

const res = await fetch(&"cm">#039;https://api.multimail.dev/v1/send_email', {
  method: &"cm">#039;POST',
  headers: {
    &"cm">#039;Authorization': `Bearer ${process.env.MULTIMAIL_API_KEY}`,
    &"cm">#039;Content-Type': 'application/json'
  },
  body: JSON.stringify({
    from: &"cm">#039;[email protected]',
    to: email,
    subject: `Action required: ${name}`,
    text: `Hi ${name}, this is an automated follow-up from your project workspace.`,
    oversight_mode: &"cm">#039;gated_send',
    metadata: { notion_page_id: page.id }
  })
});

console.log(await res.json());
4

Register approval webhooks

Subscribe to MultiMail approval and delivery events so your agent can react to outcomes — updating Notion records, creating follow-up tasks, or escalating rejections.

bash
curl -X POST https://api.multimail.dev/v1/webhooks \
  -H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY...' \
  -H &"cm">#039;Content-Type: application/json' \
  -d &"cm">#039;{
    "url": "https://your-worker.yourdomain.workers.dev/webhooks/multimail",
    "events": ["message.approved", "message.rejected", "message.delivered", "email.inbound"]
  }&"cm">#039;

Common questions

Does MultiMail have a native Notion integration?
No. MultiMail is a REST API — the Notion connection lives in your agent's code. Your agent reads from Notion using the Notion SDK, then calls MultiMail endpoints to send, reply, or decide on email. This keeps the integration under your control: you can change the Notion query logic without touching the email delivery layer, and vice versa.
Can I use a Notion database as a live email routing policy?
Yes. A common pattern is a Notion database where each row defines a routing rule — domain pattern, keyword match, or sender tag mapped to an action (archive, tag, forward). Your inbound webhook handler queries the database at runtime, determines the correct action, and calls MultiMail's decide_email endpoint. Non-engineers can edit the Notion table to change routing behavior without touching code.
What oversight mode should I start with?
Start with gated_all. Every send and reply requires human approval, which lets you audit the agent's decisions before anything reaches a recipient. Once the output looks correct across a representative sample, move to gated_send (reads are autonomous, sends require approval). Run monitored or autonomous only after validating the full workflow end-to-end.
How do I keep Notion records in sync with email delivery outcomes?
Pass a notion_page_id in the metadata field when calling send_email. When MultiMail fires a message.delivered or message.approved webhook, your handler reads metadata.notion_page_id and calls notion.pages.update to set the Status property, log the message_id, or create a follow-up task. The metadata object is returned verbatim in all webhook payloads.
Does this work with Python as well as JavaScript?
Yes. Install the notion-client Python package for Notion access. For MultiMail you can use the multimail-sdk Python package or call the REST API directly with httpx or requests. The oversight_mode parameter, endpoint paths, and webhook payload shapes are identical regardless of language.
What happens if the Notion API is slow or unavailable when my agent tries to send?
MultiMail and Notion are independent services — a Notion timeout does not affect MultiMail's ability to accept or deliver messages. If your agent fails to fetch Notion context before calling send_email, you should handle the Notion error in your agent code and either retry or skip that send. Consider caching Notion responses in KV for routing rules that change infrequently.

Explore more

The only agent email with a verifiable sender

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