This is architectural guidance for agents that need to send and receive email, not SDK documentation. You get seven opinionated patterns: one inbox per agent (never share), two-way conversation loops using extracted_text to strip quoted replies, human-in-the-loop drafts for high-stakes messages, WebSockets over polling, multi-agent topologies with clear role separation, OTP extraction for verification flows, and labels for workflow state. The security section is brief but covers the critical stuff like prompt injection via inbound email and allow lists for production inboxes. Useful if you're building support bots, notification agents, or anything that needs structured email communication instead of just fire-and-forget sending.
npx -y skills add agentmail-to/agentmail-skills --skill agent-email-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Opinionated patterns for building AI agents that communicate over email. This skill covers architecture decisions, not SDK specifics. For AgentMail SDK usage, use the agentmail skill.
Every agent gets its own email address. Never share inboxes between agents.
from agentmail import AgentMail
from agentmail.inboxes.types import CreateInboxRequest
client = AgentMail()
support_inbox = client.inboxes.create(
request=CreateInboxRequest(
username="support-agent",
display_name="Acme Support",
client_id="support-v1", # idempotent
),
)
# support-agent@agentmail.to is now live
Why:
Anti-pattern: one shared inbox with multiple agents reading from it. This creates race conditions and makes debugging impossible.
The core agent email pattern: agent sends, human replies, agent reads the reply and responds.
Agent sends initial email
-> Human replies
-> Agent reads reply (use extracted_text to strip quoted history)
-> Agent decides next action and responds
-> Loop continues until resolved
Implementation:
# 1. Agent sends the opening message
client.inboxes.messages.send(
inbox_id,
to="user@example.com",
subject="Your support ticket #1234",
text="We received your request. Can you clarify the issue?",
)
# 2. Later: agent reads the reply.
# messages.list() returns MessageItem objects (metadata only — NO body).
# Fetch the full Message with .get() to access .text / .extracted_text.
response = client.inboxes.messages.list(inbox_id, limit=5)
for item in response.messages:
msg = client.inboxes.messages.get(
inbox_id=item.inbox_id,
message_id=item.message_id,
)
# extracted_text strips quoted history and signatures
new_content = msg.extracted_text or msg.text
# Feed new_content to your LLM for next response
Key rules:
extracted_text / extracted_html for inbound replies to avoid processing the entire quoted chainclient.inboxes.messages.reply(inbox_id, message_id, ...) with the parent message_id — AgentMail routes the reply into the existing thread automatically. There is no thread_id parameter on the reply call.For high-stakes emails, let the agent draft and a human approve before sending.
# Agent drafts
draft = client.inboxes.drafts.create(
inbox_id,
to="important-client@example.com",
subject="Contract proposal",
text=agent_generated_text,
)
# Human reviews in console or via API, then:
client.inboxes.drafts.send(inbox_id, draft.draft_id)
Use drafts when:
Send directly when:
Never poll for new emails. Use WebSockets or webhooks.
WebSockets (best for agents, no public URL needed):
from agentmail import AgentMail, Subscribe, MessageReceivedEvent
client = AgentMail()
with client.websockets.connect() as socket:
socket.send_subscribe(Subscribe(inbox_ids=[inbox_id]))
for event in socket:
if isinstance(event, MessageReceivedEvent):
process_email(event.message)
Webhooks (for servers with public endpoints):
webhook = client.webhooks.create(
url="https://your-server.com/agent/email",
event_types=["message.received"],
)
Decision guide:
| Factor | WebSockets | Webhooks |
|---|---|---|
| Public URL needed | No | Yes |
| Best for | Agents, bots, local dev | Servers, serverless |
| Latency | Lowest (persistent) | HTTP round-trip |
| Reconnection | You handle it | AgentMail retries |
For systems with multiple agents, assign clear roles:
support@agentmail.to -> customer support
sales@agentmail.to -> sales inquiries
billing@agentmail.to -> invoices and payments
router@agentmail.to -> intake, routes to correct agent
Agents can email each other for internal coordination:
# Support agent escalates to sales
client.inboxes.messages.send(
support_inbox_id,
to=sales_inbox.email,
subject="Lead handoff: Acme Corp",
text="Customer wants enterprise pricing. Full thread below.",
)
Use allow lists (references/security.md) to restrict which external senders can reach each agent. For hub-and-spoke, peer-to-peer, and hierarchical escalation patterns, see references/multi-agent-topologies.md.
Agents that sign up for services need to receive and extract verification codes.
import re
inbox = client.inboxes.create()
# Use inbox.email to sign up for a service
# Listen for OTP via WebSocket
with client.websockets.connect() as socket:
socket.send_subscribe(Subscribe(inbox_ids=[inbox.inbox_id]))
for event in socket:
if isinstance(event, MessageReceivedEvent):
text = event.message.text or ""
match = re.search(r"\b(\d{4,8})\b", text)
if match:
otp = match.group(1)
break
Best practices:
Use labels to track message processing state within an inbox:
# When agent processes a message
client.inboxes.messages.update(
inbox_id, message_id,
add_labels=["processed", "needs-followup"],
remove_labels=["unread"],
)
# Query by label
unprocessed = client.inboxes.messages.list(inbox_id, labels=["unread"])
Common label schemes:
unread / processed / archivedneeds-reply / replied / escalatedbilling / support / sales (category routing)See references/security.md for full coverage. Critical rules:
references/multi-agent-topologies.md -- hub-and-spoke, peer-to-peer, and hierarchical agent email architecturesreferences/security.md -- prompt injection defense, sender validation, credential isolationsickn33/antigravity-awesome-skills
moizibnyousaf/ai-agent-skills
github/awesome-copilot