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

  1. Sourcing — agent finds prospects (your own pipeline, an enrichment API, etc.) and gets verified email addresses.
  2. Initial send — for each prospect, agent calls send_message with verified: true.
  3. Reply tracking — agent registers a webhook on message.received OR polls list_messages?direction=inbound on a schedule. Each reply is matched back to the original outbound thread.
  4. Follow-up — for prospects with no reply after N days, agent uses create_draft + send_draft to 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.