Email infrastructure for Agno agents and teams

Give your Agno agents real email access—with oversight modes that let you control exactly how much autonomy each agent gets over outbound messages.


Agno is a Python framework for building production-ready AI agents with tools, memory, knowledge bases, and multi-agent teams. Its emphasis on practical usability over experimental abstractions makes it a natural fit for agents that need to interact with external systems like email.

MultiMail provides a REST API and webhook infrastructure designed specifically for AI agents. Where most email APIs assume a human is composing and sending messages, MultiMail is built around the reality that an agent may be doing the composing—and a human may want to approve the result before it leaves your system.

When you combine Agno teams with MultiMail, you get a clean boundary: agents handle research, drafting, and routing decisions, while MultiMail enforces which messages actually get delivered. A support agent, a research agent, and an operations agent can all share access to a mailbox without any of them being able to send without your approval unless you explicitly grant it.

Built for Agno developers

Centralized oversight across agent teams

Agno teams can spawn multiple agents that all want to send email. MultiMail's oversight modes—gated_send, gated_all, monitored, autonomous—apply at the mailbox level, so you configure the trust boundary once rather than patching it into every agent's tool logic.

Approval queue for outbound messages

Set oversight_mode to gated_send and every outbound message from your Agno agents lands in a review queue before delivery. Use the list_pending and decide_email endpoints to approve or reject from a dashboard, webhook, or another agent acting as a reviewer.

Inbound webhooks your agents can react to

MultiMail delivers inbound email to a webhook endpoint you control. Your Agno agent can receive the payload, decide whether to reply or tag or escalate, and call the appropriate API endpoint—all within the same tool-calling loop it already uses.

Thread-aware context for long conversations

The get_thread endpoint returns the full message history for a thread. Agno agents can load this as context before replying, which keeps replies coherent across multi-turn email conversations without you manually stitching messages together.

Identity verification on inbound senders

MultiMail checks DKIM, SPF, and DMARC on every inbound message and exposes the results in the read_email payload. Your agents can use this signal to decide whether to trust a message's claimed sender before acting on instructions embedded in it.


Get started in minutes

Basic Agno agent with email send tool
python
import requests
from agno.agent import Agent
from agno.models.openai import OpenAIChat

MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"

def send_email(to: str, subject: str, body: str) -> dict:
    """Send an email via MultiMail. Returns message_id and status."""
    resp = requests.post(
        f"{MULTIMAIL_BASE}/send_email",
        headers={"Authorization": f"Bearer {MULTIMAIL_API_KEY}"},
        json={
            "from": "[email protected]",
            "to": to,
            "subject": subject,
            "body": body,
        },
    )
    resp.raise_for_status()
    return resp.json()

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[send_email],
    description="You are an outreach agent. Use send_email when you need to contact someone.",
)

agent.print_response(
    "Send a follow-up email to [email protected] about the Q2 integration demo."
)

Define a send_email function that wraps the MultiMail REST API and register it as a tool on an Agno agent. The agent can then call it during any run.

Reading the inbox and replying to a thread
python
import requests
from agno.agent import Agent
from agno.models.openai import OpenAIChat

MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
HEADERS = {"Authorization": f"Bearer {MULTIMAIL_API_KEY}"}

def check_inbox(mailbox: str, limit: int = 10) -> list:
    """Return recent messages for a mailbox."""
    resp = requests.get(
        f"{MULTIMAIL_BASE}/check_inbox",
        headers=HEADERS,
        params={"mailbox": mailbox, "limit": limit},
    )
    resp.raise_for_status()
    return resp.json()["messages"]

def get_thread(thread_id: str) -> list:
    """Return all messages in a thread, oldest first."""
    resp = requests.get(
        f"{MULTIMAIL_BASE}/get_thread/{thread_id}",
        headers=HEADERS,
    )
    resp.raise_for_status()
    return resp.json()["messages"]

def reply_email(message_id: str, body: str) -> dict:
    """Reply to an existing message, preserving the thread."""
    resp = requests.post(
        f"{MULTIMAIL_BASE}/reply_email",
        headers=HEADERS,
        json={"message_id": message_id, "body": body},
    )
    resp.raise_for_status()
    return resp.json()

support_agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[check_inbox, get_thread, reply_email],
    description="You are a support agent. Check the inbox, read the full thread before replying, and keep replies concise.",
)

support_agent.print_response(
    "Check [email protected] for any open questions and reply to each one."
)

Check for new messages, load the full thread for context, and reply—all using MultiMail endpoints as Agno tools.

Agno team with centralized email oversight
python
import requests
from agno.agent import Agent
from agno.team import Team
from agno.models.openai import OpenAIChat

MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
HEADERS = {"Authorization": f"Bearer {MULTIMAIL_API_KEY}"}

"cm"># Mailbox is configured with oversight_mode=gated_send in the MultiMail dashboard.
"cm"># Calls to send_email will queue the message for human approval, not deliver it immediately.

def send_email(to: str, subject: str, body: str) -> dict:
    resp = requests.post(
        f"{MULTIMAIL_BASE}/send_email",
        headers=HEADERS,
        json={"from": "[email protected]", "to": to, "subject": subject, "body": body},
    )
    resp.raise_for_status()
    return resp.json()  "cm"># {"message_id": "...", "status": "pending_approval"}

def list_pending() -> list:
    """Return messages waiting for human approval."""
    resp = requests.get(f"{MULTIMAIL_BASE}/list_pending", headers=HEADERS)
    resp.raise_for_status()
    return resp.json()["messages"]

research_agent = Agent(
    name="Researcher",
    model=OpenAIChat(id="gpt-4o"),
    description="You gather information and summarize findings for the communications agent.",
)

comms_agent = Agent(
    name="Communications",
    model=OpenAIChat(id="gpt-4o"),
    tools=[send_email, list_pending],
    description="You draft and queue outbound emails based on the researcher&"cm">#039;s findings. Always call list_pending after sending to confirm the message is in the approval queue.",
)

team = Team(
    name="OutreachTeam",
    agents=[research_agent, comms_agent],
    model=OpenAIChat(id="gpt-4o"),
)

team.print_response(
    "Research the top 3 open-source Python agent frameworks by GitHub stars and draft a partnership inquiry email to each project&"cm">#039;s maintainers."
)

A research agent and a communications agent collaborate. The communications agent drafts outbound messages; MultiMail's gated_send mode holds them for human approval before delivery.

Inbound webhook handler routing to an Agno agent
python
import requests
from fastapi import FastAPI, Request
from agno.agent import Agent
from agno.models.openai import OpenAIChat

app = FastAPI()

MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
HEADERS = {"Authorization": f"Bearer {MULTIMAIL_API_KEY}"}

def tag_email(message_id: str, tag: str) -> dict:
    resp = requests.post(
        f"{MULTIMAIL_BASE}/tag_email",
        headers=HEADERS,
        json={"message_id": message_id, "tag": tag},
    )
    resp.raise_for_status()
    return resp.json()

def reply_email(message_id: str, body: str) -> dict:
    resp = requests.post(
        f"{MULTIMAIL_BASE}/reply_email",
        headers=HEADERS,
        json={"message_id": message_id, "body": body},
    )
    resp.raise_for_status()
    return resp.json()

triage_agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[tag_email, reply_email],
    description=(
        "You triage inbound emails. Tag urgent messages as &"cm">#039;urgent', tag billing questions as 'billing', "
        "and reply to simple questions immediately. For anything complex, tag as &"cm">#039;needs-human' and do not reply."
    ),
)

@app.post("/webhooks/inbound")
async def handle_inbound(request: Request):
    payload = await request.json()
    message_id = payload["message_id"]
    sender = payload["from"]
    subject = payload["subject"]
    body = payload["body_text"]
    dkim_valid = payload.get("dkim_valid", False)

    if not dkim_valid:
        # Do not act on messages that fail DKIM — tag for human review.
        tag_email(message_id, "dkim-failed")
        return {"status": "held"}

    triage_agent.run(
        f"New email from {sender}.\nSubject: {subject}\nBody: {body}\nMessage ID: {message_id}\n\nTriage and respond as appropriate."
    )
    return {"status": "processed"}

Receive inbound email via MultiMail webhook and hand it off to an Agno agent for triage and response. Run this with any ASGI server.


Step by step

1

Install Agno and get a MultiMail API key

Install Agno with pip and create a MultiMail account to get a live API key. Use a test key (mm_test_...) during development—test keys behave identically to live keys but messages are not delivered externally.

bash
pip install agno
"cm"># Get your API key from https://app.multimail.dev/settings/api-keys
2

Create a mailbox and set oversight mode

Create a mailbox via the MultiMail dashboard or API. For a new agent integration, start with oversight_mode=gated_send so every outbound message requires your approval before delivery. You can relax this to monitored or autonomous once you trust the agent's behavior.

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

Define MultiMail functions and register them as Agno tools

Write thin wrapper functions around the MultiMail endpoints you need—send_email, check_inbox, reply_email—and pass them to your Agno agent's tools list. Agno will call them automatically when the model decides email action is needed.

bash
import requests
from agno.agent import Agent
from agno.models.openai import OpenAIChat

HEADERS = {"Authorization": "Bearer $MULTIMAIL_API_KEY"}
BASE = "https://api.multimail.dev"

def send_email(to: str, subject: str, body: str) -> dict:
    return requests.post(f"{BASE}/send_email", headers=HEADERS,
        json={"from": "[email protected]", "to": to, "subject": subject, "body": body}
    ).json()

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[send_email],
)
4

Run the agent and review the approval queue

Run the agent against a real task. Any outbound messages will land in the approval queue because oversight_mode is gated_send. Call list_pending to see what's queued, then use decide_email to approve or reject each message.

bash
agent.print_response("Email [email protected] about scheduling a technical review call.")

"cm"># Check the approval queue
pending = requests.get(f"{BASE}/list_pending", headers=HEADERS).json()
print(pending)

"cm"># Approve a specific message
requests.post(f"{BASE}/decide_email", headers=HEADERS,
    json={"message_id": pending["messages"][0]["id"], "decision": "approve"}
)

Common questions

Does MultiMail require a dedicated SDK for Agno?
No. Agno accepts any Python callable as a tool, so you call MultiMail's REST API directly using requests or httpx. There is no Agno-specific SDK—the wrapper functions you write are typically under 10 lines each.
How do I prevent one agent in an Agno team from sending emails without oversight?
Oversight mode is enforced at the mailbox level in MultiMail, not in the agent's code. Set the mailbox to gated_send or gated_all, and every call to send_email or reply_email from any agent sharing that mailbox will queue for human approval, regardless of which agent made the call.
Can multiple Agno agents share the same mailbox?
Yes. Multiple agents can use the same API key and mailbox address. MultiMail logs every API call with a timestamp and the originating request, so you can audit which messages came from which agent run after the fact.
How do I handle inbound email in an Agno workflow?
Configure a webhook URL in your MultiMail mailbox settings. When a message arrives, MultiMail POSTs the payload (sender, subject, body, DKIM result, thread ID) to your endpoint. From there, trigger an agent.run() call with the payload as context, and the agent can reply, tag, or escalate using MultiMail tool functions.
What happens if my Agno agent tries to send to itself in a loop?
MultiMail does not automatically prevent loop sending, but gated_send oversight gives you a human checkpoint to catch runaway sequences before they leave your system. For automated loop detection, check the to field in your send_email wrapper and reject self-addressed sends before calling the API.
How does MultiMail relate to Phidata, which Agno was formerly part of?
Agno is the agent framework that evolved out of the Phidata project. The integration pattern is identical—MultiMail works with any Python agent framework that supports function-calling tools. If you were using Phidata before, the same wrapper functions work in Agno without modification.

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.