> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://docs.agentmail.to/llms.txt. For full content including API reference and SDK examples, see https://docs.agentmail.to/llms-full.txt.

# Drafts

> Learn how to create, manage, and send Drafts to enable advanced agent workflows like human-in-the-loop review and scheduled sending.

## 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.

```python
# You'll need an inbox ID to create a draft in.

new_draft=client.inboxes.drafts.create(
    inbox_id="outbound@domain.com",
    to=["review-team@example.com"],
    subject="[NEEDS REVIEW] Agent's proposed response"
)

print(f"Draft created successfully with ID: {new_draft.draft_id}")

```

**`TypeScript`**

```typescript title="TypeScript"
// You'll need an inbox ID to create a draft in.

const newDraft = await client.inboxes.drafts.create(
	"my_inbox@domain.com",
	{
		to: [
				"review-team@example.com"
			],
		subject: "[NEEDS REVIEW] Agent's proposed response"
	}
)

console.log(`Draft created successfully with ID: ${newDraft.draftId}`);
```

**`CLI`**

```bash title="CLI"
# create a draft in an inbox
agentmail inboxes drafts create \
  --inbox-id outbound@domain.com \
  --to review-team@example.com \
  --subject "[NEEDS REVIEW] Agent's proposed response"
```

### 2. Get `Draft`

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

**`Python`**

```python title="Python"
# Get the draft
draft = client.inboxes.drafts.get(inbox_id = “my_inbox@domain.com”, draft_id = “draft_id_123”)

```

**`TypeScript`**

```typescript title="TypeScript"

// Get the draft
const draft = await client.inboxes.drafts.get(
	"inbox_id",
	"draft_id_123"
)

```

**`CLI`**

```bash title="CLI"
# get a draft by id
agentmail 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.

**`Python`**

```python title="Python"

# This sends the draft and deletes it

sent_message = client.inboxes.drafts.send(inbox_id = 'my_inbox@domain.com', draft_id = 'draft_id_123')

print(f"Draft sent! New message ID: {sent_message.message_id}")

```

**`TypeScript`**

```typescript title="TypeScript"

const sentMessage = await client.inboxes.drafts.send('my_inbox@domain.com', 'draft_id_123');

console.log(`Draft sent! New message ID: ${sentMessage.message_id}`);
```

**`CLI`**

```bash title="CLI"
# send a draft
agentmail inboxes drafts send \
  --inbox-id my_inbox@domain.com \
  --draft-id draft_id_123
```

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.

**`Python`**

```python title="Python"
# create a draft reply to the sender of a received message
draft = client.inboxes.drafts.create(
    inbox_id="agent@domain.com",
    in_reply_to="<message-id@domain.com>",
    text="Thanks — looping in my manager for approval.",
)

# reply to everyone on the thread
reply_all = client.inboxes.drafts.create(
    inbox_id="agent@domain.com",
    in_reply_to="<message-id@domain.com>",
    reply_all=True,
    text="Thanks all — I'll follow up with the details.",
)

# forward a message as a draft for later review
forward = client.inboxes.drafts.create(
    inbox_id="agent@domain.com",
    forward_of="<message-id@domain.com>",
    to=["teammate@example.com"],
    text="See below — can you take this one?",
)

# review, then send when ready
client.inboxes.drafts.send(inbox_id="agent@domain.com", draft_id=draft.draft_id)
```

**`TypeScript`**

```typescript title="TypeScript"
// create a draft reply to the sender of a received message
const draft = await client.inboxes.drafts.create("agent@domain.com", {
  inReplyTo: "<message-id@domain.com>",
  text: "Thanks — looping in my manager for approval.",
});

// reply to everyone on the thread
const replyAll = await client.inboxes.drafts.create("agent@domain.com", {
  inReplyTo: "<message-id@domain.com>",
  replyAll: true,
  text: "Thanks all — I'll follow up with the details.",
});

// forward a message as a draft for later review
const forward = await client.inboxes.drafts.create("agent@domain.com", {
  forwardOf: "<message-id@domain.com>",
  to: ["teammate@example.com"],
  text: "See below — can you take this one?",
});

// review, then send when ready
await client.inboxes.drafts.send("agent@domain.com", draft.draftId);
```

**`CLI`**

```bash title="CLI"
# create a draft reply to a message
agentmail inboxes drafts create \
  --inbox-id agent@domain.com \
  --in-reply-to "<message-id@domain.com>" \
  --text "Thanks — looping in my manager for approval."

# forward a message as a draft
agentmail inboxes drafts create \
  --inbox-id agent@domain.com \
  --forward-of "<message-id@domain.com>" \
  --to teammate@example.com \
  --text "See below — can you take this one?"
```

## 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`.

**`Python`**

```python title="Python"
# edit the body and recipients, add a label, and clear the cc list
client.inboxes.drafts.update(
    inbox_id="agent@domain.com",
    draft_id="draft_id_123",
    text="Updated body copy.",
    cc=None,  # clear the cc field
    add_labels=["reviewed"],
)
```

**`TypeScript`**

```typescript title="TypeScript"
// edit the body and recipients, add a label, and clear the cc list
await client.inboxes.drafts.update("agent@domain.com", "draft_id_123", {
  text: "Updated body copy.",
  cc: null, // clear the cc field
  addLabels: ["reviewed"],
});
```

**`CLI`**

```bash title="CLI"
# update a draft's body
agentmail inboxes drafts update \
  --inbox-id agent@domain.com \
  --draft-id draft_id_123 \
  --text "Updated body copy."
```

## 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`.

**`Python`**

```python title="Python"
from datetime import datetime, timedelta

# Schedule an email for tomorrow at 9:00 AM UTC
send_time = (datetime.utcnow() + timedelta(days=1)).replace(
    hour=9, minute=0, second=0
)

scheduled_draft = client.inboxes.drafts.create(
    inbox_id="outreach@domain.com",
    to=["prospect@example.com"],
    subject="Following up on our conversation",
    text="Hi, just wanted to follow up on our chat yesterday...",
    send_at=send_time.isoformat() + "Z"
)

print(f"Draft scheduled for {scheduled_draft.send_at}")
# send_status will be "scheduled"
```

**`TypeScript`**

```typescript title="TypeScript"
// Schedule an email for tomorrow at 9:00 AM UTC
const sendTime = new Date();
sendTime.setUTCDate(sendTime.getUTCDate() + 1);
sendTime.setUTCHours(9, 0, 0, 0);

const scheduledDraft = await client.inboxes.drafts.create(
    "outreach@domain.com",
    {
        to: ["prospect@example.com"],
        subject: "Following up on our conversation",
        text: "Hi, just wanted to follow up on our chat yesterday...",
        sendAt: sendTime.toISOString()
    }
);

console.log(`Draft scheduled for ${scheduledDraft.sendAt}`);
// sendStatus will be "scheduled"
```

**`CLI`**

```bash title="CLI"
# schedule a draft for tomorrow at 9:00 am utc
agentmail inboxes drafts create \
  --inbox-id outreach@domain.com \
  --to prospect@example.com \
  --subject "Following up on our conversation" \
  --text "Hi, just wanted to follow up on our chat yesterday..." \
  --send-at 2026-04-01T09:00:00Z
```

### 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.

**`Python`**

```python title="Python"
# Reschedule to a different time
new_time = (datetime.utcnow() + timedelta(days=3)).replace(hour=14, minute=0, second=0)
client.inboxes.drafts.update(
    inbox_id="outreach@domain.com",
    draft_id=scheduled_draft.draft_id,
    send_at=new_time.isoformat() + "Z"
)

# Or un-schedule but keep the draft (clears send_at and the 'scheduled' label)
client.inboxes.drafts.update(
    inbox_id="outreach@domain.com",
    draft_id=scheduled_draft.draft_id,
    send_at=None
)

# Or cancel by deleting the draft entirely
client.inboxes.drafts.delete(
    inbox_id="outreach@domain.com",
    draft_id=scheduled_draft.draft_id
)
```

**`TypeScript`**

```typescript title="TypeScript"
// Reschedule to a different time
const newTime = new Date();
newTime.setUTCDate(newTime.getUTCDate() + 3);
newTime.setUTCHours(14, 0, 0, 0);

await client.inboxes.drafts.update(
    "outreach@domain.com",
    scheduledDraft.draftId,
    { sendAt: newTime.toISOString() }
);

// Or un-schedule but keep the draft (clears sendAt and the 'scheduled' label)
await client.inboxes.drafts.update(
    "outreach@domain.com",
    scheduledDraft.draftId,
    { sendAt: null }
);

// Or cancel by deleting the draft entirely
await client.inboxes.drafts.delete(
    "outreach@domain.com",
    scheduledDraft.draftId
);
```

**`CLI`**

```bash title="CLI"
# reschedule to a different time
agentmail inboxes drafts update \
  --inbox-id outreach@domain.com \
  --draft-id scheduled_draft_id \
  --send-at 2026-04-02T14:00:00Z

# or cancel by deleting the draft entirely
agentmail inboxes drafts delete \
  --inbox-id outreach@domain.com \
  --draft-id scheduled_draft_id
```

### 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.

**`Python`**

```python title="Python"
# List all scheduled drafts in an inbox
scheduled = client.inboxes.drafts.list(
    inbox_id="outreach@domain.com",
    labels=["scheduled"]
)

for draft in scheduled.drafts:
    print(f"{draft.subject} — scheduled for {draft.send_at} ({draft.send_status})")
```

**`TypeScript`**

```typescript title="TypeScript"
// List all scheduled drafts in an inbox
const scheduled = await client.inboxes.drafts.list(
    "outreach@domain.com",
    { labels: ["scheduled"] }
);

for (const draft of scheduled.drafts) {
    console.log(`${draft.subject} — scheduled for ${draft.sendAt} (${draft.sendStatus})`);
}
```

**`CLI`**

```bash title="CLI"
# list all scheduled drafts in an inbox
agentmail inboxes drafts list \
  --inbox-id outreach@domain.com \
  --labels scheduled
```

#### 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:**

**`Python`**

```python title="Python"
from datetime import datetime, timedelta

inbox_id = "outreach@domain.com"

# Send initial email
initial = client.inboxes.messages.send(
    inbox_id=inbox_id,
    to=["prospect@example.com"],
    subject="Quick question about your workflow",
    text="Hi, I noticed your team is scaling quickly..."
)

# Schedule follow-up for 3 days later
follow_up_time = (datetime.utcnow() + timedelta(days=3)).replace(hour=9, minute=0, second=0)

follow_up = client.inboxes.drafts.create(
    inbox_id=inbox_id,
    to=["prospect@example.com"],
    subject="Re: Quick question about your workflow",
    text="Hi again — just bumping this in case it got buried...",
    in_reply_to=initial.message_id,
    send_at=follow_up_time.isoformat() + "Z"
)

# Tag the thread so your webhook handler can find the draft
client.inboxes.threads.update(
    inbox_id=inbox_id,
    thread_id=initial.thread_id,
    add_labels=[f"follow-up:{follow_up.draft_id}"]
)
```

**`TypeScript`**

```typescript title="TypeScript"
const inboxId = "outreach@domain.com";

// Send initial email
const initial = await client.inboxes.messages.send(inboxId, {
    to: ["prospect@example.com"],
    subject: "Quick question about your workflow",
    text: "Hi, I noticed your team is scaling quickly..."
});

// Schedule follow-up for 3 days later
const followUpTime = new Date();
followUpTime.setUTCDate(followUpTime.getUTCDate() + 3);
followUpTime.setUTCHours(9, 0, 0, 0);

const followUp = await client.inboxes.drafts.create(inboxId, {
    to: ["prospect@example.com"],
    subject: "Re: Quick question about your workflow",
    text: "Hi again — just bumping this in case it got buried...",
    inReplyTo: initial.messageId,
    sendAt: followUpTime.toISOString()
});

// Tag the thread so your webhook handler can find the draft
await client.inboxes.threads.update(inboxId, initial.threadId, {
    addLabels: [`follow-up:${followUp.draftId}`]
});
```

**`CLI`**

```bash title="CLI"
# send the initial email
agentmail inboxes messages send \
  --inbox-id outreach@domain.com \
  --to prospect@example.com \
  --subject "Quick question about your workflow" \
  --text "Hi, I noticed your team is scaling quickly..."

# schedule follow-up for 3 days later
agentmail inboxes drafts create \
  --inbox-id outreach@domain.com \
  --to prospect@example.com \
  --subject "Re: Quick question about your workflow" \
  --text "Hi again — just bumping this in case it got buried..." \
  --in-reply-to initial_message_id \
  --send-at 2026-04-03T09:00:00Z
```

**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.

**`Python`**

```python title="Python"
# In your webhook handler for "message.received":
thread = client.inboxes.threads.get(inbox_id=inbox_id, thread_id=thread_id)

for label in thread.labels:
    if label.startswith("follow-up:"):
        draft_id = label.split("follow-up:")[1]
        try:
            client.inboxes.drafts.delete(inbox_id=inbox_id, draft_id=draft_id)
        except Exception:
            pass  # Draft may have already been sent
        client.inboxes.threads.update(
            inbox_id=inbox_id, thread_id=thread_id,
            remove_labels=[label]
        )
        break
```

**`TypeScript`**

```typescript title="TypeScript"
// In your webhook handler for "message.received":
const thread = await client.inboxes.threads.get(inboxId, threadId);

for (const label of thread.labels) {
    if (label.startsWith("follow-up:")) {
        const draftId = label.split("follow-up:")[1];
        try {
            await client.inboxes.drafts.delete(inboxId, draftId);
        } catch {
            // Draft may have already been sent
        }
        await client.inboxes.threads.update(inboxId, threadId, {
            removeLabels: [label]
        });
        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.

**`Python`**

```python title="Python"
# Get all drafts across the entire organization
all_drafts = client.drafts.list()

print(f"Found {all_drafts.count} drafts pending review.")

```

**`TypeScript`**

```typescript title="TypeScript"
// Get all drafts across the entire organization
const allDrafts = await client.drafts.list();

console.log(`Found ${allDrafts.count} drafts pending review.`);
```

**`CLI`**

```bash title="CLI"
# list all drafts across the entire organization
agentmail drafts list
```

## Copy for Cursor / Claude

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

**`Python`**

```python title="Python"
"""
AgentMail Drafts — copy into Cursor/Claude.

Setup: pip install agentmail python-dotenv. Set AGENTMAIL_API_KEY in .env.

API reference:
- inboxes.drafts.create(inbox_id, to?, subject?, text?, html?, cc?, bcc?, reply_to?, attachments?, in_reply_to?, forward_of?, reply_all?, send_at?)
    * in_reply_to=<message_id> -> reply draft (add reply_all=True for the whole thread)
    * forward_of=<message_id> -> forward draft (body + attachments merged at send time)
    * in_reply_to/forward_of are mutually exclusive; reading the source needs message_read permission
- inboxes.drafts.get(inbox_id, draft_id)
- 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?)
    * kind (plain/reply/forward) is fixed at create; omit=unchanged, null (or [] for recipients)=clear
    * send_at=null un-schedules; a draft already 'sending' can't be edited (409)
- inboxes.drafts.send(inbox_id, draft_id) — converts to Message, deletes draft
- inboxes.drafts.delete(inbox_id, draft_id)
- inboxes.drafts.list(inbox_id, limit?, page_token?, labels?)
- drafts.list(limit?, page_token?) — org-wide

Scheduled sending: pass send_at (ISO 8601 datetime) to create() or update().
Draft is auto-labeled 'scheduled' and sent at the specified time.
send_status: 'scheduled' | 'sending' | 'failed'.
Reschedule by updating send_at, un-schedule with send_at=None, or delete the draft.

Errors: SDK raises on 4xx/5xx. Rate limit: 429 with Retry-After.
"""
import os
from datetime import datetime, timedelta
from dotenv import load_dotenv
from agentmail import AgentMail

load_dotenv()
client = AgentMail(api_key=os.getenv("AGENTMAIL_API_KEY"))

inbox_id = "agent@agentmail.to"

# Create and send immediately
draft = client.inboxes.drafts.create(inbox_id, to=["review@example.com"], subject="[REVIEW] Proposed reply")
sent = client.inboxes.drafts.send(inbox_id, draft.draft_id)
print(sent.message_id)

# Schedule for later
send_time = (datetime.utcnow() + timedelta(days=1)).replace(hour=9, minute=0, second=0)
scheduled = client.inboxes.drafts.create(
    inbox_id, to=["prospect@example.com"], subject="Follow up",
    text="Just following up...", send_at=send_time.isoformat() + "Z"
)
print(f"Scheduled for {scheduled.send_at}, status: {scheduled.send_status}")

all_drafts = client.drafts.list()
```

**`TypeScript`**

```typescript title="TypeScript"
/**
 * AgentMail Drafts — copy into Cursor/Claude.
 *
 * Setup: npm install agentmail dotenv. Set AGENTMAIL_API_KEY in .env.
 *
 * API reference:
 * - inboxes.drafts.create(inboxId, { to?, subject?, text?, html?, cc?, bcc?, replyTo?, attachments?, inReplyTo?, forwardOf?, replyAll?, sendAt? })
 *     * inReplyTo=<messageId> -> reply draft (add replyAll: true for the whole thread)
 *     * forwardOf=<messageId> -> forward draft (body + attachments merged at send time)
 *     * inReplyTo/forwardOf are mutually exclusive; reading the source needs message_read permission
 * - inboxes.drafts.get(inboxId, draftId)
 * - inboxes.drafts.update(inboxId, draftId, { to?, cc?, bcc?, replyTo?, subject?, text?, html?, addAttachments?, removeAttachments?, addLabels?, removeLabels?, sendAt? })
 *     * kind (plain/reply/forward) is fixed at create; omit=unchanged, null (or [] for recipients)=clear
 *     * sendAt: null un-schedules; a draft already 'sending' can't be edited (409)
 * - inboxes.drafts.send(inboxId, draftId) — converts to Message, deletes draft
 * - inboxes.drafts.delete(inboxId, draftId)
 * - inboxes.drafts.list(inboxId, { limit?, pageToken?, labels? })
 * - drafts.list({ limit?, pageToken? }) — org-wide
 *
 * Scheduled sending: pass sendAt (ISO 8601 datetime) to create() or update().
 * Draft is auto-labeled 'scheduled' and sent at the specified time.
 * sendStatus: 'scheduled' | 'sending' | 'failed'.
 * Reschedule by updating sendAt, un-schedule with sendAt: null, or delete the draft.
 *
 * Errors: SDK throws on 4xx/5xx. Rate limit: 429 with Retry-After.
 */
import { AgentMailClient } from "agentmail";
import "dotenv/config";

const client = new AgentMailClient({ apiKey: process.env.AGENTMAIL_API_KEY! });

async function main() {
  const inboxId = "agent@agentmail.to";

  // Create and send immediately
  const draft = await client.inboxes.drafts.create(inboxId, {
    to: ["review@example.com"],
    subject: "[REVIEW] Proposed reply",
  });
  const sent = await client.inboxes.drafts.send(inboxId, draft.draftId);
  console.log(sent.messageId);

  // Schedule for later
  const sendTime = new Date();
  sendTime.setUTCDate(sendTime.getUTCDate() + 1);
  sendTime.setUTCHours(9, 0, 0, 0);

  const scheduled = await client.inboxes.drafts.create(inboxId, {
    to: ["prospect@example.com"],
    subject: "Follow up",
    text: "Just following up...",
    sendAt: sendTime.toISOString(),
  });
  console.log(`Scheduled for ${scheduled.sendAt}, status: ${scheduled.sendStatus}`);

  const allDrafts = await client.drafts.list();
}
main();
```