LOOMAL
Examples

Sales outreach

Draft a personalized cold email for each lead with Claude, send it, and track replies by label

A Buyer agent that takes a list of leads, drafts a personalized opener with Claude, sends each from your project's inbox, and labels every message outreach + pending-reply. A second pass watches for replies and moves them to replied, giving you a label-driven pipeline you can query at any time.

Before you start

  • A project at console.loomal.ai with Mail turned on.
  • Your Loomal API key (loid-...) and an Anthropic API key (sk-ant-...).
npm install @loomal/sdk @anthropic-ai/sdk

The agent

outreach.ts
import { Loomal } from "@loomal/sdk";
import Anthropic from "@anthropic-ai/sdk";

const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

type Lead = { email: string; name: string; company: string; note?: string };

const leads: Lead[] = [
  { email: "alice@startup.com", name: "Alice", company: "Startup Inc",
    note: "Just raised a Series A, hiring engineers fast." },
  { email: "bob@bigcorp.com", name: "Bob", company: "BigCorp",
    note: "Recently launched an AI product team." },
];

async function draft(lead: Lead): Promise<{ subject: string; text: string }> {
  const completion = await anthropic.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 400,
    system:
      "You write short, specific cold outreach emails (under 100 words). " +
      "Output JSON only: {\"subject\":\"...\",\"text\":\"...\"}. No prose.",
    messages: [
      {
        role: "user",
        content:
          `Lead: ${lead.name} at ${lead.company}.\n` +
          `Context: ${lead.note ?? "none"}.\n` +
          `Product: an email-and-payments platform for AI agents. ` +
          `Goal: get a 15-min intro call.`,
      },
    ],
  });
  const raw = completion.content
    .filter((b): b is Anthropic.TextBlock => b.type === "text")
    .map((b) => b.text)
    .join("");
  return JSON.parse(raw);
}

async function sendOutreach() {
  for (const lead of leads) {
    const { subject, text } = await draft(lead);

    const msg = await loomal.mail.send({
      to: [lead.email],
      subject,
      text,
    });

    await loomal.mail.updateLabels(msg.messageId, {
      addLabels: ["outreach", "pending-reply"],
    });

    console.log(`sent ${lead.email}: ${subject}`);
  }
}

// Second pass — move outreach threads to `replied` when the prospect writes back.
async function trackReplies() {
  const { messages } = await loomal.mail.listMessages({
    limit: 50,
    labels: "unread",
  });

  for (const m of messages) {
    const thread = await loomal.mail.getThread(m.threadId);
    const isOutreach = thread.messages.some((t) =>
      t.labels.includes("outreach"),
    );
    if (!isOutreach) continue;

    await loomal.mail.updateLabels(m.messageId, {
      addLabels: ["read", "replied"],
      removeLabels: ["unread"],
    });

    // Also clear `pending-reply` from the original outreach message.
    const original = thread.messages.find((t) => t.labels.includes("outreach"));
    if (original) {
      await loomal.mail.updateLabels(original.messageId, {
        removeLabels: ["pending-reply"],
      });
    }

    console.log(`reply from ${m.from[0]}`);
  }
}

await sendOutreach();
await trackReplies();

Run with LOOMAL_API_KEY=loid-... ANTHROPIC_API_KEY=sk-ant-... npx tsx outreach.ts.

Query the pipeline

const waiting = await loomal.mail.listMessages({ labels: "pending-reply" });
const replied = await loomal.mail.listMessages({ labels: "replied" });
const qualified = await loomal.mail.listMessages({ labels: "qualified" });

Add stages by inventing more labels — qualified, demo-scheduled, closed-won — and apply them from your CRM, a follow-up agent, or by hand in the console.

On this page