ExamplesOutbound
Cold Email Sequence
Multi-step email sequences with scheduling
Overview
A cold email sequence agent orchestrates multi-step campaigns with scheduled sends, automatic follow-ups, and intelligent sequence cancellation based on recipient behavior. This example demonstrates the Drafts API for scheduling future sends.
Note
Sending emails requires a paid tier. This example uses the Drafts API to schedule sends and manage sequences.
How It Works
- Create a multi-step sequence template (3-5 emails)
- Schedule all steps as drafts with
send_attimestamps - Monitor for replies via webhooks
- Cancel remaining drafts if prospect replies
- Respect send limits and timing rules
- Track sequence performance metrics
Complete Implementation
import { DaimonClient } from 'daimon-email';
import express from 'express';
const client = new DaimonClient({ apiKey: process.env.DAIMON_API_KEY });
const app = express();
app.use(express.json());
interface Prospect {
email: string;
name: string;
company: string;
}
interface SequenceStep {
dayOffset: number; // Days after sequence start
subject: string;
bodyTemplate: (prospect: Prospect) => string;
}
// Step 1: Define sequence template
const coldSequence: SequenceStep[] = [
{
dayOffset: 0,
subject: 'Quick question about {{company}}',
bodyTemplate: (p) => `Hi ${p.name},
I noticed ${p.company} is in the [industry] space and thought you might be interested in how we're helping similar companies [specific value prop].
Would love to share a quick case study if you're open to it.
Best,
Sales Agent`
},
{
dayOffset: 3,
subject: 'Re: Quick question about {{company}}',
bodyTemplate: (p) => `Hi ${p.name},
Following up on my previous email.
I wanted to share a quick win from a company similar to ${p.company} - they saw a 3x improvement in [metric] within 30 days.
Worth a 10-minute call to explore?
Best,
Sales Agent`
},
{
dayOffset: 7,
subject: 'Re: Quick question about {{company}}',
bodyTemplate: (p) => `${p.name},
I know you're busy, so I'll keep this brief.
We have a limited number of Q2 onboarding slots, and I think ${p.company} would be a perfect fit.
If you're interested, I'd love to share more details. Otherwise, I'll assume the timing isn't right.
Best,
Sales Agent`
},
{
dayOffset: 14,
subject: 'Re: Quick question about {{company}}',
bodyTemplate: (p) => `${p.name},
No worries if the timing isn't right!
I'll close this out on my end. Feel free to reach out if anything changes in the future.
Best of luck with ${p.company}!
Sales Agent`
}
];
// Step 2: Schedule entire sequence as drafts
async function scheduleSequence(inboxId: string, prospect: Prospect) {
console.log(`Scheduling sequence for ${prospect.email}`);
const now = new Date();
const draftIds: string[] = [];
for (const step of coldSequence) {
const sendAt = new Date(now.getTime() + step.dayOffset * 24 * 60 * 60 * 1000);
const subject = step.subject.replace('{{company}}', prospect.company);
const body = step.bodyTemplate(prospect);
// Create draft with scheduled send time
const draft = await client.drafts.create(inboxId, {
to: prospect.email,
subject,
body,
sendAt: sendAt.toISOString(),
clientId: `sequence-${prospect.email}-step-${step.dayOffset}`,
metadata: {
sequenceId: `sequence-${prospect.email}`,
stepNumber: coldSequence.indexOf(step),
prospectEmail: prospect.email
}
});
draftIds.push(draft.id);
console.log(` Step ${step.dayOffset}: Scheduled for ${sendAt.toISOString()}`);
}
// Store draft IDs for potential cancellation
return {
sequenceId: `sequence-${prospect.email}`,
draftIds,
prospect
};
}
// Step 3: Monitor for replies and cancel sequence
app.post('/webhook', async (req, res) => {
const { event, message } = req.body;
if (event === 'message.received') {
await handleReply(message);
}
res.status(200).send('OK');
});
async function handleReply(message: any) {
const prospectEmail = message.from;
console.log(`✅ Reply received from ${prospectEmail} - canceling remaining sequence`);
// Find and cancel all pending drafts for this prospect
const drafts = await client.drafts.list(message.inboxId, {
status: 'scheduled'
});
const prospectDrafts = drafts.filter(d =>
d.metadata?.prospectEmail === prospectEmail &&
d.to === prospectEmail
);
for (const draft of prospectDrafts) {
await client.drafts.delete(message.inboxId, draft.id);
console.log(` Cancelled draft: Step ${draft.metadata.stepNumber}`);
}
console.log(`Sequence cancelled for ${prospectEmail}`);
// Handle the reply (send personalized response, etc.)
await sendPersonalizedReply(message);
}
async function sendPersonalizedReply(message: any) {
const response = `Thanks for your reply! I'll reach out with more details shortly.`;
await client.inboxes.messages.reply(message.inboxId, message.id, {
body: response,
clientId: `manual-reply-${message.id}`
});
}
// Step 4: Bulk sequence launch
async function launchCampaign(inboxId: string, prospects: Prospect[]) {
console.log(`Launching campaign for ${prospects.length} prospects`);
const sequences = [];
for (const prospect of prospects) {
const sequence = await scheduleSequence(inboxId, prospect);
sequences.push(sequence);
// Rate limiting: 1 sequence per 2 seconds
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log(`\n✅ Campaign launched!`);
console.log(`Total sequences: ${sequences.length}`);
console.log(`Total scheduled emails: ${sequences.length * coldSequence.length}`);
return sequences;
}
// Step 5: Respect send limits
async function checkSendLimits(inboxId: string, prospectCount: number) {
const capabilities = await client.account.getCapabilities();
// Find send limit from capabilities
const sendLimit = capabilities.can.find(c => c.action === 'send_messages');
if (!sendLimit) {
throw new Error('Sending not available on current tier');
}
const totalEmails = prospectCount * coldSequence.length;
console.log(`Send limit check:`);
console.log(` Total emails in campaign: ${totalEmails}`);
console.log(` Current tier: ${capabilities.tier}`);
// Could check daily/monthly limits here
// if (totalEmails > dailyLimit) { ... }
}
// Step 6: Campaign analytics
async function getCampaignAnalytics(inboxId: string, sequenceIds: string[]) {
const stats = {
sent: 0,
pending: 0,
cancelled: 0,
replied: 0,
openRate: 0,
replyRate: 0
};
// Get all drafts
const allDrafts = await client.drafts.list(inboxId);
for (const sequenceId of sequenceIds) {
const sequenceDrafts = allDrafts.filter(d =>
d.metadata?.sequenceId === sequenceId
);
stats.pending += sequenceDrafts.filter(d => d.status === 'scheduled').length;
stats.sent += sequenceDrafts.filter(d => d.status === 'sent').length;
}
// Get messages to check for replies
const messages = await client.inboxes.messages.list(inboxId);
stats.replied = new Set(messages.map(m => m.from)).size;
stats.replyRate = stats.sent > 0 ? (stats.replied / stats.sent) * 100 : 0;
return stats;
}
// Step 7: Pause/resume sequences
async function pauseSequence(inboxId: string, sequenceId: string) {
const drafts = await client.drafts.list(inboxId, {
status: 'scheduled'
});
const sequenceDrafts = drafts.filter(d =>
d.metadata?.sequenceId === sequenceId
);
for (const draft of sequenceDrafts) {
// Update to change status or reschedule
await client.drafts.update(inboxId, draft.id, {
metadata: {
...draft.metadata,
paused: true
}
});
}
console.log(`Paused sequence ${sequenceId}`);
}
async function resumeSequence(inboxId: string, sequenceId: string) {
const drafts = await client.drafts.list(inboxId);
const sequenceDrafts = drafts.filter(d =>
d.metadata?.sequenceId === sequenceId &&
d.metadata?.paused === true
);
for (const draft of sequenceDrafts) {
await client.drafts.update(inboxId, draft.id, {
metadata: {
...draft.metadata,
paused: false
}
});
}
console.log(`Resumed sequence ${sequenceId}`);
}
// Complete workflow
async function runColdSequenceCampaign() {
const inbox = await client.inboxes.create({
username: 'cold-outreach',
clientId: 'cold-sequence-v1'
});
console.log(`Campaign inbox: ${inbox.address}\n`);
// Example prospects
const prospects: Prospect[] = [
{ email: 'john@techcorp.com', name: 'John', company: 'TechCorp' },
{ email: 'sarah@innovate.io', name: 'Sarah', company: 'Innovate.io' },
{ email: 'mike@startup.ai', name: 'Mike', company: 'Startup.AI' }
];
// Check send limits
await checkSendLimits(inbox.id, prospects.length);
// Launch campaign
const sequences = await launchCampaign(inbox.id, prospects);
// Register webhook for replies
const authedClient = new DaimonClient({ apiKey: inbox.apiKey });
await authedClient.webhooks.create({
endpointUrl: 'https://your-agent.com/webhook',
events: ['message.received'],
inboxId: inbox.id
});
console.log('\n📊 Campaign Dashboard:');
console.log(' Monitor replies at http://localhost:3000/analytics');
// Start webhook server
app.get('/analytics', async (req, res) => {
const stats = await getCampaignAnalytics(
inbox.id,
sequences.map(s => s.sequenceId)
);
res.json(stats);
});
app.listen(3000, () => {
console.log('\n✅ Cold sequence campaign running on port 3000');
});
}
runColdSequenceCampaign();from daimon_email import DaimonClient
from flask import Flask, request, jsonify
import os
from datetime import datetime, timedelta
from typing import List, Dict
client = DaimonClient(api_key=os.environ.get('DAIMON_API_KEY'))
app = Flask(__name__)
# Step 1: Define sequence template
def get_sequence_steps():
return [
{
'day_offset': 0,
'subject': 'Quick question about {company}',
'body_template': lambda p: f"""Hi {p['name']},
I noticed {p['company']} is in the [industry] space and thought you might be interested in how we're helping similar companies [specific value prop].
Would love to share a quick case study if you're open to it.
Best,
Sales Agent"""
},
{
'day_offset': 3,
'subject': 'Re: Quick question about {company}',
'body_template': lambda p: f"""Hi {p['name']},
Following up on my previous email.
I wanted to share a quick win from a company similar to {p['company']} - they saw a 3x improvement in [metric] within 30 days.
Worth a 10-minute call to explore?
Best,
Sales Agent"""
},
{
'day_offset': 7,
'subject': 'Re: Quick question about {company}',
'body_template': lambda p: f"""{p['name']},
I know you're busy, so I'll keep this brief.
We have a limited number of Q2 onboarding slots, and I think {p['company']} would be a perfect fit.
If you're interested, I'd love to share more details. Otherwise, I'll assume the timing isn't right.
Best,
Sales Agent"""
},
{
'day_offset': 14,
'subject': 'Re: Quick question about {company}',
'body_template': lambda p: f"""{p['name']},
No worries if the timing isn't right!
I'll close this out on my end. Feel free to reach out if anything changes in the future.
Best of luck with {p['company']}!
Sales Agent"""
}
]
# Step 2: Schedule entire sequence as drafts
def schedule_sequence(inbox_id: str, prospect: Dict):
print(f"Scheduling sequence for {prospect['email']}")
now = datetime.now()
draft_ids = []
sequence_steps = get_sequence_steps()
for idx, step in enumerate(sequence_steps):
send_at = now + timedelta(days=step['day_offset'])
subject = step['subject'].format(company=prospect['company'])
body = step['body_template'](prospect)
# Create draft with scheduled send time
draft = client.drafts.create(inbox_id, {
'to': prospect['email'],
'subject': subject,
'body': body,
'send_at': send_at.isoformat(),
'client_id': f"sequence-{prospect['email']}-step-{step['day_offset']}",
'metadata': {
'sequence_id': f"sequence-{prospect['email']}",
'step_number': idx,
'prospect_email': prospect['email']
}
})
draft_ids.append(draft['id'])
print(f" Step {step['day_offset']}: Scheduled for {send_at.isoformat()}")
return {
'sequence_id': f"sequence-{prospect['email']}",
'draft_ids': draft_ids,
'prospect': prospect
}
# Step 3: Monitor for replies and cancel sequence
@app.post('/webhook')
def handle_webhook():
data = request.json
event = data.get('event')
message = data.get('message')
if event == 'message.received':
handle_reply(message)
return 'OK', 200
def handle_reply(message: Dict):
prospect_email = message['from']
print(f"✅ Reply received from {prospect_email} - canceling remaining sequence")
# Find and cancel all pending drafts for this prospect
drafts = client.drafts.list(message['inbox_id'], status='scheduled')
prospect_drafts = [
d for d in drafts
if d.get('metadata', {}).get('prospect_email') == prospect_email
and d['to'] == prospect_email
]
for draft in prospect_drafts:
client.drafts.delete(message['inbox_id'], draft['id'])
print(f" Cancelled draft: Step {draft['metadata']['step_number']}")
print(f"Sequence cancelled for {prospect_email}")
# Handle the reply
send_personalized_reply(message)
def send_personalized_reply(message: Dict):
response = "Thanks for your reply! I'll reach out with more details shortly."
client.inboxes.messages.reply(message['inbox_id'], message['id'], {
'body': response,
'client_id': f"manual-reply-{message['id']}"
})
# Step 4: Bulk sequence launch
def launch_campaign(inbox_id: str, prospects: List[Dict]):
print(f"Launching campaign for {len(prospects)} prospects")
sequences = []
for prospect in prospects:
sequence = schedule_sequence(inbox_id, prospect)
sequences.append(sequence)
# Rate limiting
import time
time.sleep(2)
print(f"\n✅ Campaign launched!")
print(f"Total sequences: {len(sequences)}")
print(f"Total scheduled emails: {len(sequences) * len(get_sequence_steps())}")
return sequences
# Step 5: Campaign analytics
def get_campaign_analytics(inbox_id: str, sequence_ids: List[str]):
stats = {
'sent': 0,
'pending': 0,
'cancelled': 0,
'replied': 0,
'open_rate': 0,
'reply_rate': 0
}
# Get all drafts
all_drafts = client.drafts.list(inbox_id)
for sequence_id in sequence_ids:
sequence_drafts = [
d for d in all_drafts
if d.get('metadata', {}).get('sequence_id') == sequence_id
]
stats['pending'] += len([d for d in sequence_drafts if d['status'] == 'scheduled'])
stats['sent'] += len([d for d in sequence_drafts if d['status'] == 'sent'])
# Get messages to check for replies
messages = client.inboxes.messages.list(inbox_id)
stats['replied'] = len(set(m['from'] for m in messages))
if stats['sent'] > 0:
stats['reply_rate'] = (stats['replied'] / stats['sent']) * 100
return stats
# Complete workflow
def run_cold_sequence_campaign():
inbox = client.inboxes.create(
username='cold-outreach',
client_id='cold-sequence-v1'
)
print(f"Campaign inbox: {inbox.address}\n")
# Example prospects
prospects = [
{'email': 'john@techcorp.com', 'name': 'John', 'company': 'TechCorp'},
{'email': 'sarah@innovate.io', 'name': 'Sarah', 'company': 'Innovate.io'},
{'email': 'mike@startup.ai', 'name': 'Mike', 'company': 'Startup.AI'}
]
# Launch campaign
sequences = launch_campaign(inbox['id'], prospects)
# Register webhook
authed_client = DaimonClient(api_key=inbox['api_key'])
authed_client.webhooks.create(
endpoint_url='https://your-agent.com/webhook',
events=['message.received'],
inbox_id=inbox['id']
)
print('\n📊 Campaign Dashboard:')
print(' Monitor replies at http://localhost:3000/analytics')
# Analytics endpoint
@app.get('/analytics')
def analytics():
stats = get_campaign_analytics(
inbox['id'],
[s['sequence_id'] for s in sequences]
)
return jsonify(stats)
app.run(port=3000)
if __name__ == '__main__':
run_cold_sequence_campaign()Key Features
- Multi-step automation: Schedule entire sequence upfront
- Smart cancellation: Stop sequence when prospect replies
- Rate limiting: Respect send limits and timing rules
- Analytics: Track campaign performance metrics
- Idempotency: Prevent duplicate sends with
clientId
Best Practices
- Start with 3-4 steps: Don't over-email prospects
- Space steps appropriately: 3-7 days between emails
- Always include opt-out: Make disengagement easy
- Personalize every step: Use prospect data in templates
- Monitor replies: Cancel sequences immediately on response