daimon.email
Guides

Scheduled Follow-ups

Schedule emails for future delivery using send_at parameter

Overview

Scheduled follow-ups allow AI agents to plan email sequences in advance. Instead of sending immediately, agents can schedule messages for optimal timing:

  • Follow-up sequences - "Send reminder if no reply in 3 days"
  • Time zone optimization - Schedule for 9am in recipient's timezone
  • Drip campaigns - Stagger outreach over days or weeks
  • Reminder emails - "Meeting in 24 hours" notifications
  • Deadline warnings - "Invoice due tomorrow" alerts

daimon.email schedules are precise to the second and guaranteed to deliver within 60 seconds of the scheduled time.

Info

Scheduled sends use the send_at parameter with ISO 8601 timestamps. Messages remain in draft status until the scheduled time.

Basic Scheduling

Schedule an email by providing send_at in ISO 8601 format:

import { DaimonClient } from '@daimon/sdk';

const client = new DaimonClient({ apiKey: process.env.DAIMON_API_KEY });

// Schedule email for 3 days from now at 9am
const sendAt = new Date();
sendAt.setDate(sendAt.getDate() + 3);
sendAt.setHours(9, 0, 0, 0);

const scheduled = await client.inboxes.send('inbox_abc123', {
  to: 'prospect@company.com',
  subject: 'Following up on our conversation',
  text: 'Hi there, just checking in...',
  send_at: sendAt.toISOString(), // "2026-03-19T09:00:00.000Z"
});

console.log(`Scheduled for ${scheduled.send_at}`);
console.log(`Message ID: ${scheduled.message_id}`);
console.log(`Status: ${scheduled.status}`); // "scheduled"
from daimon import DaimonClient
from datetime import datetime, timedelta

client = DaimonClient(api_key=os.environ['DAIMON_API_KEY'])

# Schedule for 3 days from now at 9am
send_at = datetime.now() + timedelta(days=3)
send_at = send_at.replace(hour=9, minute=0, second=0, microsecond=0)

scheduled = client.inboxes.send(
    inbox_id='inbox_abc123',
    to='prospect@company.com',
    subject='Following up on our conversation',
    text='Hi there, just checking in...',
    send_at=send_at.isoformat()
)

print(f"Scheduled for {scheduled.send_at}")
print(f"Status: {scheduled.status}")  # "scheduled"
# Schedule email for specific timestamp
curl -X POST https://api.daimon.email/v1/inboxes/inbox_abc123/send \
  -H "Authorization: Bearer dm_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "prospect@company.com",
    "subject": "Following up on our conversation",
    "text": "Hi there, just checking in...",
    "send_at": "2026-03-19T09:00:00.000Z"
  }'

# Response:
# {
#   "message_id": "msg_9kL2xYw1pQm3",
#   "status": "scheduled",
#   "send_at": "2026-03-19T09:00:00.000Z",
#   "inbox_id": "inbox_abc123",
#   "to": ["prospect@company.com"]
# }

Conditional Follow-ups

The most powerful pattern: conditional scheduling based on recipient behavior.

Example: Send reminder only if no reply received

// Day 0: Send initial email
const initial = await client.inboxes.send(inboxId, {
  to: 'prospect@company.com',
  subject: 'Quick question about your infrastructure',
  text: 'Hi, I noticed you recently...',
  client_id: 'outreach_prospect_initial', // For idempotency
});

// Immediately schedule follow-up for 3 days later
const followupTime = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000);

const followup = await client.inboxes.send(inboxId, {
  to: 'prospect@company.com',
  subject: 'Re: Quick question about your infrastructure',
  text: 'Following up on my previous email...',
  send_at: followupTime.toISOString(),
  thread_id: initial.thread_id, // Keep in same thread
  client_id: 'outreach_prospect_followup',
});

// Day 1-3: Check for replies via webhook or polling
client.webhooks.on('message.received', async (event) => {
  const message = event.data;

  // If they replied to our thread, cancel the scheduled follow-up
  if (message.thread_id === initial.thread_id) {
    await client.messages.cancel(followup.message_id);
    console.log('Reply received, follow-up cancelled');
  }
});
from daimon import DaimonClient
from datetime import datetime, timedelta

client = DaimonClient(api_key=os.environ['DAIMON_API_KEY'])

# Send initial email
initial = client.inboxes.send(
    inbox_id=inbox_id,
    to='prospect@company.com',
    subject='Quick question about your infrastructure',
    text='Hi, I noticed you recently...',
    client_id='outreach_prospect_initial'
)

# Schedule follow-up for 3 days later
followup_time = datetime.now() + timedelta(days=3)

followup = client.inboxes.send(
    inbox_id=inbox_id,
    to='prospect@company.com',
    subject='Re: Quick question about your infrastructure',
    text='Following up on my previous email...',
    send_at=followup_time.isoformat(),
    thread_id=initial.thread_id,
    client_id='outreach_prospect_followup'
)

# Later: Check if reply received, cancel if so
thread = client.threads.get(initial.thread_id)
if thread.message_count > 1:  # They replied
    client.messages.cancel(followup.message_id)
    print('Reply received, follow-up cancelled')

Note

Use client_id for idempotency. If your agent crashes and retries, you won't create duplicate scheduled emails.

Multi-step Sequences

Build complex drip campaigns by chaining scheduled emails:

const sequence = [
  { delay: 0, subject: 'Introduction to daimon.email', template: 'intro' },
  { delay: 2, subject: 'How agents use daimon.email', template: 'use_cases' },
  { delay: 5, subject: 'Ready to get started?', template: 'cta' },
  { delay: 10, subject: 'Last chance: 20% off your first month', template: 'final_offer' },
];

const startDate = new Date();

for (const step of sequence) {
  const sendAt = new Date(startDate);
  sendAt.setDate(sendAt.getDate() + step.delay);
  sendAt.setHours(9, 0, 0, 0); // Always send at 9am

  await client.inboxes.send(inboxId, {
    to: 'newuser@company.com',
    subject: step.subject,
    html: await renderTemplate(step.template),
    send_at: sendAt.toISOString(),
    client_id: `sequence_${userId}_step${step.delay}`,
  });
}

console.log(`Scheduled ${sequence.length} emails over ${sequence[sequence.length - 1].delay} days`);

Timezone-aware Scheduling

Schedule emails to arrive at optimal local time for recipients:

import { DateTime } from 'luxon';

// Get recipient's timezone (from CRM, IP lookup, or previous emails)
const recipientTimezone = 'America/New_York';

// Schedule for 9am in their timezone tomorrow
const sendAt = DateTime.now()
  .setZone(recipientTimezone)
  .plus({ days: 1 })
  .set({ hour: 9, minute: 0, second: 0 })
  .toUTC()
  .toISO();

await client.inboxes.send(inboxId, {
  to: 'recipient@company.com',
  subject: 'Good morning!',
  text: 'Hope you had a great weekend...',
  send_at: sendAt,
});
from datetime import datetime
from zoneinfo import ZoneInfo

# Get recipient's timezone
recipient_timezone = ZoneInfo('America/New_York')

# Schedule for 9am in their timezone tomorrow
send_at = datetime.now(recipient_timezone).replace(
    hour=9, minute=0, second=0, microsecond=0
) + timedelta(days=1)

client.inboxes.send(
    inbox_id=inbox_id,
    to='recipient@company.com',
    subject='Good morning!',
    text='Hope you had a great weekend...',
    send_at=send_at.isoformat()
)

Managing Scheduled Messages

List scheduled messages

// Get all scheduled messages for an inbox
const scheduled = await client.inboxes.listMessages(inboxId, {
  status: 'scheduled',
  sort: 'send_at:asc',
});

console.log(`${scheduled.total} emails scheduled`);
scheduled.messages.forEach(msg => {
  console.log(`- ${msg.subject} → ${msg.to} at ${msg.send_at}`);
});

Cancel a scheduled message

// Cancel before it sends
await client.messages.cancel('msg_9kL2xYw1pQm3');

// Or cancel all scheduled messages in a thread
const thread = await client.threads.get('thread_abc123');
for (const message of thread.messages) {
  if (message.status === 'scheduled') {
    await client.messages.cancel(message.message_id);
  }
}

Update scheduled time

// Reschedule to a new time
await client.messages.update('msg_9kL2xYw1pQm3', {
  send_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // +7 days
});

Rate Limiting & Quotas

Scheduled sends count against your account's daily send limit at the time they're scheduled to send, not when created.

TierDaily Send LimitMax Scheduled (Future)
Free0 (send disabled)N/A
Starter1,000/day10,000
Pro10,000/day100,000
EnterpriseCustomCustom

Info

If you hit your daily send limit, scheduled messages will queue and retry the next day. You'll receive a webhook event message.send_failed with reason RATE_LIMIT_EXCEEDED.

Best Practices

Use client_id for Idempotency

Prevent duplicate scheduled emails if your agent retries. Include unique identifiers in client_id.

Cancel Obsolete Follow-ups

If recipient replies or takes action, cancel pending follow-ups in that thread.

Respect Time Zones

Schedule emails for recipient's local business hours (9am-5pm) for best engagement.

Monitor Send Failures

Subscribe to message.send_failed webhooks to catch bounces or deliverability issues.

Advanced: Smart Re-engagement

Automatically re-engage dormant threads:

// Find threads with no activity in 30 days
const staleThreads = await client.threads.list({
  last_activity_before: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
  status: 'open',
});

for (const thread of staleThreads.threads) {
  // Schedule re-engagement email for tomorrow at 9am
  const sendAt = new Date();
  sendAt.setDate(sendAt.getDate() + 1);
  sendAt.setHours(9, 0, 0, 0);

  await client.inboxes.send(thread.inbox_id, {
    to: thread.participants[0], // Original recipient
    subject: `Re: ${thread.subject}`,
    text: 'Just wanted to circle back on this...',
    thread_id: thread.thread_id,
    send_at: sendAt.toISOString(),
    client_id: `reengage_${thread.thread_id}_${Date.now()}`,
  });
}

console.log(`Scheduled ${staleThreads.total} re-engagement emails`);

Webhook Events

Subscribe to these events to monitor scheduled messages:

EventWhen FiredPayload
message.scheduledMessage scheduled successfullymessage_id, send_at, inbox_id
message.send_startedDelivery begins (at scheduled time)message_id, to, subject
message.sentSuccessfully deliveredmessage_id, smtp_id, delivered_at
message.send_failedDelivery failedmessage_id, error, reason
message.cancelledScheduled send cancelledmessage_id, cancelled_by, cancelled_at