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.
| Tier | Daily Send Limit | Max Scheduled (Future) |
|---|---|---|
| Free | 0 (send disabled) | N/A |
| Starter | 1,000/day | 10,000 |
| Pro | 10,000/day | 100,000 |
| Enterprise | Custom | Custom |
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:
| Event | When Fired | Payload |
|---|---|---|
message.scheduled | Message scheduled successfully | message_id, send_at, inbox_id |
message.send_started | Delivery begins (at scheduled time) | message_id, to, subject |
message.sent | Successfully delivered | message_id, smtp_id, delivered_at |
message.send_failed | Delivery failed | message_id, error, reason |
message.cancelled | Scheduled send cancelled | message_id, cancelled_by, cancelled_at |