OpenAI Agents SDK Integration Guide
Add a governed email mailbox to your OpenAI Agents SDK agents. Send, receive, and track email with built-in policy enforcement.
The OpenAI Agents SDK lets you build agents that call tools in a loop. Email is a natural fit - but without a governed mailbox, any tool call can trigger a send with no deduplication, no rate limiting, no suppression checks. One runaway agent can exhaust your sender reputation.
This guide shows how to wire a Molted mailbox into an OpenAI Agents SDK agent as a hosted tool. Every email the agent proposes runs through the Molted policy engine before delivery - 20+ rules evaluated in under a second. The agent works through the same function_tool interface it uses for everything else; policy runs at the infrastructure layer and cannot be overridden by the model.
Prerequisites
- A Molted account with an API key (sign up at molted.email/signup)
- A verified sending domain (see Domains)
- OpenAI Agents SDK installed:
pip install openai-agents
1. Create a mailbox for your agent
Each agent (or agent team) should have its own mailbox. Log in to the portal under Mailboxes, or create one via the API:
curl -X POST https://api.molted.email/v1/me/mailboxes \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Outbound Agent",
"emailAddress": "agent@yourdomain.com"
}'Note the mailboxId from the response - you will pass it in each send request.
2. Define the send tool
Use function_tool to wrap the Molted mailbox API. The agent sees this as a regular tool call; policy runs transparently at the infrastructure layer.
import os
import requests
from agents import function_tool
MOLTED_API_KEY = os.environ["MOLTED_API_KEY"]
MOLTED_TENANT_ID = os.environ["MOLTED_TENANT_ID"]
MOLTED_MAILBOX_ID = os.environ["MOLTED_MAILBOX_ID"]
@function_tool
def send_email(
to: str,
subject: str,
body: str,
dedupe_key: str | None = None,
) -> str:
"""Send an email to a contact through the governed mailbox.
The email is evaluated against policy rules before delivery.
Blocked sends return a reason and are not retried.
Args:
to: Recipient email address.
subject: Email subject line.
body: Email body in plain text or HTML.
dedupe_key: Optional unique key to prevent duplicate sends.
Defaults to recipient+subject if omitted.
"""
payload = {
"tenantId": MOLTED_TENANT_ID,
"mailboxId": MOLTED_MAILBOX_ID,
"recipientEmail": to,
"templateId": "_default",
"dedupeKey": dedupe_key or f"{to}-{subject}",
"agentId": "openai-agent",
"payload": {
"subject": subject,
"html": body,
"text": body,
},
}
resp = requests.post(
"https://api.molted.email/v1/agent/send",
json=payload,
headers={"Authorization": f"Bearer {MOLTED_API_KEY}"},
timeout=10,
)
resp.raise_for_status()
result = resp.json()
if result["status"] == "blocked":
return (
f"Email blocked by policy: {result['blockReason']}. "
f"Decision trace: {result['requestId']}"
)
return f"Email queued. requestId={result['requestId']}, status={result['status']}"The tool returns the policy decision as a string. If a send is blocked (duplicate, rate limit, suppressed recipient), the agent sees the reason and can decide what to do next rather than failing silently or retrying blindly.
3. Add a status-check tool (optional)
Your agent can verify delivery status of a previously queued email:
import os
import requests
from agents import function_tool
MOLTED_API_KEY = os.environ["MOLTED_API_KEY"]
@function_tool
def check_email_status(request_id: str) -> str:
"""Check the delivery status of a previously queued email.
Args:
request_id: The requestId returned when the email was sent.
"""
resp = requests.get(
f"https://api.molted.email/v1/agent/send/{request_id}/status",
headers={"Authorization": f"Bearer {MOLTED_API_KEY}"},
timeout=10,
)
resp.raise_for_status()
data = resp.json()
provider = data.get("provider", "unknown")
last_event = data.get("lastEvent", "none")
return f"Status: {data['status']}. Provider: {provider}. Last event: {last_event}."4. Wire the tools into an agent
import asyncio
from agents import Agent, Runner
from tools.send_email import send_email
from tools.check_email_status import check_email_status
outreach_agent = Agent(
name="Outreach Agent",
instructions="""
You are a customer outreach agent. When sending emails:
- Always supply a dedupe_key to prevent accidental duplicate sends.
- If a send is blocked by policy, report the reason to the user - do not retry.
- After sending, check delivery status if the user asks.
- 'duplicate_send' means the email was already sent recently - inform the user and stop.
- 'suppressed_recipient' means this contact cannot be emailed - inform the user and stop.
- 'rate_limit_exceeded' means the send rate has been reached - wait and inform the user.
""",
tools=[send_email, check_email_status],
)
async def main():
result = await Runner.run(
outreach_agent,
input=(
"Send a trial welcome email to alice@example.com. "
"Subject: 'Welcome to the trial' "
"Body: 'Thanks for signing up - your 14-day trial starts now.'"
),
)
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())5. Handle policy blocks
The policy engine may block a send for several reasons. The tool surfaces these directly to the agent.
| Reason | What it means |
|---|---|
duplicate_send | The same dedupe_key was used within the cooldown window |
rate_limit_exceeded | The mailbox or tenant has hit a send rate limit |
suppressed_recipient | The recipient has unsubscribed or bounced previously |
cooldown_active | A cooldown window is in effect for this recipient |
risk_budget_exceeded | The agent's risk budget for this period is exhausted |
Add explicit handling in your agent instructions so the model knows the right action for each block - especially distinguishing retryable (rate_limit_exceeded) from terminal (suppressed_recipient, duplicate_send) cases.
6. Build a multi-agent handoff
The OpenAI Agents SDK supports handoffs between specialized agents. You can give only the outreach agent access to the mailbox and route email tasks to it from other agents.
from agents import Agent, Runner
from tools.send_email import send_email
from tools.check_email_status import check_email_status
# Specialized email agent - the only one with mailbox access
email_agent = Agent(
name="Email Agent",
instructions="""
You send and track emails for the team. Always use a dedupe_key.
Report policy blocks clearly - do not retry terminal blocks.
""",
tools=[send_email, check_email_status],
)
# Triage agent - routes tasks and delegates email sends
triage_agent = Agent(
name="Triage Agent",
instructions="""
You coordinate tasks. For any email sending, hand off to the Email Agent.
Do not attempt to send emails directly.
""",
handoffs=[email_agent],
)This pattern keeps mailbox access scoped to a single agent. If any other agent in the team tries to send email directly, it cannot - the tool is not in its tool list. Policy is enforced regardless, but scoping the tool also limits the blast radius of a misconfigured agent.
7. Listen for inbound replies
Use the Molted CLI to stream inbound events to your agent so it can react to replies:
molted listen --pipe "python reply_handler.py" --events "inbound.*"In reply_handler.py:
import sys
import json
import asyncio
from agents import Agent, Runner
from tools.send_email import send_email
reply_agent = Agent(
name="Reply Agent",
instructions="You handle inbound replies. Respond helpfully and concisely.",
tools=[send_email],
)
async def handle_event(event: dict) -> None:
if event.get("event") != "inbound.classified":
return
data = event["data"]
intent = data.get("intent")
suggested = data.get("suggestedAction")
if intent == "support" and suggested == "reply":
from_email = data["fromEmail"]
subject = data["subject"]
await Runner.run(
reply_agent,
input=(
f"Reply to {from_email} who wrote: '{subject}'. "
"Keep it short and helpful."
),
)
for line in sys.stdin:
event = json.loads(line.strip())
asyncio.run(handle_event(event))See Reactive Agent Guide for the full event reference and daemon setup.
What happens under the hood
When your agent calls send_email, the mailbox evaluates 20+ policy rules in sequence before anything leaves:
- Suppression check - has this recipient unsubscribed or bounced?
- Deduplication - has this
dedupe_keybeen used within the cooldown window? - Cooldown - is there an active cooldown for this recipient?
- Rate limits - has the mailbox or tenant exceeded its send rate (per-minute, per-hour, per-day)?
- Risk budget - has the agent exhausted its send budget for this period?
- Consent - does the contact have a valid consent record?
If all rules pass, the email is delivered. If any rule fires, the send is blocked and an immutable decision trace records which rule triggered and why. The agent sees the blockReason immediately and can respond accordingly.
The model cannot override these checks - they run at the infrastructure layer, outside the tool's return path.
Related
- Quickstart - send your first policy-checked email
- Policy Simulation - test rules against draft sends before deploying
- Reactive Agent Guide - build agents that respond to inbound email
- Agent Coordination - multi-agent leases and consensus
- Autonomy Levels - require human approval for high-risk sends
- LangChain Integration Guide
- CrewAI Integration Guide
- AutoGen Integration Guide