Schedule Emails with Agent Intelligence and Human Control

AI agents determine the best send window; humans approve before delivery. Scheduled email that doesn't mean unreviewed email.


Why this matters

Scheduled email delivery is useful precisely because it decouples writing from sending. But when an agent drafts a message days before it goes out, that gap creates risk: context may have changed, the tone may be off, or the recipient situation may have shifted. A message sitting in a send queue until it fires automatically is a message that bypasses the review window operators actually wanted. Most scheduling tools treat the schedule as final — once queued, the message goes out unless someone manually intervenes. Agents can generate high volumes of scheduled messages, which makes manual cancellation impractical and silent delivery the default. At scale, this means unreviewed outbound email is the norm rather than the exception.


How MultiMail solves this

MultiMail's send_email endpoint accepts a scheduled_at timestamp, queuing the message for future delivery. Scheduling and gated_send oversight work together: a scheduled message requires human approval before it releases, not after the timer fires. Operators see the message, its intended send time, and full context during the review window — not a race against the clock. list_pending surfaces all queued messages with their scheduled times and approval status. cancel_message removes any message from the queue before delivery, giving operators a clean intervention path without digging through logs or reversing a sent message.

1

Agent drafts and selects a send window

The agent calls send_email with a scheduled_at ISO 8601 timestamp — derived from recipient timezone data, engagement patterns, or business rules. The message is queued but not yet approved or delivered.

2

Message enters the pending queue

list_pending returns all scheduled messages with their send times, recipients, subjects, and status. Operators have full visibility into the outbox before anything goes out.

3

Approval webhook fires before the send window

Under gated_send, MultiMail fires an approval webhook when a scheduled message requires review. The operator receives the message content and intended send time together — context and timing in a single decision.

4

Operator approves, edits, or cancels

The operator calls decide_email to approve or reject, or cancel_message to remove the message entirely. Nothing delivers until the gate is cleared. If context changed since drafting, cancel_message is available at any point prior to delivery.

5

Message delivers at the scheduled time

Once approved, the message holds in queue until the scheduled_at timestamp, then delivers. Delivery and open events flow back via webhook, giving the agent feedback to refine future send-time selection.


Implementation

Queue a scheduled email via REST API
python
import httpx
from datetime import datetime, timezone, timedelta

"cm"># Schedule for tomorrow at 9 AM UTC
send_at = (datetime.now(timezone.utc) + timedelta(days=1)).replace(
    hour=9, minute=0, second=0, microsecond=0
)

response = httpx.post(
    "https://api.multimail.dev/v1/send_email",
    headers={"Authorization": "Bearer $MULTIMAIL_API_KEY"},
    json={
        "from": "[email protected]",
        "to": "[email protected]",
        "subject": "Scheduled follow-up for tomorrow at 9:00 AM",
        "body": "Following up on our conversation — wanted to share the report while it&"cm">#039;s fresh for your morning review.",
        "scheduled_at": send_at.isoformat(),
        "oversight_mode": "gated_send"
    }
)

data = response.json()
print(f"Queued: {data[&"cm">#039;message_id']}")
print(f"Scheduled for: {data[&"cm">#039;scheduled_at']}")
print(f"Status: {data[&"cm">#039;status']}")  # 'pending_approval' until reviewed

Schedule a message for future delivery with gated_send oversight. The message enters the pending queue and requires human approval before the send window opens.

List and manage the pending message queue
python
import httpx

client = httpx.Client(
    base_url="https://api.multimail.dev/v1",
    headers={"Authorization": "Bearer $MULTIMAIL_API_KEY"}
)

"cm"># Show all pending and scheduled messages
pending = client.get("/list_pending").json()

for msg in pending["messages"]:
    print(f"[{msg[&"cm">#039;message_id']}] To: {msg['to']}")
    print(f"  Subject: {msg[&"cm">#039;subject']}")
    print(f"  Scheduled: {msg[&"cm">#039;scheduled_at']}")
    print(f"  Status: {msg[&"cm">#039;status']}")
    print()

# Cancel a specific message if the situation changed before delivery
message_id = "msg_01HX3J9KQRST456"
cancel_resp = client.post(
    "/cancel_message",
    json={
        "message_id": message_id,
        "reason": "Deal closed before send window — no longer relevant"
    }
)
print(f"Cancelled: {cancel_resp.json()[&"cm">#039;cancelled']}")

Retrieve all scheduled messages awaiting delivery or approval. Use cancel_message to remove a message before it fires if context has changed.

Timezone-aware scheduling with the Python SDK
python
from multimail_sdk import MultiMailClient
from datetime import datetime, timedelta
import pytz

client = MultiMailClient(api_key="$MULTIMAIL_API_KEY")

def schedule_for_recipient_morning(
    to_email: str,
    recipient_tz: str,
    subject: str,
    body: str
) -> dict:
    tz = pytz.timezone(recipient_tz)
    now_local = datetime.now(tz)

    "cm"># Target next-day 9 AM in recipient's timezone
    target = now_local.replace(hour=9, minute=0, second=0, microsecond=0)
    if target <= now_local:
        target += timedelta(days=1)

    send_at_utc = target.astimezone(pytz.utc)

    return client.send_email(
        from_address="[email protected]",
        to=to_email,
        subject=subject,
        body=body,
        scheduled_at=send_at_utc.isoformat(),
        oversight_mode="gated_send"
    )

result = schedule_for_recipient_morning(
    to_email="[email protected]",
    recipient_tz="America/Chicago",
    subject="Proposal follow-up — three questions before your review",
    body="Wanted to flag three open items in the proposal before your team reviews it Thursday."
)
print(f"Queued {result[&"cm">#039;message_id']} for {result['scheduled_at']}")

Use the MultiMail Python SDK to schedule messages for the recipient's local morning delivery window. Useful for outbound sales or account management agents operating across time zones.

Handle approval webhooks for scheduled messages
python
from fastapi import FastAPI, Request
import httpx

app = FastAPI()
MULTIMAIL_TOKEN = "$MULTIMAIL_API_KEY"
SAFE_DOMAINS = {"trusted-client.com", "enterprise-prospect.com"}

@app.post("/webhooks/multimail")
async def handle_approval_event(request: Request):
    event = await request.json()

    if event["type"] != "approval.required":
        return {"ok": True}

    msg = event["message"]
    print(f"Review needed: &"cm">#039;{msg['subject']}' → {msg['to']}")
    print(f"Scheduled for: {msg[&"cm">#039;scheduled_at']}")

    recipient_domain = msg["to"].split("@")[-1]
    auto_approve = recipient_domain in SAFE_DOMAINS

    httpx.post(
        "https://api.multimail.dev/v1/decide_email",
        headers={"Authorization": f"Bearer {MULTIMAIL_TOKEN}"},
        json={
            "message_id": msg["message_id"],
            "decision": "approve" if auto_approve else "reject",
            "note": "Auto-approved: matched trusted domain" if auto_approve
                    else "Routed to human reviewer — unknown domain"
        }
    )

    return {"ok": True}

Process approval events from MultiMail before the scheduled send window. Apply rule-based auto-approval or route to a human reviewer depending on message attributes.

Schedule a reply in an existing thread
python
import httpx
from datetime import datetime, timezone, timedelta

"cm"># Schedule a reply for 48 hours from now
follow_up_at = datetime.now(timezone.utc) + timedelta(hours=48)

response = httpx.post(
    "https://api.multimail.dev/v1/reply_email",
    headers={"Authorization": "Bearer $MULTIMAIL_API_KEY"},
    json={
        "thread_id": "thread_01HX7R2MNPQST789",
        "from": "[email protected]",
        "body": "Circling back as promised — happy to set up a 30-minute call to walk through the integration options if that&"cm">#039;s useful.",
        "scheduled_at": follow_up_at.isoformat(),
        "oversight_mode": "gated_send"
    }
)

data = response.json()
print(f"Reply queued: {data[&"cm">#039;message_id']}")
print(f"Thread: {data[&"cm">#039;thread_id']}")
print(f"Delivers at: {data[&"cm">#039;scheduled_at']}")

Use reply_email with scheduled_at to queue a follow-up that stays in context with the original thread. Useful for support or sales agents managing multi-turn conversations.


What you get

Approval before delivery, not after

gated_send oversight ensures humans review scheduled messages while there's still time to edit or cancel — not after the send window fires. The approval step is part of the scheduling flow, not a race against the clock.

Full queue visibility via list_pending

Operators can see every scheduled message, its intended recipient, send time, and current status in a single API call. No digging through logs to understand what's queued or what's about to go out.

Clean cancellation at any point before delivery

cancel_message removes a queued message at any point before delivery. If deal terms change, a meeting moves, or the agent's judgment was wrong, operators have a clear intervention path without infrastructure support.

CAN-SPAM compliant approval audit trail

The gated_send approval step creates a documented review record for each outbound message — supporting CAN-SPAM documentation requirements for commercial email programs that must demonstrate intentional, human-reviewed sending.

Timezone-aware delivery without custom scheduling infrastructure

Pass any ISO 8601 timestamp to scheduled_at and the API handles delivery at the correct moment. Agents compute optimal send windows from recipient data; MultiMail manages the queue, retries, and delivery confirmation.


Recommended oversight mode

Recommended
gated_send
Scheduled email creates a delayed-action risk: the agent drafts a message now that will reach a human later, after context may have shifted. gated_send fits naturally into the scheduling window — the operator reviews message content and intended send time together, then approves or cancels. This preserves agent efficiency for read operations and drafting while keeping a human in the loop on every outbound message. For high-volume use cases with CAN-SPAM compliance requirements, gated_send also provides the documented approval record that supports commercial email program audits.

Common questions

What happens if approval isn't completed before the scheduled send time?
Under gated_send, a message that hasn't been approved will not deliver even if the scheduled_at timestamp passes. MultiMail holds the message and fires an escalation webhook. The send window shifts to the first available moment after approval is granted, or the message can be rescheduled by cancelling and requeuing with a new scheduled_at.
Can an agent reschedule a message after it's already been queued?
Yes. Call cancel_message on the existing message and send_email again with the updated scheduled_at timestamp. If the message has already been approved, cancellation is still available until the delivery process begins.
How far in advance can messages be scheduled?
MultiMail supports scheduling up to 30 days in advance via the scheduled_at parameter. For longer time horizons, store the send intent in your own datastore and call send_email closer to the target window.
Does scheduled delivery affect CAN-SPAM compliance?
Scheduled delivery is CAN-SPAM neutral — the compliance requirements (accurate From headers, functioning unsubscribe mechanism, physical mailing address) apply to message content regardless of when it sends. The gated_send approval step adds a documented review record, which is useful for commercial email programs that need to demonstrate human oversight of outbound campaigns.
How does list_pending differ from check_inbox?
check_inbox retrieves inbound messages that have arrived in a mailbox. list_pending surfaces outbound messages the agent has queued but not yet delivered — scheduled sends, messages awaiting approval, and messages in retry state. They operate on opposite sides of the email flow.
Can scheduled replies stay in context with the original email thread?
Yes. The reply_email endpoint accepts a scheduled_at parameter and a thread_id. The reply queues against the original thread and delivers in context, maintaining the conversation history visible to both the recipient and the agent.

Explore more use cases

The only agent email with a verifiable sender

Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 50-tool MCP server. Formally verified in Lean 4.