Human-in-the-Loop Email Approvals
Keep humans in the loop for sensitive or high-stakes email sends. Use autonomy levels and the approval API to build review workflows around your agent.
Fully autonomous agents are the goal for routine email - transactional confirmations, onboarding sequences, follow-up replies. But for outbound prospecting, legal communications, or any email your agent hasn't proven itself on yet, you want a human to review the draft before it goes out.
Molted's autonomy levels and approval API give you fine-grained control over when humans review agent-proposed sends, without changing your agent code.
How it works
Every send request flows through two gates:
- Policy engine - checks deduplication, rate limits, suppression, consent, and risk budgets
- Autonomy level - decides whether a passing send goes straight to delivery or waits for human approval
Your agent always calls the same endpoint. The approval requirement is a mailbox setting - the agent proposes, the infrastructure decides whether to deliver immediately or route to the approval queue.
agent calls /v1/agent/send/request
│
▼
policy engine runs 20+ rules
│
├── BLOCKED → returns blockReason to agent
│
└── PASSED → autonomy level check
│
├── L1/L2 trigger → held for approval → human reviews
│ │
│ approved → delivered
│ rejected → not delivered
│
└── L3 → delivered immediatelyAutonomy levels
Set per mailbox. Default is L3 (fully autonomous).
| Level | Name | When to use |
|---|---|---|
| L1 | Assisted | Every send waits for approval. For new agents, high-stakes mailboxes, or any context where you want full visibility. |
| L2 | Guarded Auto | First message to a new contact waits for approval. Replies and follow-ups to known contacts auto-send. Good for outbound prospecting where new contacts need review but ongoing threads do not. |
| L3 | Fully Autonomous | No approval required. Right for proven transactional agents with established patterns. |
Set up a supervised mailbox
Set a mailbox to L1 or L2 via the portal, CLI, or API:
molted mailboxes autonomy set YOUR_MAILBOX_ID --level 1curl -X PATCH "https://api.molted.email/v1/agent/mailboxes/YOUR_MAILBOX_ID/autonomy?tenantId=YOUR_TENANT_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"autonomyLevel": 1}'Your agent code does not change. The next send from this mailbox will be held for approval instead of delivered.
Check the approval queue
When a send is held, it appears in the approval queue. Poll it or integrate it into your internal tools:
curl "https://api.molted.email/v1/send/approvals?tenantId=YOUR_TENANT_ID&status=pending" \
-H "Authorization: Bearer YOUR_API_KEY"{
"approvals": [
{
"id": "appr_abc123",
"requestId": "req_xyz789",
"status": "pending",
"recipientEmail": "prospect@example.com",
"subject": "Following up on your trial",
"htmlPreview": "<p>Hi Sarah, just wanted to check in...</p>",
"agentId": "outreach-agent",
"createdAt": "2026-04-01T09:00:00Z",
"expiresAt": "2026-04-02T09:00:00Z"
}
]
}Approve or reject
curl -X PATCH "https://api.molted.email/v1/send/approvals/appr_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "approved"}'curl -X PATCH "https://api.molted.email/v1/send/approvals/appr_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "rejected", "rejectionReason": "Tone too aggressive - rephrase"}'Approved sends are delivered immediately. Rejected sends are not delivered. Pending approvals expire after 24 hours by default.
Build an approval workflow with your agent
A practical pattern: your agent submits the send, then polls for the approval decision and acts on it.
import { MailClient } from "@molted/mail";
const client = new MailClient({
baseUrl: "https://api.molted.email",
apiKey: process.env.MOLTED_API_KEY!,
});
async function sendWithApproval(
to: string,
subject: string,
body: string
): Promise<{ delivered: boolean; reason?: string }> {
// Submit the send
const result = await client.requestSend({
tenantId: process.env.MOLTED_TENANT_ID!,
recipientEmail: to,
templateId: "_default",
dedupeKey: `${to}-${subject}`,
payload: { subject, html: body, text: body },
});
if (result.status === "blocked") {
return { delivered: false, reason: `Policy block: ${result.blockReason}` };
}
if (result.status === "queued") {
// Immediately queued - L3 mailbox, no approval needed
return { delivered: true };
}
if (result.status === "pending_approval") {
// L1 or L2 mailbox - wait for human review
console.log(`Send held for approval. requestId: ${result.requestId}`);
console.log(`Review in portal or via API: /v1/send/approvals`);
// Optionally: poll for the decision (or use webhooks instead)
return { delivered: false, reason: "Pending human approval" };
}
return { delivered: false, reason: "Unknown status" };
}import os
import time
import requests
def send_with_approval(to: str, subject: str, body: str) -> dict:
"""Submit a send and return immediately. Check approval queue for decision."""
response = requests.post(
"https://api.molted.email/v1/agent/send/request",
headers={
"Authorization": f"Bearer {os.environ['MOLTED_API_KEY']}",
"Content-Type": "application/json",
},
json={
"tenantId": os.environ["MOLTED_TENANT_ID"],
"recipientEmail": to,
"templateId": "_default",
"dedupeKey": f"{to}-{subject}",
"payload": {"subject": subject, "html": body, "text": body},
},
timeout=10,
)
data = response.json()
if data["status"] == "blocked":
return {"delivered": False, "reason": f"Policy block: {data['blockReason']}"}
if data["status"] == "queued":
return {"delivered": True, "requestId": data["requestId"]}
if data["status"] == "pending_approval":
print(f"Send held for approval. requestId: {data['requestId']}")
print("Review in portal: Dashboard > Approvals")
return {"delivered": False, "reason": "pending_approval", "requestId": data["requestId"]}
return {"delivered": False, "reason": "unknown"}Use webhooks to react to approval decisions
Instead of polling, set up a webhook to notify your agent or internal system when a human approves or rejects:
curl -X POST "https://api.molted.email/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tenantId": "YOUR_TENANT_ID",
"url": "https://your-app.example.com/webhooks/email-approval",
"events": ["approval.approved", "approval.rejected", "approval.expired"]
}'Your webhook handler receives the decision:
import type { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
const event = await req.json();
if (event.type === "approval.approved") {
const { requestId, recipientEmail } = event.data;
console.log(`Email to ${recipientEmail} approved and delivered. requestId: ${requestId}`);
// Notify your agent, update CRM, etc.
}
if (event.type === "approval.rejected") {
const { requestId, rejectionReason } = event.data;
console.log(`Email rejected. Reason: ${rejectionReason}`);
// Alert agent, log for review, potentially regenerate with feedback
}
if (event.type === "approval.expired") {
console.log(`Approval expired. requestId: ${event.data.requestId}`);
// Handle timeout - re-queue or abandon
}
return new Response("ok");
}See Webhooks for full event reference and signature verification.
Progressive trust: start supervised, graduate to autonomous
A reliable pattern for deploying new agents safely:
Phase 1 - L1 (Assisted): Every send is reviewed. Build confidence in the agent's output quality and volume. Review for tone, targeting accuracy, and edge cases.
Phase 2 - L2 (Guarded Auto): Once first-contact quality is proven, graduate to L2. New prospects are still reviewed, ongoing threads auto-send. This is the right steady state for most outbound agents.
Phase 3 - L3 (Fully Autonomous): After L2 is stable with no rejection patterns, graduate to L3 for high-confidence mailboxes (transactional, support replies, internal notifications).
Switch levels at any time via API or portal - no code changes needed:
# Graduate from L1 to L2 after reviewing 100 sends
molted mailboxes autonomy set YOUR_MAILBOX_ID --level 2Separate mailboxes for different risk profiles
A single tenant can have multiple mailboxes with different autonomy levels. This lets you apply the right level of oversight per use case without a blanket policy:
| Mailbox | Autonomy | Use case |
|---|---|---|
outreach@yourdomain.com | L2 | Outbound prospecting - new contacts reviewed |
support@yourdomain.com | L3 | Support replies - proven patterns, auto-send |
enterprise@yourdomain.com | L1 | Enterprise outreach - always reviewed |
billing@yourdomain.com | L3 | Transactional dunning - fully automated |
Related
- Autonomy Levels - full reference for level configuration
- Webhooks - receive approval decision events
- Policy Simulation - test policy rules before going live
- LangChain Integration Guide - LangChain setup
- CrewAI Integration Guide - CrewAI setup
- AutoGen Integration Guide - AutoGen setup
AutoGen Integration Guide
Add a governed email mailbox to your AutoGen agents and multi-agent conversations. Send, receive, and track email with built-in policy enforcement.
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.