Skip to content

Guide: Sending & Receiving Email ​

Fresh 🌱

A step-by-step guide to the practical workflow of sending initial emails and handling replies to have a full conversation.

This guide walks you through the complete, practical workflow of an agent having a conversation. While the Core Concepts pages detail the individual API calls, this guide shows you how to stitch them together to create a functional conversational loop.

The Foundation: Sending HTML & Text ​

As a quick reminder from our Messages documentation, it's a critical best practice to always provide both an html and a text version of your email. This ensures readability on all email clients and significantly improves deliverability.

python
# Always provide both html and text when possible
client.inboxes.messages.send(
    inbox_id="outreach@agentmail.to",
    to=["potential-customer@example.com"],
    subject="Following up",
    text="Hi Jane,\n\nThis is a plain-text version of our email.",
    html="<p>Hi Jane,</p><p>This is a <strong>rich HTML</strong> version of our email.</p>",
    labels=["outreach-campaign"]
)

The Conversational Loop ​

A common task for an agent is to check for replies in an Inbox and then respond to them. While using Webhooks is the most efficient method for this, you can also build a simple polling mechanism.

Here's the step-by-step logic for a polling-based conversational agent.

First, you need to identify which conversations have new messages that your agent hasn't responded to. A great way to manage this is with Labels. You can list Threads in a specific Inbox that have an unreplied Label.

python
# Find all threads in this inbox that are marked as unreplied
threadsRes = client.threads.list(
    labels = ["unreplied"]
)
if threadsRes.count == 0:
    print("No threads need a reply.")
else:
    # Let's work on the first unreplied thread
    thread_to_reply_to = threadsRes.thread[0]
typescript
// Find all threads in this inbox that are marked as unreplied
const threadRes = await client.threads.list(
    {
        labels: [
            "huh"
        ]
    }
)

if (threadRes.count === 0) {
    console.log("No threads need a reply.");
} else {
    // Let's work on the first unreplied thread
    const threadToReplyTo = threadRes.threads[0];
}

To reply to a conversation, you need to reply to the most recent message in the Thread. You can get a specific Thread by its ID, which will contain a list of all its Messages. You'll then grab the ID of the last Message in that list.

python
# Get the full thread object to access its messages
thread_details = client.threads.get(thread_to_reply_to.thread_id)

# The last message in the list is the one we want to reply to
last_message = thread_details.messages[-1]
message_id_to_reply_to = last_message.message_id
typescript
// Get the full thread object to access its messages
const threadDetails = await client.threads.get('thread_id');

// The last message in the array is the one we want to reply to
const lastMessage = threadDetails.messages[threadDetails.messages.length - 1];
const messageIdToReplyTo = lastMessage.message_id;

Use last_message.extracted_text (or extracted_html) when you need just the new reply content, without quoted history.

Now that you have the message_id to reply to, you can send your agent's response. It's also a best practice to update the Labels on the original Message at the same time, removing the unreplied Label and adding a replied Label to prevent the agent from replying to the same message twice.

python
# Send the reply
client.inboxes.messages.reply(
    inbox_id="support@agentmail.to",
    message_id=message_id_to_reply_to,
    text="This is our agent's helpful reply!"
)

# Update the labels on the original message
client.inboxes.messages.update(
    inbox_id="support@agentmail.to",
    message_id=message_id_to_reply_to,
    add_labels=["replied"],
    remove_labels=["unreplied"]
)
typescript
// Send the reply
await client.inboxes.messages.reply("support@agentmail.to", messageIdToReplyTo, {
    text: "This is our agent's helpful reply!",
});

// Update the labels on the original message
await client.inboxes.messages.update("support@agentmail.to", messageIdToReplyTo, {
    addLabels: ["replied"],
    removeLabels: ["unreplied"],
});
bash
# send the reply
agentmail inboxes:messages reply \
  --inbox-id support@agentmail.to \
  --message-id <message_id> \
  --text "This is our agent's helpful reply!"

# update the labels on the original message
agentmail inboxes:messages update \
  --inbox-id support@agentmail.to \
  --message-id <message_id> \
  --add-label replied \
  --remove-label unreplied

For production applications, polling is inefficient. The best way to handle incoming replies is to use Webhooks. This allows AgentMail to notify your agent instantly when a new Message arrives, so you can reply in real-time.

Learn how to set up Webhooks →

Scheduling Emails ​

Instead of sending immediately, you can schedule emails for a future time—perfect for delivering messages during business hours or spacing out outreach.

Create a Draft with the send_at field and AgentMail handles the rest. The email is automatically sent at the specified time.

python
from datetime import datetime, timedelta

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

client.inboxes.drafts.create(
    inbox_id="outreach@agentmail.to",
    to=["prospect@example.com"],
    subject="Quick question about your workflow",
    text="Hi, I noticed you're using...",
    html="<p>Hi, I noticed you're using...</p>",
    send_at=send_time.isoformat() + "Z"
)
typescript
// Schedule for tomorrow at 9 AM UTC
const sendTime = new Date();
sendTime.setUTCDate(sendTime.getUTCDate() + 1);
sendTime.setUTCHours(9, 0, 0, 0);

await client.inboxes.drafts.create(
    "outreach@agentmail.to",
    {
        to: ["prospect@example.com"],
        subject: "Quick question about your workflow",
        text: "Hi, I noticed you're using...",
        html: "<p>Hi, I noticed you're using...</p>",
        sendAt: sendTime.toISOString()
    }
);

For more details on scheduled sending—including how to cancel, reschedule, list scheduled drafts, and build conditional follow-up workflows—see the Drafts page.