Anthropic-Powered Outreach Variant Generator

Agentic AI Systems intermediate Building with AI ~25 min read

Overview

Effective sales outreach rarely succeeds on the first attempt, and a single email framing rarely resonates with all recipients. An outreach variant generator solves this by using Claude to produce multiple stylistically distinct versions of the same core message in a single API call. Rather than calling the model once per variant — which multiplies latency and cost — the system crafts a single structured prompt that instructs Claude to emit all variants sequentially, separated by a machine-readable delimiter (===VARIANT===). The response is then split and each variant is independently validated, normalized, and stored for A/B testing or direct dispatch.

The prompt that drives this generation is not a simple one-liner. It is assembled from several structured inputs: client metadata (company name, industry, recent signals, contact name and title), reusable email body templates that encode the core value proposition, and a style guide that defines tone, sentence length, forbidden phrases, and subject line character limits. These components are concatenated in a deterministic order — system context first, then metadata, then templates, then the style guide, then the generation instruction — so that Claude has full context before it encounters the directive. The strict ordering prevents the model from inferring intent from incomplete information and producing off-brand or generically worded emails.

Once Claude returns the delimited response, a post-processing pipeline handles the practical realities of model output variability. Even with explicit formatting instructions, subjects may carry trailing whitespace, paragraphs may run together into a single block of text, or line endings may be inconsistent across variants. The normalizer addresses all of these: it splits on the delimiter, strips each variant into discrete subject and body fields, and then applies paragraph normalization — splitting on double newlines, trimming each paragraph, and rejoining — to produce consistently structured objects that downstream systems (CRM writers, queue producers, email senders) can consume without defensive parsing.

Core Concepts

Structured Prompt Assembly from Templates, Metadata & Style Guides

Rather than embedding all context inline in a single ad-hoc string, the generator composes the prompt from discrete, versioned components. Client metadata provides personalization signals (company, contact, recent activity). Email templates encode the reusable value proposition body — the part that doesn’t change per recipient. The style guide imposes brand constraints: tone adjectives (“authoritative but approachable”), subject line length caps, forbidden filler phrases (“I hope this finds you well”), and paragraph count limits. Assembling these in code — rather than in a CMS or config file — allows the generator to validate each component, substitute template variables, and version-track which template and style guide version produced each batch of variants.

Variant Delimiter Protocol (===VARIANT===)

Instructing Claude to produce multiple distinct outputs in one response requires a delimiter that is unambiguous — unlikely to appear in natural email prose and easy to split on programmatically. The string ===VARIANT=== satisfies both requirements. The system prompt tells Claude to begin each new variant with this exact delimiter on its own line, with no text before the first occurrence. The parser then calls response.split("===VARIANT===").filter(Boolean) to obtain an array of raw variant strings. The delimiter approach also makes prompt-response pairs easy to inspect during debugging: engineers can read the raw Claude output and immediately see the variant boundaries.

Subject & Body Format Enforcement

Within each variant, the system prompt requires Claude to emit exactly two labeled lines before the body: SUBJECT: <subject text> and BODY: followed by the email body on subsequent lines. This predictable structure allows the parser to extract fields with simple string operations rather than heuristics or fuzzy regex. The system prompt also states the enforcement rules explicitly: subjects must be under 60 characters, must not begin with “Re:” or “Fwd:”, and must not use all-caps words. If a variant fails these checks after parsing, it is flagged as invalid and excluded from the output batch — rather than silently forwarded with a malformed subject line.

Post-Processing & Paragraph Normalization

Email body text returned by Claude often contains subtle formatting inconsistencies: paragraphs separated by a single newline instead of two, trailing spaces on lines, or an extra blank line at the start or end. Paragraph normalization is a deterministic post-processing step that resolves these: split the body on /\n{2,}/ (two or more consecutive newlines), trim each resulting segment, filter out empty segments, and rejoin with exactly \n\n. This produces a canonical paragraph structure regardless of how the model chose to format its output. The normalized body is then safe to render in HTML email clients (where \n\n maps to <p> tags) or plain-text senders without additional munging.

Temperature & Sampling for Variant Diversity

Generating three variants at temperature 0 produces nearly identical text — the model always picks the highest-probability tokens, resulting in little stylistic variation. To achieve meaningful diversity, the generator sets temperature between 0.7 and 0.9 depending on the desired range: lower for professional B2B outreach where deviation from brand voice is risky, higher for exploratory campaigns where creative differentiation is the goal. Some implementations also use top_p nucleus sampling alongside temperature to further control the diversity-coherence tradeoff. The appropriate sampling parameters should be tuned empirically against a held-out quality rubric, not set arbitrarily.

How It Works

Prompt Assembly Pipeline

The prompt assembler begins with a static system message that describes Claude’s role (“You are an expert B2B sales copywriter…”), the output format contract (delimiter, subject/body labels), and the enforcement rules from the style guide. It then interpolates the client metadata — replacing {{contactName}}, {{companyName}}, and {{recentSignal}} placeholders in the email template strings. The assembled template block is appended after the system instructions, followed by the full style guide as a structured list of constraints. The final user message states how many variants to generate and explicitly reminds Claude to begin each one with ===VARIANT=== on its own line. The entire assembly happens synchronously in memory — there are no I/O calls once the source components are loaded — keeping assembly latency under a millisecond.

Variant Generation & Parsing

The assembled prompt is sent to the Anthropic Messages API with the chosen temperature setting. When the response arrives, the raw text content is extracted from the first content block. The parser splits on ===VARIANT===, discards any empty segments (typically the text before the first delimiter if Claude starts immediately), and processes each raw variant string in sequence. For each variant, it scans for the SUBJECT: line using a case-insensitive prefix match and extracts everything after the colon as the subject. Everything after the BODY: line becomes the raw body string. Both fields are trimmed. If either field is missing or the subject fails the length or content checks, the variant is marked invalid and logged — but processing continues for the remaining variants.

Post-Processing & Normalization

Valid variants pass through the paragraph normalizer, which applies the double-newline split-trim-rejoin sequence described above. After normalization, the generator applies one final pass: it checks for any style guide forbidden phrases (e.g., “synergy”, “circle back”, “touching base”) remaining in the body and replaces or flags them. The result is an array of strongly typed EmailVariant objects — each with a subject string, a normalized body string, a validity flag, and the index of the variant within the batch. These objects are returned to the caller, which may write them to a database, push them to a human review queue, or dispatch them directly to an email sender depending on the campaign’s confidence threshold.

Implementation Patterns

Prompt Construction

Assemble the structured prompt from metadata, templates, and style guide components, then call the Anthropic API for multi-variant generation.

// outreach/variant-generator.ts
import Anthropic from "@anthropic-ai/sdk";

interface ClientMetadata {
  contactName: string;
  contactTitle: string;
  companyName: string;
  industry: string;
  recentSignal: string; // e.g. "just closed a Series B"
}

interface EmailTemplate {
  id: string;
  bodyTemplate: string; // may contain {{placeholders}}
}

interface StyleGuide {
  tone: string;               // e.g. "confident and concise"
  subjectMaxChars: number;    // e.g. 60
  maxParagraphs: number;      // e.g. 3
  forbiddenPhrases: string[]; // e.g. ["circle back", "synergy"]
}

export interface EmailVariant {
  index: number;
  subject: string;
  body: string;
  valid: boolean;
  invalidReason?: string;
}

function interpolateTemplate(
  template: string,
  meta: ClientMetadata
): string {
  return template
    .replace(/\{\{contactName\}\}/g, meta.contactName)
    .replace(/\{\{contactTitle\}\}/g, meta.contactTitle)
    .replace(/\{\{companyName\}\}/g, meta.companyName)
    .replace(/\{\{industry\}\}/g, meta.industry)
    .replace(/\{\{recentSignal\}\}/g, meta.recentSignal);
}

function buildSystemPrompt(guide: StyleGuide): string {
  return [
    "You are an expert B2B sales copywriter specializing in cold outreach.",
    "Your task is to write multiple distinct email variants for the same prospect.",
    "",
    "OUTPUT FORMAT (mandatory):",
    "- Begin each variant with ===VARIANT=== on its own line.",
    "- Immediately follow with: SUBJECT: ",
    "- Then: BODY:",
    "- Then the email body paragraphs.",
    "- Do not include any text before the first ===VARIANT===.",
    "",
    "STYLE CONSTRAINTS:",
    `- Tone: ${guide.tone}`,
    `- Subject line: max ${guide.subjectMaxChars} characters, no ALL-CAPS words`,
    `- Body: max ${guide.maxParagraphs} paragraphs`,
    `- Forbidden phrases: ${guide.forbiddenPhrases.join(", ")}`,
    "- Do not use generic openers like 'I hope this finds you well'",
    "- Each variant must differ meaningfully in angle and framing",
  ].join("\n");
}

function buildUserMessage(
  meta: ClientMetadata,
  template: EmailTemplate,
  variantCount: number
): string {
  const interpolated = interpolateTemplate(template.bodyTemplate, meta);
  return [
    `PROSPECT CONTEXT:`,
    `Name: ${meta.contactName}, ${meta.contactTitle} at ${meta.companyName}`,
    `Industry: ${meta.industry}`,
    `Recent signal: ${meta.recentSignal}`,
    "",
    "BASE TEMPLATE (adapt and vary — do not copy verbatim):",
    interpolated,
    "",
    `Generate ${variantCount} email variants now.`,
    `Remember: start each with ===VARIANT=== on its own line.`,
  ].join("\n");
}

export async function generateVariants(
  client: Anthropic,
  meta: ClientMetadata,
  template: EmailTemplate,
  guide: StyleGuide,
  variantCount = 3,
  temperature = 0.8
): Promise<EmailVariant[]> {
  const response = await client.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 2048,
    temperature,
    system: buildSystemPrompt(guide),
    messages: [
      {
        role: "user",
        content: buildUserMessage(meta, template, variantCount),
      },
    ],
  });

  const rawText =
    response.content[0].type === "text"
      ? response.content[0].text
      : "";

  return parseVariants(rawText, guide);
}

Variant Parsing & Paragraph Normalization

Split the raw Claude response on the delimiter, extract subject and body fields from each variant, normalize paragraph formatting, and validate against style constraints.

// outreach/variant-parser.ts

const VARIANT_DELIMITER = "===VARIANT===";
const SUBJECT_PREFIX = /^subject:\s*/i;
const BODY_MARKER = /^body:\s*/i;

function normalizeParagraphs(rawBody: string): string {
  return rawBody
    .split(/\n{2,}/)          // split on 2+ consecutive newlines
    .map((p) => p.trim())     // trim each paragraph
    .filter((p) => p.length > 0) // drop empty segments
    .join("\n\n");             // rejoin with canonical double-newline
}

function checkForbiddenPhrases(
  body: string,
  forbidden: string[]
): string | null {
  const lower = body.toLowerCase();
  for (const phrase of forbidden) {
    if (lower.includes(phrase.toLowerCase())) {
      return `Contains forbidden phrase: "${phrase}"`;
    }
  }
  return null;
}

function parseRawVariant(
  raw: string,
  index: number,
  guide: StyleGuide
): EmailVariant {
  const lines = raw.split("\n");
  let subject = "";
  let bodyLines: string[] = [];
  let inBody = false;

  for (const line of lines) {
    if (!inBody && SUBJECT_PREFIX.test(line)) {
      subject = line.replace(SUBJECT_PREFIX, "").trim();
    } else if (!inBody && BODY_MARKER.test(line)) {
      inBody = true;
      // Capture any text on the same line as BODY:
      const inline = line.replace(BODY_MARKER, "").trim();
      if (inline) bodyLines.push(inline);
    } else if (inBody) {
      bodyLines.push(line);
    }
  }

  const rawBody = bodyLines.join("\n").trim();
  const body = normalizeParagraphs(rawBody);

  // Validate
  if (!subject) {
    return { index, subject, body, valid: false,
             invalidReason: "Missing SUBJECT line" };
  }
  if (subject.length > guide.subjectMaxChars) {
    return { index, subject, body, valid: false,
             invalidReason: `Subject exceeds ${guide.subjectMaxChars} chars` };
  }
  if (/\b[A-Z]{2,}\b/.test(subject)) {
    return { index, subject, body, valid: false,
             invalidReason: "Subject contains ALL-CAPS word" };
  }
  if (!body) {
    return { index, subject, body, valid: false,
             invalidReason: "Missing BODY content" };
  }

  const paragraphs = body.split("\n\n");
  if (paragraphs.length > guide.maxParagraphs) {
    return { index, subject, body, valid: false,
             invalidReason: `Body exceeds ${guide.maxParagraphs} paragraphs` };
  }

  const forbiddenHit = checkForbiddenPhrases(body, guide.forbiddenPhrases);
  if (forbiddenHit) {
    return { index, subject, body, valid: false,
             invalidReason: forbiddenHit };
  }

  return { index, subject, body, valid: true };
}

export function parseVariants(
  rawResponse: string,
  guide: StyleGuide
): EmailVariant[] {
  const segments = rawResponse
    .split(VARIANT_DELIMITER)
    .map((s) => s.trim())
    .filter((s) => s.length > 0);

  return segments.map((raw, i) => parseRawVariant(raw, i, guide));
}

// Example usage
import Anthropic from "@anthropic-ai/sdk";
import { generateVariants } from "./variant-generator";

const client = new Anthropic();

const variants = await generateVariants(
  client,
  {
    contactName: "Sarah Chen",
    contactTitle: "VP of Revenue",
    companyName: "Nexlify",
    industry: "SaaS",
    recentSignal: "just closed a $20M Series B",
  },
  {
    id: "cold-intro-v3",
    bodyTemplate:
      "Congratulations on the Series B, {{contactName}}. " +
      "We've helped several {{industry}} companies scale their " +
      "outbound motion after a funding round — " +
      "{{companyName}} seems like a strong fit.",
  },
  {
    tone: "confident and concise",
    subjectMaxChars: 60,
    maxParagraphs: 3,
    forbiddenPhrases: ["circle back", "synergy", "touch base", "leverage"],
  },
  3,   // variant count
  0.8  // temperature
);

const valid = variants.filter((v) => v.valid);
console.log(`Generated ${valid.length} valid variants of ${variants.length} total`);
valid.forEach((v) => {
  console.log(`\n--- Variant ${v.index + 1} ---`);
  console.log(`Subject: ${v.subject}`);
  console.log(`Body:\n${v.body}`);
});

Practical Applications

In the merchant dashboard platform, the outreach variant generator sits inside a workflow DAG node of type generate_email_variants. When the DAG engine dispatches this node, it passes the accumulated run context — which includes the contact’s CRM record, the assigned email template ID, and a style guide resolved from the contact’s market segment — to the generator function. The generator calls Claude once and returns an array of validated EmailVariant objects. These objects are written to the email_variants table and their IDs are pushed onto the workflow run’s context for the next node (which may be a human review queue node or a direct send node, depending on the campaign’s confidence threshold).

The system generates three variants per contact by default: a “problem-framing” angle that leads with a pain point, a “social proof” angle that leads with a customer story, and a “congratulatory” angle that leads with a recent company signal (funding, hiring surge, product launch). This three-angle strategy is baked into the email templates rather than the generator itself — the generator is intentionally angle-agnostic, and the template corpus encodes the sales team’s tested messaging playbook. The style guide is versioned in the database alongside the template, so the team can iterate on tone and constraints without touching code.

Campaigns that run through this pipeline consistently outperform manually written single-variant outreach on reply rate, partly because A/B testing across the three angles quickly surfaces which framing resonates with a given segment. The variant generator’s deterministic output format also makes this analysis tractable: every email sent by the system has a traceable variantIndex and templateId, so attribution flows cleanly back to the prompt components that produced the winning copy.

Common Pitfalls

Further Reading