AutoGen Email Integration for Multi-Agent Teams
Wire real email into AutoGen: provision mailboxes, expose a send tool, turn inbound webhooks into agent messages, and keep replies in the same thread.
John Joubert
Founder, Robotomail

Table of contents
AutoGen has no email transport of its own. To build an AutoGen email integration you need three pieces: a real mailbox with an address your agents can send from and receive at, a function tool that wraps the send call so an agent can use it mid-conversation, and an inbound webhook that converts arriving mail back into a message inside your group chat. This post walks through all three, plus the decision that trips people up first: whether your agent team shares one inbox or each agent gets its own.
What an AutoGen email integration actually needs
AutoGen's model is conversational. Agents pass messages to each other, a group chat manager picks who speaks next, and function tools let an agent take an action in the outside world. Email fits that model well, with one catch: email is asynchronous and arrives days later, while an AutoGen run usually finishes in seconds.
So the integration splits into two halves that never share a process:
- Outbound. A registered tool on one or more agents that calls the send API. Synchronous, easy.
- Inbound. An HTTP endpoint that receives a webhook when mail lands, loads the relevant conversation state, and injects the message into a new AutoGen run.
If you try to make one long-lived AutoGen run block on incoming email, you will end up holding conversation state in memory for hours and losing it on every deploy. Treat each inbound email as a fresh trigger that resumes work from durable state instead.
Shared inbox or per-agent inboxes?
Both work. The choice depends on whether the outside world should see one identity or several.
| Shared inbox | Per-agent inboxes | |
|---|---|---|
| Recipient sees | One address, e.g. procurement@yourdomain.com |
sourcing@, negotiator@, finance@ |
| Routing logic | Webhook handler must decide which agent speaks | Mailbox address is the route |
| Thread hygiene | All threads in one place, easy audit | Threads split across mailboxes |
| Failure mode | Two agents reply to the same email | Vendor confused by three senders |
| Best for | Customer-facing work, support, outreach | Internal pipelines, distinct roles, parallel work |
Our default recommendation: one shared mailbox per external relationship or workflow, not per agent. A vendor negotiating an order should not receive mail from three different addresses. Keep the agents internal and let the group chat manager decide who drafts the reply.
Per-agent inboxes earn their keep when the agents genuinely play different external roles. A recruiting pipeline where a screener agent talks to candidates and a scheduler agent talks to hiring managers is a real case for two addresses. So is a research team where each agent subscribes to different sources. If you are still deciding, the patterns in our multi-agent workflows use case map cleanly onto AutoGen group chats.
You can also do both: a shared external address plus internal-only mailboxes used for handoffs and alerting.
Step 1: provision the mailboxes
Fastest path from the CLI:
npx @robotomail/cli mailbox create procurement-team
Or from the API, which is what you want if agents create their own mailboxes at runtime:
curl -X POST https://api.robotomail.com/v1/mailboxes \
-H "Authorization: Bearer rm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"address": "procurement-team"}'
That returns a mailbox with an ID and a live address that can receive immediately. No DNS, no OAuth consent screen, no per-user Google Workspace seat. If you want the address on your own domain, add the domain first and pass its UUID as domainId. There is more on address strategy in how to give your AI agent an email address.
For per-agent inboxes, create one per role and store the mapping in whatever config your AutoGen team already reads:
MAILBOXES = {
"sourcing_agent": "mbx_sourcing",
"negotiator_agent": "mbx_negotiator",
"finance_agent": "mbx_finance",
}
Step 2: register the send tool on your agents
AutoGen tools are plain Python functions with type hints and a docstring. Keep the signature narrow so the model cannot invent recipients or headers.
import os
import requests
from typing import Optional
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
API_KEY = os.environ["ROBOTOMAIL_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def send_email(
mailbox_id: str,
to: str,
subject: str,
body_text: str,
in_reply_to: Optional[str] = None,
) -> str:
"""Send an email from one of the team's mailboxes.
Pass in_reply_to with the inbound message id when replying so the
message stays in the existing thread."""
payload = {"to": [to], "subject": subject, "bodyText": body_text}
if in_reply_to:
payload["inReplyTo"] = in_reply_to
r = requests.post(
f"https://api.robotomail.com/v1/mailboxes/{mailbox_id}/messages",
headers=HEADERS,
json=payload,
timeout=20,
)
r.raise_for_status()
return f"sent: {r.json().get('id', 'ok')}"
model = OpenAIChatCompletionClient(model="gpt-4o")
negotiator = AssistantAgent(
name="negotiator_agent",
model_client=model,
tools=[send_email],
system_message=(
"You negotiate with suppliers over email. Send from mailbox "
"mbx_procurement only. When replying to a supplier, always pass "
"in_reply_to with the message id you were given. Never invent "
"recipient addresses; use only addresses present in the conversation."
),
)
A few things worth doing here that people skip:
- Pin the mailbox in the system message rather than letting the model choose. If you use per-agent inboxes, hard-code the ID inside a closure so the parameter is not exposed to the model at all.
- Only one agent in the group chat should hold the send tool. Multi-agent chats produce duplicate sends fast when three agents can all reach the outside world. Give the tool to a single "communicator" agent and let the others draft.
- Validate recipients server-side. Check the
toaddress against an allowlist derived from the current thread before you call the API. Models will occasionally address mail to a company name they hallucinated a domain for.
Step 3: turn inbound webhooks into AutoGen messages
Register a webhook once, then handle the POST. Inbound arrives as { event, timestamp, data } with snake_case fields:
{
"event": "message.received",
"data": {
"message_id": "...", "mailbox_id": "...", "mailbox_address": "procurement-team@robotomail.co",
"from": "vendor@example.com", "subject": "Re: Order #A-4521",
"body_text": "...", "thread_id": "...", "received_at": "2026-04-17T10:00:00.000Z"
}
}
The handler's job is to acknowledge fast and hand off. Do not run a group chat inside the request.
from fastapi import FastAPI, Request, BackgroundTasks
app = FastAPI()
@app.post("/hooks/email")
async def inbound(req: Request, background: BackgroundTasks):
payload = await req.json()
if payload.get("event") != "message.received":
return {"ok": True}
data = payload["data"]
if already_processed(data["message_id"]): # idempotency, see below
return {"ok": True}
background.add_task(run_team, data)
return {"ok": True}
Then the run itself. Load prior state for the thread, seed the group chat with the email as a task, save state back:
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import MaxMessageTermination
async def run_team(data: dict):
thread_id = data["thread_id"]
state = load_state(thread_id) # your DB
team = RoundRobinGroupChat(
[analyst, negotiator],
termination_condition=MaxMessageTermination(10),
)
if state:
await team.load_state(state)
task = (
f"Inbound email on thread {thread_id}.\n"
f"mailbox_id: {data['mailbox_id']}\n"
f"in_reply_to: {data['message_id']}\n"
f"from: {data['from']}\n"
f"subject: {data['subject']}\n\n"
f"{data['body_text']}"
)
await team.run(task=task)
save_state(thread_id, await team.save_state())
Keying state on thread_id is the whole trick. AutoGen teams serialize their state, so a long email negotiation becomes a sequence of short runs that each pick up where the last one left off. If you are new to the receive side, receive and reply covers the mechanics without AutoGen in the way, and webhook concepts covers registration and retry behavior.
Step 4: keep replies in the thread
Passing inReplyTo with the inbound message_id is what makes the reply land in the same conversation in the recipient's client, with correct In-Reply-To and References headers and no subject-line guessing. Skip it and every reply starts a new thread, which reads as broken to the human on the other end and destroys your own grouping.
Two rules that keep this reliable in a multi-agent setup:
- The
message_idmust reach the agent that sends. That is why it is in the task string above rather than hidden in application state the model cannot see. - One reply per inbound message. Track sent replies by inbound
message_idand refuse a second send for the same one, at the code level, not in the prompt. Group chats loop, and a loop that emails is a loop your vendor notices. How email threading works has the header-level detail if you want to know why the ID matters.
Operational details that bite
Idempotency. Webhook deliveries can repeat. Store message_id with a unique constraint and drop duplicates before you spend tokens.
Concurrency per thread. Two emails arriving 30 seconds apart will start two runs against the same saved state and one will clobber the other. Take a per-thread lock, or queue by thread_id.
Termination. Always set a termination condition. An unbounded group chat with a send tool is the worst possible bug to discover from a customer.
Human approval for first contact. Replies inside an existing thread are low risk. Cold first messages to new addresses deserve a review step, at least until you trust the pipeline.
Rate and volume awareness. Agents batch-processing an inbox can burst. Check limits before you point a team at a thousand queued messages.
If you are comparing frameworks, the same architecture ports over almost unchanged. Our CrewAI email integration guide uses the identical webhook-to-run pattern with different orchestration primitives.
FAQ
Can AutoGen agents share one mailbox without stepping on each other?
Yes, and it is usually the right default. Give the send tool to exactly one agent in the group chat and route inbound by thread_id rather than by agent. The other agents contribute drafts and analysis inside the conversation.
How does an AutoGen run stay alive while waiting for a reply?
It does not, and it should not. End the run after sending, persist team state keyed by thread_id, and start a new run when the webhook fires. save_state and load_state make this a few lines of code.
Do I need a custom domain for this?
No. A mailbox on the platform domain is live as soon as you create it, which is what you want while building. Move to your own domain when the mail is customer-facing, since recipients read the domain as a trust signal.
Can I use the Gmail API instead?
You can, but you inherit OAuth token refresh, per-user consent, and quota models designed for human accounts. For agents that need many addresses provisioned programmatically, an API-key mailbox is far less work. See our Gmail API comparison for the specifics.
Ready to wire it up? Create a mailbox, point a webhook at your handler, and register the send tool on one agent. Start at robotomail.com.
Give your AI agent a real email address
One API call creates a mailbox with full send and receive. Webhooks for inbound, automatic threading, deliverability handled. 30-day money-back guarantee.
Related posts

AI Email Agent: Build One That Sends and Receives
What an AI email agent is, and how to build one with its own mailbox, inbound webhook, reply loop, and correct threading. With working code.
Read post
How to Parse Inbound Email Webhook Payloads
How to parse inbound email webhook payloads: required fields, MIME edge cases, attachments, threading ids, and idempotent retry handling.
Read post
Build an n8n Email Agent With a Real Inbox
Give your n8n AI agent its own email address: trigger workflows from inbound mail via webhook, send threaded replies, and run per-workflow mailboxes.
Read post