Drafts

Preparing and scheduling Messages for your agents.

What is a Draft?

A Draft is an unsent Message. It’s a resource that allows your agent to prepare the contents of an email—including recipients, a subject, a body, and Attachments—without sending it immediately.

We know agent reliability is big these days—with Drafts you can have agents have ready-to-send emails and only with your permission it can send them off into the world.

Drafts are a key component for building advanced agent workflows. They enable:

  • Human-in-the-Loop Review: An agent can create a Draft for a sensitive or important Message, which a human can then review and approve before it’s sent.
  • Scheduled Sending: Your agent can create a Draft and then have a separate process send it at a specific time, such as during business hours for the recipient.
  • Complex Composition: For Messages that require multiple steps to build (e.g., fetching data from several sources, generating content), Drafts allow you to save the state of the email as it’s being composed.

The Draft Lifecycle

You can interact with Drafts throughout their lifecycle, from creation to the moment they are sent.

1. Create a Draft

This is the first step. You create a Draft in a specific Inbox that will eventually be the sender.

1# You'll need an inbox ID to create a draft in.
2
3new_draft=client.inboxes.drafts.create(
4 inbox_id="outbound@domain.com",
5 to=["review-team@example.com"],
6 subject="[NEEDS REVIEW] Agent's proposed response"
7)
8
9print(f"Draft created successfully with ID: {new_draft.draft_id}")

2. Get Draft

Once a Draft is created, you can retrieve it by its ID

1# Get the draft
2draft = client.inboxes.drafts.get(inbox_id = “my_inbox@domain.com, draft_id = “draft_id_123”)

3. Send a Draft

This is the final step that converts the Draft into a sent Message. Once sent, the Draft is deleted.

1# This sends the draft and deletes it
2
3sent_message = client.inboxes.drafts.send(inbox_id = 'my_inbox@domain.com', draft_id = 'draft_id_123')
4
5print(f"Draft sent! New message ID: {sent_message.message_id}")

Note that now we access it by message_id now because now its a message!!

Reply and Forward Drafts

Instead of building a reply or forward by hand, you can create a Draft directly from an existing Message. Pass in_reply_to or forward_of to inboxes.drafts.create and AgentMail derives the subject and threading from the source message (for replies, the recipients too), so a human can review the prepared draft before it goes out. This creates a Draft (it does not send) — send it later with inboxes.drafts.send.

  • Reply: set in_reply_to to the source message ID. Add reply_all=True to address the whole thread (you then cannot also pass to, cc, or bcc).
  • Forward: set forward_of to the source message ID. The original body and attachments are merged in at send time, so recipients are optional — a forward draft can be saved now and addressed later.

in_reply_to and forward_of are mutually exclusive, and reading the referenced message requires the message_read permission.

1# create a draft reply to the sender of a received message
2draft = client.inboxes.drafts.create(
3 inbox_id="agent@domain.com",
4 in_reply_to="<message-id@domain.com>",
5 text="Thanks — looping in my manager for approval.",
6)
7
8# reply to everyone on the thread
9reply_all = client.inboxes.drafts.create(
10 inbox_id="agent@domain.com",
11 in_reply_to="<message-id@domain.com>",
12 reply_all=True,
13 text="Thanks all — I'll follow up with the details.",
14)
15
16# forward a message as a draft for later review
17forward = client.inboxes.drafts.create(
18 inbox_id="agent@domain.com",
19 forward_of="<message-id@domain.com>",
20 to=["teammate@example.com"],
21 text="See below — can you take this one?",
22)
23
24# review, then send when ready
25client.inboxes.drafts.send(inbox_id="agent@domain.com", draft_id=draft.draft_id)

Editing a Draft

Use inboxes.drafts.update to edit an existing draft’s to, cc, bcc, reply_to, subject, text, html, attachments, and labels. A draft’s kind (plain, reply, or forward) is fixed at creation and cannot be changed — to “make this a reply,” create a new draft.

  • Omit a field to leave it unchanged.
  • Pass null (or [] for a recipient field) to clear it.
  • Add or remove attachments with add_attachments / remove_attachments (by attachment ID).
  • Add or remove labels with add_labels / remove_labels.

A draft that is already being sent cannot be edited, and the request returns a 409 Conflict.

1# edit the body and recipients, add a label, and clear the cc list
2client.inboxes.drafts.update(
3 inbox_id="agent@domain.com",
4 draft_id="draft_id_123",
5 text="Updated body copy.",
6 cc=None, # clear the cc field
7 add_labels=["reviewed"],
8)

Scheduled Sending

You can schedule a Draft to be sent automatically at a future time by passing the send_at field when creating or updating a Draft. AgentMail will automatically send it at the specified time—no cron jobs or polling required.

Schedule a Draft

Pass an ISO 8601 datetime string to send_at. The Draft will be automatically labeled scheduled and its send_status will be set to scheduled.

1from datetime import datetime, timedelta
2
3# Schedule an email for tomorrow at 9:00 AM UTC
4send_time = (datetime.utcnow() + timedelta(days=1)).replace(
5 hour=9, minute=0, second=0
6)
7
8scheduled_draft = client.inboxes.drafts.create(
9 inbox_id="outreach@domain.com",
10 to=["prospect@example.com"],
11 subject="Following up on our conversation",
12 text="Hi, just wanted to follow up on our chat yesterday...",
13 send_at=send_time.isoformat() + "Z"
14)
15
16print(f"Draft scheduled for {scheduled_draft.send_at}")
17# send_status will be "scheduled"

Cancel or Reschedule

To reschedule, update send_at with a new time. To keep the draft but cancel the scheduled send, update send_at to null — this clears send_at and the scheduled label, leaving an unscheduled draft you can edit or send manually. To discard the draft entirely, delete it.

1# Reschedule to a different time
2new_time = (datetime.utcnow() + timedelta(days=3)).replace(hour=14, minute=0, second=0)
3client.inboxes.drafts.update(
4 inbox_id="outreach@domain.com",
5 draft_id=scheduled_draft.draft_id,
6 send_at=new_time.isoformat() + "Z"
7)
8
9# Or un-schedule but keep the draft (clears send_at and the 'scheduled' label)
10client.inboxes.drafts.update(
11 inbox_id="outreach@domain.com",
12 draft_id=scheduled_draft.draft_id,
13 send_at=None
14)
15
16# Or cancel by deleting the draft entirely
17client.inboxes.drafts.delete(
18 inbox_id="outreach@domain.com",
19 draft_id=scheduled_draft.draft_id
20)

List Scheduled Drafts

When a Draft is created with send_at, it is automatically labeled scheduled. You can filter for scheduled drafts using the labels query parameter.

1# List all scheduled drafts in an inbox
2scheduled = client.inboxes.drafts.list(
3 inbox_id="outreach@domain.com",
4 labels=["scheduled"]
5)
6
7for draft in scheduled.drafts:
8 print(f"{draft.subject} — scheduled for {draft.send_at} ({draft.send_status})")
send_status Values
  • scheduled — The draft is queued and will be sent at the send_at time.
  • sending — The draft is currently being processed for delivery.
  • failed — The send attempt failed. You can retry by updating send_at to a new time.

Conditional Follow-Ups

A common pattern is “send a follow-up in 3 days, but only if they haven’t replied.” You can implement this by scheduling a follow-up Draft, then cancelling it via Webhook if a reply arrives.

1. Send the initial email and schedule the follow-up:

1from datetime import datetime, timedelta
2
3inbox_id = "outreach@domain.com"
4
5# Send initial email
6initial = client.inboxes.messages.send(
7 inbox_id=inbox_id,
8 to=["prospect@example.com"],
9 subject="Quick question about your workflow",
10 text="Hi, I noticed your team is scaling quickly..."
11)
12
13# Schedule follow-up for 3 days later
14follow_up_time = (datetime.utcnow() + timedelta(days=3)).replace(hour=9, minute=0, second=0)
15
16follow_up = client.inboxes.drafts.create(
17 inbox_id=inbox_id,
18 to=["prospect@example.com"],
19 subject="Re: Quick question about your workflow",
20 text="Hi again — just bumping this in case it got buried...",
21 in_reply_to=initial.message_id,
22 send_at=follow_up_time.isoformat() + "Z"
23)
24
25# Tag the thread so your webhook handler can find the draft
26client.inboxes.threads.update(
27 inbox_id=inbox_id,
28 thread_id=initial.thread_id,
29 add_labels=[f"follow-up:{follow_up.draft_id}"]
30)

2. Cancel on reply via webhook:

When a reply comes in, look for the follow-up:<draft_id> label on the thread and delete the draft.

1# In your webhook handler for "message.received":
2thread = client.inboxes.threads.get(inbox_id=inbox_id, thread_id=thread_id)
3
4for label in thread.labels:
5 if label.startswith("follow-up:"):
6 draft_id = label.split("follow-up:")[1]
7 try:
8 client.inboxes.drafts.delete(inbox_id=inbox_id, draft_id=draft_id)
9 except Exception:
10 pass # Draft may have already been sent
11 client.inboxes.threads.update(
12 inbox_id=inbox_id, thread_id=thread_id,
13 remove_labels=[label]
14 )
15 break

If the prospect replies, the follow-up is cancelled. If they don’t, it sends automatically at the scheduled time.

Org-Wide Draft Management

Similar to Threads, you can list all Drafts across your entire Organization. This is perfect for building a central dashboard where a human supervisor can view, approve, or delete any Draft created by any agent in your fleet.

1# Get all drafts across the entire organization
2all_drafts = client.drafts.list()
3
4print(f"Found {all_drafts.count} drafts pending review.")

Copy for Cursor / Claude

Copy one of the blocks below into Cursor or Claude for complete Drafts API knowledge in one shot.

1"""
2AgentMail Drafts — copy into Cursor/Claude.
3
4Setup: pip install agentmail python-dotenv. Set AGENTMAIL_API_KEY in .env.
5
6API reference:
7- inboxes.drafts.create(inbox_id, to?, subject?, text?, html?, cc?, bcc?, reply_to?, attachments?, in_reply_to?, forward_of?, reply_all?, send_at?)
8 * in_reply_to=<message_id> -> reply draft (add reply_all=True for the whole thread)
9 * forward_of=<message_id> -> forward draft (body + attachments merged at send time)
10 * in_reply_to/forward_of are mutually exclusive; reading the source needs message_read permission
11- inboxes.drafts.get(inbox_id, draft_id)
12- inboxes.drafts.update(inbox_id, draft_id, to?, cc?, bcc?, reply_to?, subject?, text?, html?, add_attachments?, remove_attachments?, add_labels?, remove_labels?, send_at?)
13 * kind (plain/reply/forward) is fixed at create; omit=unchanged, null (or [] for recipients)=clear
14 * send_at=null un-schedules; a draft already 'sending' can't be edited (409)
15- inboxes.drafts.send(inbox_id, draft_id) — converts to Message, deletes draft
16- inboxes.drafts.delete(inbox_id, draft_id)
17- inboxes.drafts.list(inbox_id, limit?, page_token?, labels?)
18- drafts.list(limit?, page_token?) — org-wide
19
20Scheduled sending: pass send_at (ISO 8601 datetime) to create() or update().
21Draft is auto-labeled 'scheduled' and sent at the specified time.
22send_status: 'scheduled' | 'sending' | 'failed'.
23Reschedule by updating send_at, un-schedule with send_at=None, or delete the draft.
24
25Errors: SDK raises on 4xx/5xx. Rate limit: 429 with Retry-After.
26"""
27import os
28from datetime import datetime, timedelta
29from dotenv import load_dotenv
30from agentmail import AgentMail
31
32load_dotenv()
33client = AgentMail(api_key=os.getenv("AGENTMAIL_API_KEY"))
34
35inbox_id = "agent@agentmail.to"
36
37# Create and send immediately
38draft = client.inboxes.drafts.create(inbox_id, to=["review@example.com"], subject="[REVIEW] Proposed reply")
39sent = client.inboxes.drafts.send(inbox_id, draft.draft_id)
40print(sent.message_id)
41
42# Schedule for later
43send_time = (datetime.utcnow() + timedelta(days=1)).replace(hour=9, minute=0, second=0)
44scheduled = client.inboxes.drafts.create(
45 inbox_id, to=["prospect@example.com"], subject="Follow up",
46 text="Just following up...", send_at=send_time.isoformat() + "Z"
47)
48print(f"Scheduled for {scheduled.send_at}, status: {scheduled.send_status}")
49
50all_drafts = client.drafts.list()