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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
pip install agno
"cm"># Get your API key from https://app.multimail.dev/settings/api-keysCreate 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.
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;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.
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],
)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.
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"}
)Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.