Example: Cold outreach with reply tracking
Examples
Send personalized cold emails, watch for replies, and trigger follow-ups when nothing comes back after N days.
The flow
- Sourcing — agent finds prospects (your own pipeline, an enrichment API, etc.) and gets verified email addresses.
- Initial send — for each prospect, agent calls
send_messagewithverified: true. - Reply tracking — agent registers a webhook on
message.receivedOR pollslist_messages?direction=inboundon a schedule. Each reply is matched back to the original outbound thread. - Follow-up — for prospects with no reply after N days, agent uses
create_draft+send_draftto send a different-angle follow-up.
Cron-style implementation
This is a simplified version of what Scout (our reference outbound agent) does:
// cron: every 4 hours
async function outboundExecuteCycle(inboxId: string, leads: Lead[]) {
for (const lead of leads) {
const sinceLastTouch = Date.now() - (lead.lastTouchAt ?? 0);
const FOLLOW_UP_AFTER = 3 * 24 * 60 * 60 * 1000; // 3 days
// First contact
if (lead.touchCount === 0) {
const sent = await mam("POST", `/inboxes/${inboxId}/send`, {
to: lead.email,
subject: pickSubject(lead),
plainBody: composeOpener(lead),
verified: true,
});
await db.updateLead(lead.id, {
threadId: sent.threadId,
touchCount: 1,
lastTouchAt: Date.now(),
});
continue;
}
// Reply check — if they replied, the inbox cycle handles it
if (lead.replied) continue;
// Follow-up due
if (sinceLastTouch > FOLLOW_UP_AFTER && lead.touchCount < 3) {
const sent = await mam("POST", `/inboxes/${inboxId}/send`, {
to: lead.email,
subject: `Re: ${lead.lastSubject}`, // keeps the original conversation
plainBody: composeFollowUp(lead),
verified: true,
});
await db.updateLead(lead.id, {
touchCount: lead.touchCount + 1,
lastTouchAt: Date.now(),
});
}
}
}
// Separate cron: every 30 min — check replies and mark leads
async function inboxMonitorCycle(inboxId: string) {
const { messages } = await mam("GET", `/inboxes/${inboxId}/messages?direction=inbound&limit=100`);
for (const m of messages) {
if (m.isRead) continue;
const lead = await db.findLeadByEmail(extractEmail(m.from));
if (!lead) continue;
await db.updateLead(lead.id, { replied: true, lastReplyAt: Date.now() });
await mam("PATCH", `/inboxes/${inboxId}/messages/${m.id}`, { isRead: true });
}
}
Real Scout adds a lot more — bounce handling, send-window enforcement, angle rotation, plan-limit checks — but the core loop is exactly this shape.