Skip to main content

Cut RFP Response Time 40hrs to 4: Codex Automation [2026]

Β· 7 min read
Sunder Iyer
Founder, marketbetter.ai

RFPs are the bane of every sales team's existence. You get a 50-page document, 200 questions, and a deadline that's always "last week." Your best reps spend days copy-pasting from old proposals while new deals pile up.

What if you could cut RFP response time from 40 hours to 4?

That's exactly what AI coding agents like OpenAI Codex make possible. In this guide, I'll show you how to build an automated RFP response system that drafts 80% of your answers automaticallyβ€”letting your team focus on customization rather than repetitive typing.

AI RFP Response Automation Workflow

The Problem: RFPs Are a Time Black Hole​

Here's the ugly truth about RFPs:

  • Average time to complete: 20-40 hours per response
  • Win rate: 5-25% (meaning 75%+ of that effort is wasted)
  • Repetition: 60-70% of questions are repeated across RFPs
  • Cost: $3,000-$10,000 in labor per response

Your best salespeopleβ€”the ones who should be closing dealsβ€”are stuck copying answers from old Word docs. It's insane.

How AI Changes the Game​

OpenAI's Codex (powered by GPT-5.3, released February 5, 2026) is the most capable agentic coding model ever built. Combined with its mid-turn steering capability, you can build RFP automation that:

  1. Parses RFP documents automatically (PDFs, Word docs, spreadsheets)
  2. Matches questions to your answer library using semantic search
  3. Drafts responses that match your company's voice and style
  4. Flags gaps where human input is needed
  5. Formats output to match required submission formats

The key insight: You don't need 100% automation. You need to eliminate the 70% that's copy-paste.

Building Your AI RFP System​

Step 1: Create Your Answer Library​

Your answer library is the foundation. This is where Codex pulls responses from.

Structure it like this:

rfp-library/
β”œβ”€β”€ security/
β”‚ β”œβ”€β”€ soc2-compliance.md
β”‚ β”œβ”€β”€ data-encryption.md
β”‚ └── access-controls.md
β”œβ”€β”€ technical/
β”‚ β”œβ”€β”€ api-capabilities.md
β”‚ β”œβ”€β”€ integrations.md
β”‚ └── uptime-sla.md
β”œβ”€β”€ company/
β”‚ β”œβ”€β”€ about-us.md
β”‚ β”œβ”€β”€ customer-references.md
β”‚ └── team-bios.md
└── pricing/
β”œβ”€β”€ pricing-models.md
└── enterprise-terms.md

Each file contains pre-approved answers with metadata:

# SOC 2 Compliance

**Category:** Security
**Last Updated:** 2026-01-15
**Approved By:** Legal Team

## Standard Response

MarketBetter maintains SOC 2 Type II certification, audited annually by [Auditor Name]. Our most recent audit was completed in December 2025 with zero findings.

## Short Response (for checkboxes)

Yes - SOC 2 Type II certified, audited annually.

## Long Response (for detailed sections)

[Extended response with specifics...]

Step 2: Set Up Codex for RFP Processing​

Install the Codex CLI:

npm install -g @openai/codex

Create a Codex agent specifically for RFP work:

// rfp-agent.js
const { Codex } = require('@openai/codex');

const rfpAgent = new Codex({
model: 'gpt-5.3-codex',
tools: ['file_read', 'file_write', 'search'],
context: `You are an expert RFP response writer. Your job is to:
1. Parse RFP questions accurately
2. Find matching answers in the library
3. Draft responses that are professional and specific
4. Flag questions that need human review

Always maintain the company's professional tone.
Never make up technical claimsβ€”flag for review if unsure.`
});

Step 3: Parse and Match Questions​

The magic happens in semantic matching. Codex doesn't just keyword-matchβ€”it understands intent:

async function processRFP(rfpDocument) {
// Extract questions from RFP
const questions = await rfpAgent.run(`
Parse this RFP document and extract all questions.
Return as JSON array with:
- question_id
- question_text
- category (security/technical/company/pricing/other)
- complexity (simple/moderate/complex)

Document: ${rfpDocument}
`);

// Match each question to library
for (const q of questions) {
const match = await rfpAgent.run(`
Find the best matching answer in our library for:
"${q.question_text}"

Return:
- matched_file
- confidence (0-100)
- suggested_response
- needs_review (boolean)
`);

q.match = match;
}

return questions;
}

Step 4: Use Mid-Turn Steering for Complex Questions​

GPT-5.3 Codex's killer feature is mid-turn steeringβ€”you can redirect the agent while it's working. This is perfect for RFPs where context matters:

// Start processing
const session = await rfpAgent.startTask(`
Process RFP section on data security.
Draft responses for questions 15-25.
`);

// Mid-turn steering when you notice issues
await session.steer(`
Important context: This client is in healthcare.
Emphasize HIPAA compliance in all security answers.
Reference our healthcare customer case studies.
`);

// Continue processing with new context
const results = await session.complete();

This is something ChatGPT can't do. You're not starting overβ€”you're guiding the agent mid-flight.

Manual vs AI-Powered RFP Response

Real-World Results​

Here's what teams using AI RFP automation report:

MetricBefore AIAfter AIImprovement
Time per RFP40 hours6 hours85% faster
Questions auto-answered0%72%β€”
Response quality score78%84%Higher consistency
RFPs completed/month3124x throughput

The quality actually goes UP because:

  • Answers are consistent (no more contradicting yourself)
  • Responses pull from approved, accurate content
  • Reps spend time on customization, not copy-paste

The 80/20 Rule for RFP Automation​

Don't try to automate everything. Focus on:

Automate (80% of effort):

  • Standard compliance questions (SOC 2, GDPR, etc.)
  • Company background and overview
  • Technical specifications and integrations
  • Pricing structure explanations
  • Reference and case study summaries

Keep Human (20% of effort):

  • Custom pricing negotiations
  • Novel technical requirements
  • Strategic positioning against competitors
  • Executive summary and cover letter
  • Final review and submission

Integration with Your Sales Stack​

Connect your RFP automation to the tools you already use:

HubSpot Integration​

Track RFP deals and auto-populate company context:

async function enrichFromCRM(rfpDocument, dealId) {
const deal = await hubspot.deals.get(dealId);

return rfpAgent.run(`
Process this RFP with the following context:
- Company: ${deal.company.name}
- Industry: ${deal.company.industry}
- Deal Size: ${deal.amount}
- Key Requirements: ${deal.properties.requirements}

Customize responses accordingly.
Document: ${rfpDocument}
`);
}

Slack Notifications​

Alert the team when RFPs need human review:

async function notifyTeam(rfpResults) {
const needsReview = rfpResults.filter(q => q.needs_review);

if (needsReview.length > 0) {
await slack.postMessage({
channel: '#rfp-team',
text: `πŸ”” RFP "${rfpName}" needs review on ${needsReview.length} questions`,
blocks: needsReview.map(q => ({
type: 'section',
text: `*Q${q.question_id}:* ${q.question_text}\n_Confidence: ${q.confidence}%_`
}))
});
}
}

Common Pitfalls to Avoid​

1. Garbage In, Garbage Out​

Your answer library needs to be maintained. If you feed Codex outdated information, it'll confidently give wrong answers.

Fix: Schedule monthly library reviews. Track which answers get edited mostβ€”those need attention.

2. Over-Automation​

If Codex is answering questions about features you don't have, you've got a problem.

Fix: Set confidence thresholds. Below 70% confidence? Flag for human review.

3. Ignoring Voice and Tone​

Generic AI-written responses sound like... generic AI-written responses.

Fix: Include style guides in your agent's context. Train it on your best past proposals.

Cost Analysis​

Let's do the math:

Manual RFP Response:

  • 40 hours Γ— $75/hour (fully loaded cost) = $3,000 per RFP

AI-Assisted RFP Response:

  • Codex API costs: ~$50 per RFP
  • Human review time: 6 hours Γ— $75 = $450
  • Total: ~$500 per RFP

That's an 83% cost reduction per RFP. If you do 10 RFPs per quarter, that's $100,000+ saved annually.

Getting Started Today​

  1. Audit your last 5 RFPs β€” What percentage of questions repeat?
  2. Build your answer library β€” Start with your most common categories
  3. Set up Codex β€” Use the configuration above as a starting point
  4. Run a pilot β€” Test on one RFP before going all-in
  5. Iterate β€” Improve your library based on what gets edited

What's Next?​

RFP automation is just one piece of the AI-powered sales ops puzzle. Check out these related guides:


Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Ready to Transform Your Sales Operations?​

MarketBetter combines AI automation with human expertise to help GTM teams work smarter. Our platform tells your SDRs exactly who to contact, when, and what to sayβ€”so they can focus on closing instead of researching.

Book a Demo β†’

See how teams are using AI to 10x their sales productivity.

AI-Powered Sales Commission Calculator with Claude Code: Automate RevOps Complexity [2026]

Β· 8 min read
MarketBetter Team
Content Team, marketbetter.ai

The average RevOps team spends 40+ hours per month calculating commissions.

Spreadsheets break. Formulas have errors. Reps dispute their payouts. And when comp plans change mid-quarter, everything needs to be rebuilt from scratch.

Meanwhile, your sales team loses trust every time their commission is wrongβ€”even by a few dollars.

Claude Code offers a better way. Its 200K context window can hold your entire comp plan, deal data, and calculation logic in a single session. Here's how to build a commission calculator that handles real-world complexity.

Sales commission calculation flow showing deal closure, rules engine, tier calculations, and payout

The Commission Calculation Problem​

Before we solve it, let's acknowledge why it's hard:

Complexity Layers​

A "simple" commission structure often includes:

  • Base rates that vary by product, region, or segment
  • Tiers that accelerate as reps hit quota thresholds
  • Splits between AEs, SDRs, SEs, and CSMs
  • SPIFs for strategic initiatives (new product push, multi-year deals)
  • Clawbacks for churned customers
  • Overrides for managers on team deals
  • Caps and floors on certain deal types
  • Pro-ration for mid-quarter hires or territory changes

The Spreadsheet Trap​

Most teams start with Excel. By month 6, you have:

Commission_Calculator_v14_FINAL_FINAL_v2_JohnFix_ACTUALLY_FINAL.xlsx

With nested IFs that no one understands:

=IF(AND(B2="Enterprise",C2>50000,D2="New"),
IF(E2>1,G2*0.12*1.15,G2*0.12),
IF(AND(B2="Mid-Market",C2>25000),
IF(F2="SPIF",G2*0.10*1.25,G2*0.10),
G2*0.08))

When something breaksβ€”and it willβ€”good luck debugging that.

The Trust Problem​

57% of sales reps have received an incorrect commission statement (Xactly). When reps don't trust their comp, they:

  • Spend time manually verifying every deal
  • Lose motivation during payout disputes
  • Leave for companies with "cleaner" comp plans

Trust in compensation is trust in leadership.

The Claude Code Solution​

Claude Code's strengths align perfectly with commission calculation:

  1. Natural language rules - Describe your comp plan in English, not formulas
  2. 200K context - Hold the entire comp plan + all deals in one session
  3. Explainable logic - Ask "why did this deal pay $X?" and get a real answer
  4. Adaptable - Change the plan mid-quarter without rebuilding

Step 1: Document Your Comp Plan​

Instead of nested formulas, describe your plan clearly:

# Sales Commission Plan - Q1 2026

## Base Rates

### Account Executives
- New Business: 10% of first-year ACV
- Expansion: 8% of expansion ACV
- Renewal: 3% of renewal ACV

### SDRs
- Qualified Meeting: $100 per SAL
- Opportunity Created: $250 per SQL that converts to opportunity
- Deal Credit: 2% of won ACV (capped at $2,000 per deal)

### Sales Engineers
- Technical Win: $500 per closed-won where SE was primary
- Deal Credit: 3% of won ACV for complex deals (>$50K)

## Tier Accelerators

| Quota Attainment | Multiplier |
|------------------|------------|
| 0-80% | 1.0x |
| 80-100% | 1.15x |
| 100-120% | 1.30x |
| 120%+ | 1.50x |

## SPIFs (Q1)
- New product (DataSync): Additional 2% on any deal including DataSync
- Multi-year: Additional 5% for 2+ year commitments
- Competitive displacement: Additional $1,000 for wins against Competitor X

## Splits
- SDR + AE on same deal: SDR gets meeting bonus + 2% deal credit; AE gets standard rate
- AE + SE on complex deal: SE gets technical win bonus; AE gets standard rate
- Two AEs on deal: Split based on documented territory/role agreement

## Clawbacks
- Customer churns within 6 months: 100% clawback
- Customer churns 6-12 months: 50% clawback
- Downgrade within 12 months: Clawback on difference

## Caps and Floors
- No cap on accelerated earnings
- Minimum $500 payout per closed-won deal (protects small deal motivation)
- Manager override: 5% of team deals, capped at $50K/quarter

Commission tier structure showing progression from base rate through accelerator levels

Step 2: Build the Calculator​

Feed your comp plan to Claude Code:

You are a commission calculator for a B2B SaaS sales team. 

COMMISSION PLAN:
[Paste your entire comp plan document]

CALCULATION RULES:
1. Apply base rates first
2. Apply tier multipliers based on current quota attainment
3. Apply applicable SPIFs
4. Calculate splits according to deal roles
5. Check for clawbacks on previously paid commissions
6. Apply caps/floors
7. Show your work at each step

For each deal, output:
- Gross commission before modifiers
- Applicable tier multiplier
- SPIFs applied
- Split breakdown (if multiple parties)
- Final commission per person
- Reasoning for each decision

If anything is ambiguous, flag it for human review rather than guessing.

Step 3: Process Deals​

async function calculateCommission(deal, repProfile) {
const prompt = `
Calculate commission for this deal:

DEAL:
- Company: ${deal.company}
- ACV: $${deal.acv}
- Type: ${deal.type} (New/Expansion/Renewal)
- Products: ${deal.products.join(', ')}
- Contract term: ${deal.termMonths} months
- Close date: ${deal.closeDate}
- Displaced competitor: ${deal.displacedCompetitor || 'None'}

REP:
- Name: ${repProfile.name}
- Role: ${repProfile.role}
- Quota: $${repProfile.quota}
- YTD Closed: $${repProfile.ytdClosed}
- Current attainment: ${repProfile.attainment}%

OTHER PARTIES ON DEAL:
${deal.splits.map(s => `- ${s.name} (${s.role}): ${s.contribution}`).join('\n')}

Show all calculations step by step.
`;

return await claude.calculate(prompt);
}

Step 4: Handle Edge Cases​

Claude Code shines on the weird stuff:

EDGE CASE HANDLING:

Deal: Multi-year with mid-contract expansion
- Customer signed 3-year deal in January ($100K ACV)
- Expanded in March (+$25K ACV, same rep)

Question: How do we calculate the expansion commission?

REASONING:
1. Original deal: 3-year, so multi-year SPIF applies (10% + 5% = 15%)
2. Expansion: Same contract term, inherits multi-year status
3. Expansion rate: 8% base + 5% multi-year = 13%
4. Rep attainment: Now at 125% with original deal
5. Tier multiplier: 1.50x applies to expansion

CALCULATION:
$25,000 Γ— 13% Γ— 1.50x = $4,875

FLAG: Verify if expansion should inherit multi-year SPIF
(policy may differ by team).

Practical Workflows​

Monthly Commission Run​

async function runMonthlyCommissions(month, year) {
// Get all closed deals
const deals = await crm.getClosedDeals({ month, year });

// Get all rep profiles
const reps = await getRepProfiles();

// Calculate each deal
const commissions = [];
for (const deal of deals) {
const calc = await calculateCommission(deal, reps[deal.ownerId]);
commissions.push({
deal: deal,
calculation: calc,
breakdown: parseBreakdown(calc)
});
}

// Check for clawbacks
const clawbacks = await checkClawbacks(month, year);

// Generate report
return {
totalPayout: sum(commissions.map(c => c.breakdown.finalAmount)),
byRep: groupByRep(commissions),
clawbacks: clawbacks,
flaggedForReview: commissions.filter(c => c.breakdown.hasFlags)
};
}

Rep Self-Service​

Let reps verify their own commissions:

async function repCommissionQuery(repId, question) {
const repProfile = await getRepProfile(repId);
const recentDeals = await getRecentDeals(repId);
const commissionHistory = await getCommissionHistory(repId);

const prompt = `
A sales rep is asking about their commission.

REP PROFILE:
${JSON.stringify(repProfile)}

RECENT DEALS:
${JSON.stringify(recentDeals)}

COMMISSION HISTORY (Last 3 months):
${JSON.stringify(commissionHistory)}

QUESTION:
${question}

Answer clearly, show relevant calculations, reference
specific deals and comp plan provisions.
`;

return await claude.answer(prompt);
}

Example queries:

  • "Why did the Acme deal only pay $2,400?"
  • "What's my commission if I close the pending BigCorp deal?"
  • "Am I on track for the 120% accelerator this quarter?"

Plan Modeling​

Model comp plan changes before implementing:

async function modelPlanChange(proposedChange, historicalDeals) {
const prompt = `
We're considering this comp plan change:
"${proposedChange}"

Analyze the impact using last quarter's deals:
${JSON.stringify(historicalDeals)}

Show:
1. Total payout difference (old plan vs new)
2. Impact per rep
3. Which deal types are affected most
4. Potential unintended consequences
5. Recommendation
`;

return await claude.analyze(prompt);
}

Example: "What if we increase the new business rate from 10% to 12% but cap it at 110% attainment?"

Advanced Patterns​

Multi-Currency Handling​

CURRENCY RULES:
- All commissions paid in USD
- Deals closed in other currencies: use exchange rate at close date
- Exchange rate source: Company treasury rates (monthly)

EXAMPLE:
Deal closed in EUR: €50,000
Close date: February 15, 2026
Treasury rate (Feb 2026): 1.08 EUR/USD
USD equivalent: $54,000

Commission calculated on $54,000 USD value.

Territory Changes Mid-Quarter​

TERRITORY CHANGE HANDLING:

Rep A had Territory X from Jan 1 - Feb 15
Rep B took over Territory X on Feb 16

Deal in Territory X closed March 10

RULE:
- If deal was in pipeline before transfer:
Original rep (A) gets full commission
- If deal entered pipeline after transfer:
New rep (B) gets full commission
- If deal was in active stage during transfer:
Split 50/50 or per documented agreement

This deal entered pipeline Jan 25, so Rep A gets full commission.

Clawback Automation​

async function processClawbacks() {
// Find churned customers
const churns = await crm.getChurns({ lookbackMonths: 12 });

for (const churn of churns) {
// Find original commission
const originalCommission = await findCommission(churn.dealId);

// Calculate clawback
const monthsSinceDeal = monthsBetween(
originalCommission.closeDate,
churn.churnDate
);

let clawbackRate;
if (monthsSinceDeal <= 6) clawbackRate = 1.0;
else if (monthsSinceDeal <= 12) clawbackRate = 0.5;
else clawbackRate = 0;

if (clawbackRate > 0) {
await createClawback({
repId: originalCommission.repId,
dealId: churn.dealId,
amount: originalCommission.amount * clawbackRate,
reason: `Customer churned at ${monthsSinceDeal} months`
});
}
}
}

Results to Expect​

Teams using AI-powered commission calculation typically see:

MetricBeforeAfterImpact
Calculation time40+ hrs/month2-4 hrs/month90% reduction
Error rate8-12%<1%90%+ fewer disputes
Rep trust score62%91%47% improvement
Time to resolve disputes3-5 daysSame day80% faster
Plan change implementation2-3 weeks1-2 days85% faster

The biggest win: reps stop wasting mental energy worrying about comp. That energy goes back into selling.

Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Getting Started​

  1. Document your current plan - Write it in plain English, not spreadsheet formulas

  2. Identify edge cases - What causes disputes today? Document the rules

  3. Start with one team - Run parallel calculations for one month

  4. Build rep self-service - Let them query their own commissions

  5. Add clawback automation - Remove manual tracking of churns


Ready to automate your commission complexity? Book a demo to see how MarketBetter helps RevOps teams operate at scale.

Related reading:

AI Sales Email Generator: Build Your Own Template Engine With Claude Code [2026]

Β· 6 min read
MarketBetter Team
Content Team, marketbetter.ai

Your SDRs spend 3+ hours daily writing emails. Most of those emails get ignored because they're either generic templates or poorly personalized. Here's how to fix that with Claude Code.

AI email template generator workflow

The Email Personalization Problem​

Every sales leader faces the same dilemma:

Option A: Generic templates. Fast to send, terrible results. Your prospects have seen "Hope this email finds you well" a thousand times.

Option B: Truly personalized emails. Great results, impossible to scale. Nobody has time to research every prospect and write custom copy.

Option C: AI-powered personalization. The best of both worldsβ€”if you set it up right.

Most teams try Option C with basic ChatGPT prompts and get mediocre results. The emails sound robotic, miss key details, and still require heavy editing.

Claude Code changes this equation.

Why Claude Code for Email Generation?​

Claude's 200K context window is the game-changer here. You can feed it:

  • Your entire email template library
  • Your company's voice guidelines
  • Industry-specific talking points
  • Competitor battle cards
  • Recent company news about the prospect
  • The prospect's LinkedIn activity

All at once. No truncating. No "summarize this first."

The result? Emails that sound like they were written by your best rep after 30 minutes of researchβ€”generated in 30 seconds.

Building Your Email Template Generator​

Here's the architecture for a production-ready email generator:

Step 1: Create Your Base Templates​

Start with 5-7 proven email structures:

templates/
β”œβ”€β”€ cold_outreach_problem.md
β”œβ”€β”€ cold_outreach_social_proof.md
β”œβ”€β”€ trigger_event_response.md
β”œβ”€β”€ competitor_displacement.md
β”œβ”€β”€ referral_followup.md
β”œβ”€β”€ webinar_followup.md
└── content_engagement.md

Each template should have:

  • Clear structure with variable placeholders
  • Multiple tone variations (formal, casual, direct)
  • Industry-specific versions when needed

Step 2: Build Your Context Library​

This is where most teams fail. They give Claude a prompt and expect magic. Instead, build a comprehensive context system:

Company voice guide:

  • Preferred phrases and words to use
  • Words and phrases to avoid
  • Tone guidelines by industry
  • Signature style rules

Industry insights:

  • Pain points by vertical
  • Regulatory concerns by industry
  • Budget cycle timing
  • Decision-maker titles

Competitive intel:

  • Positioning against each competitor
  • Migration success stories
  • Feature comparison talking points

Step 3: The Generation Prompt​

Here's a prompt structure that consistently produces high-quality emails:

You are an expert B2B sales copywriter for [Company].

CONTEXT:
[Insert full voice guide]
[Insert relevant industry insights]
[Insert competitive positioning if relevant]

PROSPECT INFORMATION:
Company: \{company_name\}
Industry: \{industry\}
Role: {prospect_role}
Recent News: {company_news}
LinkedIn Activity: {recent_posts}
Tech Stack: {known_tools}
Trigger Event: {trigger_if_any}

TEMPLATE TO USE:
{selected_template}

INSTRUCTIONS:
1. Personalize the template using specific details from the prospect info
2. Reference their recent news or LinkedIn activity naturally
3. Connect their industry pain points to our solution
4. Keep the email under 150 words
5. Use a {formal/casual/direct} tone
6. End with a clear, low-friction CTA

Generate the email:

Step 4: Quality Control Layer​

Don't ship emails directly to prospects. Add a scoring system:

const qualityChecks = [
{ name: 'length', check: email => email.length < 800 },
{ name: 'personalization', check: email => containsProspectDetails(email) },
{ name: 'cta_present', check: email => hasCallToAction(email) },
{ name: 'no_forbidden_words', check: email => !containsForbidden(email) },
{ name: 'sentiment_positive', check: email => scoreSentiment(email) > 0.5 }
];

Emails that fail any check go to a review queue. Emails that pass all checks can be sent automatically (or queued for approval, depending on your comfort level).

Real Results: Before and After​

Manual vs AI email comparison

Before (Manual Process):

  • 3 hours/day on email writing
  • 50 emails sent
  • 12% open rate
  • 2% response rate

After (Claude Code Generator):

  • 30 minutes/day on review and approval
  • 200 emails sent
  • 31% open rate
  • 8% response rate

The open rate jump comes from better subject lines. Claude can generate and A/B test subject line variations at scale.

The response rate jump comes from genuine personalization. When you reference a prospect's actual LinkedIn post or recent company news, they notice.

Advanced: Multi-Language Support​

If you sell internationally, Claude Code handles multilingual email generation natively:

Additional instruction: Generate this email in {language}.
Maintain cultural communication norms for {country}.

Our team uses this for EMEA outreach. The emails read naturally in German, French, and Spanishβ€”not like machine translations.

Integration with Your Stack​

The email generator becomes powerful when connected to your existing tools:

CRM Integration:

  • Pull prospect data directly from HubSpot/Salesforce
  • Push generated emails back as drafts
  • Track which templates perform best

Enrichment Integration:

  • Auto-fetch LinkedIn data via Proxycurl or similar
  • Pull company news from Crunchbase or news APIs
  • Get tech stack data from BuiltWith or similar

Sending Integration:

  • Queue emails in Outreach, Salesloft, or Apollo
  • Schedule based on timezone and optimal send times
  • Handle replies and out-of-office detection

Common Pitfalls to Avoid​

Pitfall 1: Over-personalization Don't reference everything you know about a prospect. One or two specific details is enough. More feels creepy.

Pitfall 2: Inconsistent voice Review your first 100 generated emails manually. Train the model on corrections. Your voice guide will evolve.

Pitfall 3: Ignoring negative signals If a prospect's LinkedIn shows they're job hunting or their company just had layoffs, don't send a sales email. Build filters for these cases.

Pitfall 4: Template fatigue Rotate templates and refresh them monthly. Recipients and spam filters both notice patterns.

Getting Started Today​

You don't need to build the full system to start seeing results:

  1. Day 1: Create your voice guide and 3 base templates
  2. Day 2: Set up Claude Code with your context
  3. Day 3: Generate 50 emails and manually review all of them
  4. Week 1: Refine prompts based on what you corrected
  5. Week 2: Increase volume, decrease manual review for high-scoring emails

Within a month, you'll have a system that generates better emails than your reps write manuallyβ€”in a fraction of the time.

Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

The MarketBetter Approach​

We've built email personalization directly into MarketBetter's AI SDR platform. Your playbook tells SDRs exactly what to do next, and when it's time to email, the email is already drafted with full personalization.

No prompts to write. No templates to manage. Just review and send.

Ready to see personalized email generation in action? Book a demo and we'll show you how MarketBetter handles email personalization at scale.


Related reading:

AI Sales Enablement Content Generation with Codex [2026]

Β· 9 min read
Sunder Iyer
Founder, marketbetter.ai

Your sales enablement team is drowning. New product launches, competitive updates, pricing changes, messaging pivotsβ€”and somehow they're supposed to keep 50 reps trained on all of it.

Here's the problem: Sales enablement doesn't scale with headcount. You can't hire your way out of content debt.

But AI can help. In this guide, I'll show you how to use OpenAI Codex (GPT-5.3) to automatically generate training materials, battle cards, and playbooksβ€”so your enablement team can focus on strategy, not formatting.

AI Sales Enablement Content Automation

The Sales Enablement Content Crisis​

Let's be honest about the state of sales enablement:

  • Battle cards are 6 months out of date
  • Training materials reference features that no longer exist
  • Playbooks assume a sales motion you abandoned last quarter
  • Competitive intel is scattered across 47 Slack messages and 12 Google Docs

Your reps are flying blind because enablement can't keep up.

Average sales enablement team: 2 people supporting 50+ reps. Content pieces to maintain: 200+ documents. Update frequency needed: Weekly. Update frequency actual: "When we get to it."

Why Codex for Enablement​

OpenAI's Codex (GPT-5.3, released February 5, 2026) is the most capable agentic model for this work because:

  1. Code generation β€” Create interactive training modules, not just docs
  2. Multi-file operations β€” Update 50 battle cards in one run
  3. Mid-turn steering β€” Redirect generation based on feedback
  4. Tool use β€” Pull from CRM, product docs, Slack, and more

The killer feature: Codex can read your actual product, pull competitive intel, and synthesize training materials that are always current.

Building Your AI Enablement Engine​

Step 1: Define Your Content Types​

First, map what you need to generate:

const contentTypes = {
battleCards: {
structure: [
'competitor_overview',
'strengths',
'weaknesses',
'our_differentiators',
'landmines_to_plant',
'objection_handlers',
'customer_wins'
],
updateTrigger: ['competitor_news', 'product_update', 'win_loss_feedback'],
format: 'notion_page'
},

playbooks: {
structure: [
'situation_overview',
'target_persona',
'key_messages',
'discovery_questions',
'demo_flow',
'objection_responses',
'next_steps'
],
updateTrigger: ['messaging_change', 'new_feature', 'icp_shift'],
format: 'google_doc'
},

trainingModules: {
structure: [
'learning_objectives',
'key_concepts',
'real_examples',
'practice_scenarios',
'quiz_questions',
'certification_criteria'
],
updateTrigger: ['product_launch', 'process_change', 'quarterly'],
format: 'interactive_html'
},

oneSheets: {
structure: [
'headline_value_prop',
'key_benefits',
'proof_points',
'call_to_action'
],
updateTrigger: ['campaign_launch', 'messaging_refresh'],
format: 'figma_template'
}
};

Step 2: Set Up Codex with Context​

Give Codex access to your source truth:

// enablement-agent.js
const { Codex } = require('@openai/codex');

const enablementAgent = new Codex({
model: 'gpt-5.3-codex',
tools: ['file_read', 'file_write', 'web_search', 'api_call'],
context: `You are a Sales Enablement Content Generator.

Your job is to create and update sales training materials that help reps win more deals.

SOURCES OF TRUTH:
- Product documentation: /docs/product/
- Competitive intel: /docs/competitive/
- Win/loss reports: /crm/win-loss/
- Customer stories: /docs/customers/
- Pricing: /docs/pricing/

CONTENT PRINCIPLES:
1. Be specific and actionableβ€”no generic fluff
2. Include real customer examples when available
3. Anticipate rep questions and objections
4. Make content scannable (bullets > paragraphs)
5. Include talk tracks reps can use verbatim

VOICE:
- Confident but not arrogant
- Data-driven with proof points
- Conversational, not corporate`
});

Step 3: Battle Card Generator​

Generate battle cards that actually get used:

async function generateBattleCard(competitor) {
// Gather fresh intel
const competitorData = await gatherCompetitorIntel(competitor);
const recentWins = await getWinsAgainst(competitor);
const recentLosses = await getLossesTo(competitor);

const battleCard = await enablementAgent.run(`
Generate a sales battle card for ${competitor}.

COMPETITOR DATA:
${JSON.stringify(competitorData, null, 2)}

RECENT WINS AGAINST THEM (last 90 days):
${JSON.stringify(recentWins, null, 2)}

RECENT LOSSES TO THEM (last 90 days):
${JSON.stringify(recentLosses, null, 2)}

Generate the battle card with these sections:

## Quick Stats
- Founded, HQ, funding, employee count
- Key customers
- Pricing (if known)

## Where They Win
- Their genuine strengths (be honest)
- Types of deals they tend to win

## Where We Win
- Our differentiators vs them
- Types of deals we tend to win

## Landmines to Plant
- Questions that expose their weaknesses
- Features to emphasize in demos

## Objection Handlers
For each common objection, provide:
- Objection
- Response framework
- Proof point to cite

## Talk Track
A 30-second positioning statement when ${competitor} comes up

## Customer Wins
Specific stories of customers who chose us over them
`);

return battleCard;
}

Step 4: Training Module Generator​

Create interactive training that reps actually complete:

async function generateTrainingModule(topic, config) {
const module = await enablementAgent.run(`
Create an interactive sales training module on: ${topic}

Target audience: ${config.audience}
Estimated completion time: ${config.duration} minutes
Prerequisites: ${config.prerequisites.join(', ')}

Generate as HTML with embedded interactivity:

1. LEARNING OBJECTIVES
- What will they be able to do after this module?

2. CORE CONTENT
- Break into 3-5 digestible sections
- Include real examples from our customer base
- Add "Try This" interactive elements

3. SCENARIO PRACTICE
- 3 realistic scenarios they might encounter
- Multiple choice responses with feedback
- Explain why correct answers work

4. TALK TRACK BUILDER
- Interactive section where they build their own pitch
- Template with fill-in-the-blank sections
- Example completed version

5. QUIZ
- 5 questions testing key concepts
- Mix of multiple choice and short answer
- Passing score: 80%

6. RESOURCES
- Links to related content
- Cheat sheet for quick reference
- Who to ask for help

Output as a complete, styled HTML document that can be hosted in our LMS.
`);

return module;
}

AI-Powered Sales Training Generator

Using Mid-Turn Steering​

Codex's mid-turn steering lets you refine generation in real-time:

async function createPlaybookWithFeedback(playbookType) {
// Start generation
const session = await enablementAgent.startTask(`
Create a sales playbook for: ${playbookType}
Start with discovery questions.
`);

// Review first section
const discoverySection = await session.checkpoint();
console.log('Discovery section:', discoverySection);

// Steer based on review
await session.steer(`
Good start, but:
- Add more questions about budget timing
- Include a question about competitive evaluation
- Make questions more open-ended
`);

// Continue with refinements
await session.continue();

// Final output
return session.complete();
}

Automation: Always-Current Content​

Set up automated updates so content never goes stale:

// content-updater.js

// Trigger: Competitor releases new feature
async function onCompetitorUpdate(competitor, update) {
const affectedContent = await findContentMentioning(competitor);

for (const content of affectedContent) {
const updated = await enablementAgent.run(`
Update this content to reflect new competitor information:

ORIGINAL CONTENT:
${content.body}

NEW INFORMATION:
${competitor} just ${update.summary}

Details: ${update.details}

Update the content to:
1. Reflect any new competitive threats
2. Update our positioning if needed
3. Add new objection handlers if relevant
4. Keep the same format and structure

Return only the updated content.
`);

await saveContent(content.id, updated);
await notifyEnablement(`Updated ${content.title} based on ${competitor} news`);
}
}

// Trigger: Product team ships feature
async function onProductUpdate(feature) {
// Update relevant playbooks
const playbooks = await findPlaybooksForFeature(feature.area);

for (const playbook of playbooks) {
await enablementAgent.run(`
Add information about our new feature to this playbook:

NEW FEATURE: ${feature.name}
DESCRIPTION: ${feature.description}
KEY BENEFITS: ${feature.benefits.join(', ')}
TARGET PERSONA: ${feature.persona}

CURRENT PLAYBOOK:
${playbook.body}

Updates needed:
1. Add feature to "What We Offer" section
2. Create demo talking points
3. Add discovery questions that surface need for this feature
4. Update objection handlers if this addresses known concerns
`);
}

// Create training module for new feature
await generateTrainingModule(`New Feature: ${feature.name}`, {
audience: 'all-sales',
duration: 15,
prerequisites: ['product-fundamentals']
});
}

Content Quality Checklist​

Have Codex self-check every piece of content:

async function validateContent(content, type) {
const validation = await enablementAgent.run(`
Validate this ${type} against our quality standards:

CONTENT:
${content}

CHECK FOR:
1. βœ“ Specificity β€” Are claims backed by data/examples?
2. βœ“ Accuracy β€” Do features/pricing match current product?
3. βœ“ Actionability β€” Can a rep use this immediately?
4. βœ“ Scannability β€” Is it easy to skim?
5. βœ“ Completeness β€” Are all required sections present?
6. βœ“ Freshness β€” Are examples and data current?
7. βœ“ Voice β€” Does it match our brand tone?

Return:
{
"score": 0-100,
"issues": ["list of specific problems"],
"suggestions": ["how to fix each issue"],
"approved": true/false
}
`);

return JSON.parse(validation);
}

Integration with Enablement Stack​

Seismic/Highspot Integration​

Push generated content to your enablement platform:

async function publishToSeismic(content) {
const formatted = await enablementAgent.run(`
Format this content for Seismic upload:
${content}

- Add proper metadata tags
- Set appropriate permissions (sales-team)
- Include search keywords
- Add related content suggestions
`);

await seismic.content.create({
title: formatted.title,
body: formatted.body,
tags: formatted.tags,
permissions: ['sales-team'],
keywords: formatted.keywords
});
}

Slack Integration for Requests​

Let reps request content updates directly:

// Slack command: /enablement-request
app.command('/enablement-request', async ({ command, ack, respond }) => {
await ack();

const request = command.text;

// Quick triage
const triage = await enablementAgent.run(`
Triage this enablement request:
"${request}"

Determine:
1. Content type needed (battle card, playbook, training, etc.)
2. Priority (urgent, normal, nice-to-have)
3. Can this be auto-generated or needs human input?
4. Estimated time to create
`);

if (triage.canAutoGenerate) {
const content = await generateContent(triage.contentType, request);
await respond(`βœ… Created! Here's your ${triage.contentType}: ${content.url}`);
} else {
await createEnablementTicket(request, triage);
await respond(`πŸ“‹ Request logged. Enablement team will review.`);
}
});

Measuring Impact​

Track whether AI-generated content actually works:

MetricBefore AIAfter AIChange
Battle cards accessed/month45312+593%
Content freshness (avg age)4.2 months2.1 weeks8x fresher
Rep content requests28/month8/month-71%
Time to create training40 hours4 hours-90%
Enablement NPS3471+37 pts

Reps use content more when it's actually helpful and current. Shocking, I know.

Getting Started​

  1. Audit current content β€” What's stale? What's missing?
  2. Prioritize β€” Start with battle cards (highest impact, clearest structure)
  3. Set up sources β€” Connect Codex to product docs, CRM, competitive intel
  4. Generate first batch β€” Create 3-5 battle cards manually to test quality
  5. Automate triggers β€” Set up auto-updates on competitor/product changes
  6. Measure and iterate β€” Track usage and gather rep feedback

Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Scale Enablement Without Scaling Headcount​

MarketBetter helps GTM teams work smarter with AI-powered automation. From battle cards to training modules, we help you keep reps equipped with what they need to win.

Book a Demo β†’

See how AI can transform your sales enablement.

AI Sales Meeting Transcription: Build a Free Gong Alternative with OpenClaw [2026]

Β· 9 min read
MarketBetter Team
Content Team, marketbetter.ai

Gong costs $1,200-1,600 per user per year. For a 10-person sales team, that's $12,000-16,000 annuallyβ€”just for call recording and basic insights.

What if you could get 80% of the value for $50/month?

This guide shows you how to build a sales meeting transcription and analysis system using:

  • OpenClaw for orchestration (free)
  • Whisper for transcription (free or cheap API)
  • Claude for analysis ($0.01-0.05 per call)

The result: automatic meeting summaries, action items, deal intelligence, and CRM syncβ€”without the enterprise price tag.

AI transcribing sales call to meeting notes with CRM sync

What Enterprise Tools Like Gong Actually Do​

Before we build a replacement, let's understand what we're replacing:

Gong's Core Features​

  1. Call recording β€” Records Zoom/Teams/phone calls
  2. Transcription β€” Speech-to-text for the full conversation
  3. Topic detection β€” Identifies when pricing, competition, etc. come up
  4. Action items β€” Extracts next steps from calls
  5. Deal intelligence β€” Tracks deal progression across calls
  6. Coaching insights β€” Talk ratio, filler words, etc.
  7. CRM sync β€” Pushes notes to Salesforce/HubSpot

What Actually Matters​

Here's the dirty secret: most teams use like 20% of Gong's features. The stuff that actually moves deals:

  • Searchable transcripts
  • Auto-generated summaries
  • Action items pushed to CRM
  • Basic deal tracking

We can build all of that.

Cost comparison: Enterprise tools vs free open source alternative

The Architecture​

Here's our stack:

[Zoom/Teams Recording]
↓
[Cloud Storage (S3/GCS)]
↓
[Whisper Transcription]
↓
[Claude Analysis]
↓
β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
↓ ↓
[HubSpot] [Slack Alert]

OpenClaw orchestrates the whole flowβ€”watching for new recordings, processing them, and routing the outputs.

Architecture showing Zoom to Whisper to Claude to CRM workflow

Prerequisites​

  • OpenClaw installed (setup guide)
  • Zoom Business plan (for cloud recording) or local recording workflow
  • HubSpot (or your CRM)
  • Anthropic API key
  • Optional: OpenAI API for Whisper (or run locally)

Step 1: Set Up Recording Capture​

Option A: Zoom Cloud Recording Webhook​

Zoom can automatically upload recordings. Set up a webhook to trigger processing:

// webhooks/zoom.js
const express = require('express');
const { OpenClaw } = require('openclaw');

const app = express();
const openclaw = new OpenClaw();

app.post('/webhooks/zoom', async (req, res) => {
const { event, payload } = req.body;

if (event === 'recording.completed') {
const { download_url, meeting_id, topic, start_time } = payload.object;

// Trigger the transcription pipeline
await openclaw.trigger('meeting-processor', {
recordingUrl: download_url,
meetingId: meeting_id,
title: topic,
timestamp: start_time,
source: 'zoom'
});
}

res.sendStatus(200);
});

app.listen(3000);

Option B: Local Recording Watch Folder​

If you record locally, watch a folder for new files:

// watchers/local-recordings.js
const chokidar = require('chokidar');
const { OpenClaw } = require('openclaw');

const openclaw = new OpenClaw();
const RECORDINGS_DIR = '/path/to/recordings';

const watcher = chokidar.watch(RECORDINGS_DIR, {
ignored: /(^|[\/\\])\../,
persistent: true,
awaitWriteFinish: true
});

watcher.on('add', async (filePath) => {
if (filePath.endsWith('.mp4') || filePath.endsWith('.m4a')) {
console.log(`πŸ“Ή New recording detected: ${filePath}`);

await openclaw.trigger('meeting-processor', {
filePath,
title: path.basename(filePath, path.extname(filePath)),
timestamp: new Date().toISOString(),
source: 'local'
});
}
});

console.log(`πŸ‘€ Watching ${RECORDINGS_DIR} for new recordings...`);

Step 2: Build the Transcription Pipeline​

Using OpenAI Whisper API​

// lib/transcribe.js
const fs = require('fs');
const FormData = require('form-data');

async function transcribeAudio(audioPath) {
const form = new FormData();
form.append('file', fs.createReadStream(audioPath));
form.append('model', 'whisper-1');
form.append('response_format', 'verbose_json');
form.append('language', 'en');

const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
...form.getHeaders()
},
body: form
});

const result = await response.json();

return {
text: result.text,
segments: result.segments, // Includes timestamps
duration: result.duration
};
}

module.exports = { transcribeAudio };

Using Local Whisper (Free, But Slower)​

# Install Whisper locally
pip install openai-whisper

# Transcribe
whisper recording.mp4 --model medium --output_format json
// lib/transcribe-local.js
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);

async function transcribeLocal(audioPath) {
const outputPath = audioPath.replace(/\.[^/.]+$/, '');

await execAsync(
`whisper "${audioPath}" --model medium --output_format json --output_dir /tmp`
);

const transcript = require(`${outputPath}.json`);

return {
text: transcript.text,
segments: transcript.segments,
duration: transcript.segments[transcript.segments.length - 1]?.end || 0
};
}

module.exports = { transcribeLocal };

Step 3: Claude Analysis Agent​

Now the magicβ€”Claude reads the transcript and extracts insights:

# agents/meeting-analyzer.yaml
name: MeetingAnalyzer
description: Analyzes sales call transcripts and extracts insights

model: claude-sonnet-4-20250514
temperature: 0.2

system_prompt: |
You are a sales call analyst. Given a meeting transcript, extract:

## OUTPUT FORMAT

### SUMMARY
A 2-3 sentence executive summary of the call.

### KEY DISCUSSION POINTS
Bullet points of main topics covered.

### CUSTOMER PAIN POINTS
Specific problems or challenges mentioned by the prospect.

### BUYING SIGNALS
Any positive indicators (timeline mentioned, budget discussed, stakeholders identified).

### OBJECTIONS/CONCERNS
Any hesitations or pushback from the prospect.

### COMPETITION MENTIONED
Any competitors discussed and context.

### ACTION ITEMS
Specific next steps with owners (format: "[ ] Owner: Action by Date").

### DEAL STAGE RECOMMENDATION
Based on this call, recommended deal stage:
- Discovery
- Qualification
- Demo/Evaluation
- Negotiation
- Closed Won/Lost

### FOLLOW-UP PRIORITY
High / Medium / Low with reasoning.

### COACHING NOTES
Quick notes for the rep (what went well, areas to improve).

## RULES
- Be specificβ€”quote the transcript when relevant
- Don't make up information not in the transcript
- If something is unclear, note it as "unclear from transcript"
- Action items must be actionable and specific
// lib/analyze.js
const Anthropic = require('@anthropic-ai/sdk');

const client = new Anthropic();

async function analyzeTranscript(transcript, metadata) {
const response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4000,
messages: [{
role: 'user',
content: `Analyze this sales call transcript.

Meeting: ${metadata.title}
Date: ${metadata.timestamp}
Duration: ${metadata.duration} minutes
Attendees: ${metadata.attendees?.join(', ') || 'Unknown'}

---TRANSCRIPT---
${transcript}
---END TRANSCRIPT---`
}]
});

return parseAnalysis(response.content[0].text);
}

function parseAnalysis(rawAnalysis) {
// Parse the structured output into a JSON object
const sections = {};

const sectionPatterns = [
{ key: 'summary', pattern: /### SUMMARY\n([\s\S]*?)(?=###|$)/ },
{ key: 'keyPoints', pattern: /### KEY DISCUSSION POINTS\n([\s\S]*?)(?=###|$)/ },
{ key: 'painPoints', pattern: /### CUSTOMER PAIN POINTS\n([\s\S]*?)(?=###|$)/ },
{ key: 'buyingSignals', pattern: /### BUYING SIGNALS\n([\s\S]*?)(?=###|$)/ },
{ key: 'objections', pattern: /### OBJECTIONS\/CONCERNS\n([\s\S]*?)(?=###|$)/ },
{ key: 'competition', pattern: /### COMPETITION MENTIONED\n([\s\S]*?)(?=###|$)/ },
{ key: 'actionItems', pattern: /### ACTION ITEMS\n([\s\S]*?)(?=###|$)/ },
{ key: 'dealStage', pattern: /### DEAL STAGE RECOMMENDATION\n([\s\S]*?)(?=###|$)/ },
{ key: 'priority', pattern: /### FOLLOW-UP PRIORITY\n([\s\S]*?)(?=###|$)/ },
{ key: 'coaching', pattern: /### COACHING NOTES\n([\s\S]*?)(?=###|$)/ }
];

sectionPatterns.forEach(({ key, pattern }) => {
const match = rawAnalysis.match(pattern);
sections[key] = match ? match[1].trim() : '';
});

return sections;
}

module.exports = { analyzeTranscript };

Step 4: CRM Integration​

Push the analysis to HubSpot:

// lib/crm-sync.js
const HubSpot = require('@hubspot/api-client');

const hubspot = new HubSpot.Client({ accessToken: process.env.HUBSPOT_TOKEN });

async function syncToHubSpot(dealId, analysis, recordingUrl) {
// Create engagement (call note)
const noteBody = `
## Meeting Summary
${analysis.summary}

## Key Points
${analysis.keyPoints}

## Pain Points Identified
${analysis.painPoints}

## Buying Signals
${analysis.buyingSignals}

## Action Items
${analysis.actionItems}

---
πŸŽ₯ [View Recording](${recordingUrl})
πŸ“Š Priority: ${analysis.priority}
🎯 Suggested Stage: ${analysis.dealStage}
`;

// Create note
await hubspot.crm.objects.notes.basicApi.create({
properties: {
hs_note_body: noteBody,
hs_timestamp: Date.now()
},
associations: [{
to: { id: dealId },
types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 214 }]
}]
});

// Update deal stage if recommended
const stageMap = {
'Discovery': 'appointmentscheduled',
'Qualification': 'qualifiedtobuy',
'Demo/Evaluation': 'presentationscheduled',
'Negotiation': 'contractsent'
};

const newStage = stageMap[analysis.dealStage.trim()];
if (newStage) {
await hubspot.crm.deals.basicApi.update(dealId, {
properties: { dealstage: newStage }
});
}

// Create tasks for action items
const actionItems = parseActionItems(analysis.actionItems);
for (const item of actionItems) {
await hubspot.crm.objects.tasks.basicApi.create({
properties: {
hs_task_body: item.task,
hs_task_subject: `Follow-up: ${item.task.substring(0, 50)}...`,
hs_task_status: 'NOT_STARTED',
hs_task_priority: analysis.priority.includes('High') ? 'HIGH' : 'MEDIUM',
hs_timestamp: Date.now()
}
});
}
}

function parseActionItems(actionItemsText) {
const items = [];
const lines = actionItemsText.split('\n').filter(l => l.trim().startsWith('[ ]'));

lines.forEach(line => {
const match = line.match(/\[ \] ([^:]+): (.+)/);
if (match) {
items.push({ owner: match[1].trim(), task: match[2].trim() });
}
});

return items;
}

module.exports = { syncToHubSpot };

Step 5: Slack Notifications​

Alert the team when calls are processed:

// lib/notify.js
async function sendSlackNotification(channel, analysis, meetingInfo) {
const blocks = [
{
type: 'header',
text: {
type: 'plain_text',
text: `πŸ“ž Call Analyzed: ${meetingInfo.title}`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Summary:* ${analysis.summary}`
}
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*Priority:* ${analysis.priority.split('\n')[0]}`
},
{
type: 'mrkdwn',
text: `*Stage:* ${analysis.dealStage.split('\n')[0]}`
}
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Buying Signals:*\n${analysis.buyingSignals.substring(0, 500)}`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Action Items:*\n${analysis.actionItems.substring(0, 500)}`
}
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'πŸŽ₯ View Recording' },
url: meetingInfo.recordingUrl
},
{
type: 'button',
text: { type: 'plain_text', text: 'πŸ“Š View Deal' },
url: `https://app.hubspot.com/contacts/deals/${meetingInfo.dealId}`
}
]
}
];

await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ blocks })
});
}

module.exports = { sendSlackNotification };

Step 6: The Complete Pipeline​

Bring it all together in an OpenClaw agent:

# agents/meeting-processor.yaml
name: MeetingProcessor
description: Orchestrates the full meeting analysis pipeline

triggers:
- type: webhook
path: /process-meeting

flow:
- step: download
action: Download recording from URL or copy from local path

- step: transcribe
action: Run Whisper transcription
tool: transcribe_audio

- step: analyze
action: Analyze transcript with Claude
tool: analyze_transcript

- step: find_deal
action: Match meeting to CRM deal based on attendees
tool: find_hubspot_deal

- step: sync_crm
action: Push analysis to HubSpot
tool: sync_to_hubspot

- step: notify
action: Send Slack notification
tool: send_slack_alert

- step: archive
action: Store transcript and analysis
tool: save_to_storage

Cost Comparison​

ComponentEnterprise (Gong)DIY Solution
Per-user licensing$1,400/user/year$0
TranscriptionIncluded$0.006/min (Whisper API)
AnalysisIncluded~$0.02/call (Claude)
StorageIncluded~$5/month (S3)
10-user team, 500 calls/month$14,000/year~$600/year

That's 95% cost savings while getting the features that actually matter.

What You Lose vs. Gong​

Let's be honest about trade-offs:

You won't have:

  • Native conversation search across all calls
  • Automatic competitor mention alerts
  • Deal boards with call activity
  • Manager dashboards
  • iOS/Android mobile apps
  • SOC 2 / HIPAA compliance out of the box

You will have:

  • Call transcripts and summaries βœ“
  • Action items auto-pushed to CRM βœ“
  • Deal stage recommendations βœ“
  • Slack notifications βœ“
  • Basic coaching notes βœ“
  • Your data, your infrastructure βœ“

For many early-stage teams, that's plenty.

When to Upgrade to Enterprise​

This DIY approach is great until:

  • You have 20+ reps needing coaching dashboards
  • Compliance requires certified vendors
  • You need real-time call guidance
  • Leadership wants exec-level reporting

At that point, the $14K/year for Gong or Chorus becomes worth it. But for a 5-10 person sales team? Build it yourself.


Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Want AI That Tells You What to Do Next?​

Meeting transcription is one piece of the puzzle. MarketBetter's AI SDR playbook connects all your signalsβ€”calls, emails, website visits, intent dataβ€”into a single daily task list for your team.

Book a demo β†’


Related reading:

AI Sales Reporting Automation with OpenClaw [2026]

Β· 8 min read
Sunder Iyer
Founder, marketbetter.ai

Every Monday morning, your sales ops team spends 4 hours pulling data from Salesforce, formatting Excel spreadsheets, and writing the same commentary they wrote last week.

Meanwhile, your VP of Sales waits until noon to see numbers that are already 12 hours old.

This is insane. It's 2026.

In this guide, I'll show you how to use OpenClaw to automate your entire sales reporting workflowβ€”so your reports are ready at 6am Monday, not noon, and your ops team can focus on actual operations.

AI Sales Reporting Dashboard Automation

The Sales Reporting Problem​

Here's what typical B2B sales reporting looks like:

  • Monday 7am: Sales ops starts pulling CRM data
  • Monday 9am: Data exported, formatting begins
  • Monday 11am: First draft of report ready
  • Monday 11:30am: VP asks for "one more cut of the data"
  • Monday 12pm: Report finally sent to leadership
  • Monday 1pm: Someone notices a number is wrong
  • Monday 2pm: Corrected report sent
  • Tuesday: Everyone's moved on; report was barely used

Time wasted: 8+ hours/week Data freshness: 12-18 hours old by the time leadership sees it Accuracy: Variable (humans make mistakes when copy-pasting)

Why OpenClaw for Reporting​

OpenClaw is an open-source AI gateway that runs agents 24/7. For sales reporting, this means:

  1. Scheduled execution β€” Reports generate at 6am automatically
  2. Multi-source data β€” Pull from CRM, spreadsheets, data warehouse
  3. Natural language insights β€” AI writes the commentary, not your ops team
  4. Always-on delivery β€” Reports hit Slack/email before anyone's awake
  5. Self-hosted and free β€” No $35K/year for a reporting tool

Building Automated Sales Reports​

Step 1: Set Up Data Connections​

OpenClaw can pull from any API. Here's how to connect your sources:

// data-sources.js

// HubSpot CRM
async function getHubSpotPipeline() {
const deals = await hubspot.crm.deals.getAll({
properties: [
'dealname', 'amount', 'dealstage', 'closedate',
'pipeline', 'hs_forecast_amount', 'hubspot_owner_id'
],
associations: ['companies', 'contacts']
});

return deals.filter(d =>
d.properties.pipeline === 'default' &&
d.properties.closedate >= startOfQuarter()
);
}

// Salesforce
async function getSalesforcePipeline() {
const deals = await salesforce.query(`
SELECT Id, Name, Amount, StageName, CloseDate,
ForecastCategory, OwnerId, Account.Name
FROM Opportunity
WHERE CloseDate >= THIS_QUARTER
AND IsClosed = false
`);

return deals.records;
}

// Data warehouse (for historical data)
async function getHistoricalMetrics(period) {
const result = await bigquery.query(`
SELECT
DATE_TRUNC(close_date, WEEK) as week,
SUM(amount) as closed_won,
COUNT(*) as deals_closed,
AVG(sales_cycle_days) as avg_cycle
FROM sales.opportunities
WHERE close_date >= DATE_SUB(CURRENT_DATE(), INTERVAL ${period} DAY)
AND stage = 'Closed Won'
GROUP BY 1
ORDER BY 1
`);

return result.rows;
}

Step 2: Create the Report Generator​

// report-generator.js
const Anthropic = require('@anthropic-ai/sdk');
const claude = new Anthropic();

async function generateWeeklyReport() {
// Gather all data
const pipeline = await getHubSpotPipeline();
const historical = await getHistoricalMetrics(90);
const repMetrics = await getRepPerformance();

// Calculate key metrics
const metrics = {
totalPipeline: sumPipeline(pipeline),
forecastedClose: sumForecast(pipeline),
pipelineChange: calculateWoWChange(pipeline),
topDeals: getTopDeals(pipeline, 5),
atRisk: getAtRiskDeals(pipeline),
closedThisWeek: getClosedDeals('this-week'),
repLeaderboard: calculateLeaderboard(repMetrics)
};

// Generate narrative with Claude
const narrative = await claude.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 2048,
messages: [{
role: 'user',
content: `Generate the executive summary for our weekly sales report.

CURRENT METRICS:
${JSON.stringify(metrics, null, 2)}

HISTORICAL CONTEXT:
${JSON.stringify(historical, null, 2)}

Write a 3-4 paragraph executive summary that:
1. Highlights the most important number (good or bad)
2. Explains what changed this week and why
3. Calls out deals that need attention
4. Ends with a forward-looking statement

Be direct and specific. Executives don't want fluff.
If numbers are concerning, say so clearly.
Include specific rep names and deal names when relevant.`
}]
});

return {
metrics,
narrative: narrative.content[0].text,
generatedAt: new Date().toISOString()
};
}

Step 3: Format for Different Audiences​

Different stakeholders need different views:

async function formatForAudience(report, audience) {
const formats = {
'cro': {
sections: ['executive_summary', 'forecast_vs_target', 'top_deals', 'risks'],
detail: 'high-level',
length: 'short'
},
'sales-managers': {
sections: ['team_performance', 'rep_leaderboard', 'coaching_opportunities', 'deals_needing_help'],
detail: 'medium',
length: 'medium'
},
'sales-ops': {
sections: ['full_pipeline', 'data_quality_issues', 'process_bottlenecks', 'forecasting_accuracy'],
detail: 'granular',
length: 'detailed'
},
'board': {
sections: ['arr_progress', 'quota_attainment', 'key_wins', 'market_trends'],
detail: 'strategic',
length: 'concise'
}
};

const config = formats[audience];

const formatted = await claude.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages: [{
role: 'user',
content: `Format this sales report for ${audience}:

RAW REPORT:
${JSON.stringify(report, null, 2)}

FORMAT CONFIG:
- Sections to include: ${config.sections.join(', ')}
- Detail level: ${config.detail}
- Length: ${config.length}

Output as clean markdown suitable for Slack or email.
Use tables where appropriate.
Bold key numbers.`
}]
});

return formatted.content[0].text;
}

Automated Weekly Sales Report Generation

Step 4: Schedule with OpenClaw​

Set up automated delivery:

# openclaw-config.yaml
agents:
sales-reports:
description: "Weekly sales reporting automation"
schedule:
# Monday 6am - Weekly report
- cron: "0 6 * * 1"
task: |
Generate the weekly sales report.
Format for CRO and post to #executive-updates.
Format for sales managers and post to #sales-team.
Format for ops and send to sales-ops@company.com.

# Daily 7am - Pipeline snapshot
- cron: "0 7 * * *"
task: |
Generate daily pipeline snapshot.
Only post if significant changes (>5% movement).
Alert if any deal moves backward in stage.

# Friday 4pm - Week preview
- cron: "0 16 * * 5"
task: |
Generate next week preview.
List deals expected to close.
Flag any at-risk deals needing weekend attention.

Step 5: Slack Delivery​

Reports land where people actually look:

async function deliverToSlack(report, channel, audience) {
const formatted = await formatForAudience(report, audience);

await slack.postMessage({
channel: channel,
text: `πŸ“Š Weekly Sales Report - ${formatDate(new Date())}`,
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: `πŸ“Š Weekly Sales Report` }
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Total Pipeline:*\n$${(report.metrics.totalPipeline / 1e6).toFixed(1)}M` },
{ type: 'mrkdwn', text: `*Forecast:*\n$${(report.metrics.forecastedClose / 1e6).toFixed(1)}M` },
{ type: 'mrkdwn', text: `*WoW Change:*\n${report.metrics.pipelineChange > 0 ? '↑' : '↓'} ${Math.abs(report.metrics.pipelineChange)}%` },
{ type: 'mrkdwn', text: `*Closed This Week:*\n$${(report.metrics.closedThisWeek / 1e3).toFixed(0)}K` }
]
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*Executive Summary*\n${report.narrative}` }
},
{
type: 'divider'
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*Top 5 Deals to Watch*\n${formatTopDeals(report.metrics.topDeals)}` }
},
{
type: 'actions',
elements: [
{ type: 'button', text: { type: 'plain_text', text: 'Full Report' }, url: report.fullReportUrl },
{ type: 'button', text: { type: 'plain_text', text: 'Pipeline Dashboard' }, url: report.dashboardUrl }
]
}
]
});
}

Advanced: Real-Time Alerts​

Don't wait for weekly reports when something urgent happens:

// real-time-alerts.js

async function monitorPipelineChanges() {
// Check every 15 minutes
const currentState = await getHubSpotPipeline();
const previousState = await getPreviousSnapshot();

const changes = detectChanges(currentState, previousState);

for (const change of changes) {
if (shouldAlert(change)) {
const alert = await generateAlert(change);
await deliverAlert(alert, change.severity);
}
}

await saveSnapshot(currentState);
}

function shouldAlert(change) {
const alertRules = [
// Deal moves backward
(c) => c.type === 'stage_change' && c.direction === 'backward' && c.amount > 50000,

// Large deal close date pushed
(c) => c.type === 'date_change' && c.daysPushed > 14 && c.amount > 100000,

// Deal marked at risk
(c) => c.type === 'forecast_change' && c.newCategory === 'Pipeline',

// Big deal created
(c) => c.type === 'new_deal' && c.amount > 200000,

// Deal closed won
(c) => c.type === 'stage_change' && c.newStage === 'Closed Won'
];

return alertRules.some(rule => rule(change));
}

async function generateAlert(change) {
const context = await getFullDealContext(change.dealId);

const alert = await claude.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 500,
messages: [{
role: 'user',
content: `Generate a brief alert for this pipeline change:

CHANGE: ${JSON.stringify(change)}
DEAL CONTEXT: ${JSON.stringify(context)}

Write a 2-3 sentence alert that:
1. States what changed
2. Explains potential impact
3. Suggests action if needed

Keep it brief - this is a real-time notification.`
}]
});

return alert.content[0].text;
}

Report Templates​

Weekly Pipeline Report​

# πŸ“Š Weekly Pipeline Report
**Week of [DATE] | Generated [TIMESTAMP]**

## Executive Summary
[AI-generated narrative]

## Key Metrics
| Metric | This Week | Last Week | Change |
|--------|-----------|-----------|--------|
| Total Pipeline | $X.XM | $X.XM | +X% |
| Forecast | $X.XM | $X.XM | +X% |
| Closed Won | $XXK | $XXK | +X% |
| Avg Deal Size | $XXK | $XXK | +X% |

## Top Deals
1. **[Deal Name]** - $XXK - [Stage] - Close: [Date]
- Owner: [Rep Name]
- Next Step: [Action]

## At-Risk Deals
⚠️ **[Deal Name]** - $XXK
- Risk: [Reason]
- Recommended Action: [Suggestion]

## Rep Leaderboard
| Rep | Pipeline | Closed | % of Quota |
|-----|----------|--------|------------|
| [Name] | $XXK | $XXK | XX% |

## Looking Ahead
[AI-generated forward-looking commentary]

ROI Calculation​

TaskTime (Manual)Time (AI)Weekly Savings
Data gathering2 hours02 hours
Formatting1.5 hours01.5 hours
Writing commentary1 hour5 min55 min
Distribution30 min030 min
Ad-hoc requests2 hours15 min1.75 hours
Weekly Total7 hours20 min6.7 hours

Annual savings: 6.7 hours Γ— 52 weeks = 348 hours/year

At $75/hour fully loaded, that's $26,100/year savedβ€”and your reports are better, faster, and more accurate.

Getting Started​

  1. Map your current reports β€” What do you send, to whom, when?
  2. Connect data sources β€” CRM API, data warehouse, spreadsheets
  3. Build the first report β€” Start with your weekly pipeline report
  4. Test for 2 weeks β€” Run AI reports alongside manual ones
  5. Switch over β€” Once confidence is high, automate fully
  6. Expand β€” Add real-time alerts, board reports, rep dashboards

Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Stop Building Reports, Start Using Them​

MarketBetter helps GTM teams work smarter with AI-powered automation. Our platform tells your SDRs exactly who to contact, when, and what to sayβ€”so they can focus on selling instead of spreadsheets.

Book a Demo β†’

See how teams are automating sales operations with AI.

AI-Powered Upsell Detection: Finding Hidden Revenue with Claude Code [2026]

Β· 11 min read
Sunder Iyer
Founder, marketbetter.ai

Your best sales opportunity isn't that cold lead you're chasing. It's the customer you already have.

The data is clear:

  • Acquiring a new customer costs 5-25x more than retaining one
  • Existing customers are 50% more likely to try new products
  • They spend 31% more than new customers
  • Upsell/cross-sell drives 70-95% of revenue for mature SaaS companies

Yet most sales teams treat expansion like an afterthought. CSMs occasionally notice opportunities. AEs occasionally ask. Most revenue sits on the table.

With Claude Code, you can systematically scan every customer signalβ€”usage data, support tickets, call transcripts, email threadsβ€”and surface upsell opportunities before customers even know they need more.

This guide shows you exactly how to build that system.

AI upsell detection workflow

The Signals You're Missing​

Every customer generates expansion signals. Most companies capture maybe 10% of them:

Usage Signals​

  • Hitting plan limits: Storage at 90%, seats maxed out, API calls capped
  • Feature adoption velocity: Using new features faster than average
  • Power user emergence: Individual users far exceeding normal usage
  • Cross-team adoption: New departments/teams starting to use product
  • Usage growth trajectory: 20%+ MoM growth = expansion candidate

Support Signals​

  • Feature requests: Asking for capabilities in higher tiers
  • Integration questions: "Can we connect this to X?" = deeper investment
  • Workaround requests: Hacking around limitations = need upgrade
  • Multi-team tickets: Multiple departments engaging = spreading use
  • "Is there a way to..." questions: Implicit feature discovery

Conversation Signals​

  • Budget mentions: "We're planning next year's budget..."
  • Team growth: "We're hiring 5 more SDRs..."
  • Initiative mentions: "We're launching a new product line..."
  • Pain escalation: Problems getting worse = urgency to solve
  • Competitive mentions: "We're also looking at X for this use case"

Behavioral Signals​

  • Login frequency increases: More engagement = more value extracted
  • New user invites: Account expansion happening organically
  • Report/export usage: Executives looking at data = strategic value
  • API usage growth: Technical integration deepening

Building the Detection System​

Here's how to build comprehensive upsell detection with Claude Code:

Architecture Overview​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Data Sources β”‚
β”‚ Usage metrics, Support tickets, Calls, Emails, CRM β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Signal Extraction β”‚
β”‚ Claude analyzes each source for expansion indicators β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Signal Aggregation β”‚
β”‚ Combine signals into customer-level expansion score β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Opportunity Identification β”‚
β”‚ Match signals to specific products/tiers/add-ons β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Alert & Action β”‚
β”‚ Notify CSM/AE with context, talking points, timing β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 1: Usage Signal Detection​

# usage_signal_detector.py
from anthropic import Anthropic
import json
from datetime import datetime, timedelta

class UsageSignalDetector:
def __init__(self):
self.client = Anthropic()

def analyze_usage_patterns(self, customer_id: str) -> dict:
"""Analyze customer usage data for expansion signals."""

# Pull usage data from your analytics system
usage_data = self.get_usage_data(customer_id)
plan_limits = self.get_plan_limits(customer_id)

analysis_prompt = f"""
Analyze this customer's usage data for expansion/upsell signals.

## Current Plan
{json.dumps(plan_limits, indent=2)}

## Usage Data (Last 90 Days)
{json.dumps(usage_data, indent=2)}

## Identify
1. Features approaching or exceeding limits
2. Usage growth trends (week-over-week, month-over-month)
3. Power users who might need advanced features
4. New use cases emerging (based on feature adoption)
5. Comparison to similar customers who upgraded

## Output
Return JSON:
{{
"expansion_score": 0-100,
"primary_signal": "most significant expansion indicator",
"signals": [
{{
"type": "limit_approaching|growth_trend|power_user|new_use_case",
"description": "what we observed",
"urgency": "high|medium|low",
"relevant_upsell": "which product/tier addresses this"
}}
],
"recommended_timing": "immediate|this_month|next_quarter",
"talking_points": ["point 1", "point 2", "point 3"]
}}
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1500,
messages=[{"role": "user", "content": analysis_prompt}]
)

return json.loads(response.content[0].text)

def get_usage_data(self, customer_id: str) -> dict:
"""Pull usage metrics from your analytics system."""
# Implementation depends on your stack (Mixpanel, Amplitude, custom)
pass

def detect_limit_approach(self, customer_id: str, threshold: float = 0.8) -> list:
"""Find customers approaching plan limits."""

usage = self.get_usage_data(customer_id)
limits = self.get_plan_limits(customer_id)

approaching_limits = []

for metric, current_value in usage.items():
if metric in limits:
limit = limits[metric]
utilization = current_value / limit if limit > 0 else 0

if utilization >= threshold:
approaching_limits.append({
"metric": metric,
"current": current_value,
"limit": limit,
"utilization": f"{utilization:.1%}",
"days_to_limit": self.estimate_days_to_limit(
customer_id, metric, current_value, limit
)
})

return approaching_limits

Step 2: Support Ticket Analysis​

# support_signal_detector.py
class SupportSignalDetector:
def __init__(self):
self.client = Anthropic()

def analyze_support_tickets(self, customer_id: str, days: int = 90) -> dict:
"""Analyze support tickets for expansion signals."""

tickets = self.get_recent_tickets(customer_id, days)

if not tickets:
return {"expansion_score": 0, "signals": []}

analysis_prompt = f"""
Analyze these support tickets for upsell/expansion signals.

## Tickets
{json.dumps(tickets, indent=2)}

## Signal Types to Look For
1. Feature requests that exist in higher tiers
2. Questions about integrations/APIs
3. Workaround requests (working around limitations)
4. Multi-department/team involvement
5. Scaling concerns or performance issues
6. Questions about enterprise features (SSO, audit logs, etc.)

## Output
Return JSON:
{{
"expansion_score": 0-100,
"signals": [
{{
"ticket_id": "reference",
"signal_type": "feature_request|integration|workaround|scaling|enterprise",
"quote": "relevant excerpt from ticket",
"implied_need": "what this suggests they need",
"relevant_upsell": "product/tier that addresses this"
}}
],
"pattern_summary": "overall pattern observed",
"recommended_action": "specific next step"
}}
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1500,
messages=[{"role": "user", "content": analysis_prompt}]
)

return json.loads(response.content[0].text)

def get_recent_tickets(self, customer_id: str, days: int) -> list:
"""Pull tickets from Zendesk, Intercom, Freshdesk, etc."""
# Implementation depends on your support platform
pass

Step 3: Conversation Analysis​

# conversation_signal_detector.py
class ConversationSignalDetector:
def __init__(self):
self.client = Anthropic()

def analyze_call_transcripts(self, customer_id: str) -> dict:
"""Analyze recent call transcripts for expansion signals."""

transcripts = self.get_recent_calls(customer_id)

analysis_prompt = f"""
Analyze these customer call transcripts for expansion/upsell signals.

## Transcripts
{json.dumps(transcripts, indent=2)}

## Signals to Identify
1. Budget discussions or planning mentions
2. Team growth or hiring plans
3. New initiatives or product launches
4. Pain points getting worse
5. Competitive mentions or evaluations
6. Questions about capabilities they don't have
7. Success stories they're sharing (ready to expand)
8. Executive involvement (strategic importance)

## Output
Return JSON:
{{
"expansion_score": 0-100,
"signals": [
{{
"call_date": "date",
"signal_type": "budget|growth|initiative|pain|competitive|capability|success|executive",
"quote": "exact words that indicate the signal",
"context": "surrounding context",
"urgency": "high|medium|low",
"recommended_response": "how to follow up"
}}
],
"key_stakeholders": ["names and roles identified"],
"best_timing": "when to approach based on what was said",
"risk_factors": ["any concerns or objections mentioned"]
}}
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": analysis_prompt}]
)

return json.loads(response.content[0].text)

def analyze_email_threads(self, customer_id: str) -> dict:
"""Analyze email communications for expansion signals."""

emails = self.get_customer_emails(customer_id)

analysis_prompt = f"""
Analyze these email threads with the customer for expansion signals.

## Emails
{json.dumps(emails, indent=2)}

## Look For
1. Requests that indicate growing needs
2. Forward-looking statements about plans
3. Questions about pricing or contracts
4. Mentions of other teams or use cases
5. Positive feedback (good time to expand)
6. Frustrations that upgrades would solve

## Output
Return JSON with signals found, quotes, and recommendations.
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1500,
messages=[{"role": "user", "content": analysis_prompt}]
)

return json.loads(response.content[0].text)

Customer expansion opportunity mapping

Step 4: Signal Aggregation​

# expansion_scorer.py
class ExpansionScorer:
def __init__(self):
self.client = Anthropic()
self.usage_detector = UsageSignalDetector()
self.support_detector = SupportSignalDetector()
self.conversation_detector = ConversationSignalDetector()

def calculate_expansion_score(self, customer_id: str) -> dict:
"""Aggregate all signals into a comprehensive expansion score."""

# Gather signals from all sources
usage_signals = self.usage_detector.analyze_usage_patterns(customer_id)
support_signals = self.support_detector.analyze_support_tickets(customer_id)
conversation_signals = self.conversation_detector.analyze_call_transcripts(customer_id)

# Get customer context
customer_data = self.get_customer_data(customer_id)

aggregation_prompt = f"""
Calculate a comprehensive expansion opportunity score for this customer.

## Customer Profile
{json.dumps(customer_data, indent=2)}

## Usage Signals
{json.dumps(usage_signals, indent=2)}

## Support Signals
{json.dumps(support_signals, indent=2)}

## Conversation Signals
{json.dumps(conversation_signals, indent=2)}

## Scoring Framework
- Base score from usage patterns (0-30 points)
- Support signal strength (0-25 points)
- Conversation buying signals (0-25 points)
- Account health/NPS bonus (0-10 points)
- Timing factors (0-10 points)

## Output
Return JSON:
{{
"total_score": 0-100,
"score_breakdown": {{
"usage": 0-30,
"support": 0-25,
"conversation": 0-25,
"health": 0-10,
"timing": 0-10
}},
"confidence": "high|medium|low",
"primary_opportunity": {{
"product": "specific upsell opportunity",
"estimated_value": "ARR increase",
"win_probability": "percentage",
"key_signal": "strongest indicator"
}},
"secondary_opportunities": [...],
"recommended_approach": "how to start the conversation",
"ideal_timing": "when to reach out",
"key_stakeholder": "who to engage",
"talking_points": ["personalized points based on their signals"],
"risk_factors": ["potential objections to prepare for"]
}}
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": aggregation_prompt}]
)

return json.loads(response.content[0].text)

def generate_expansion_playbook(self, customer_id: str) -> dict:
"""Generate a complete playbook for approaching this expansion opportunity."""

score_data = self.calculate_expansion_score(customer_id)
customer_data = self.get_customer_data(customer_id)

playbook_prompt = f"""
Create a detailed expansion playbook for this opportunity.

## Customer
{json.dumps(customer_data, indent=2)}

## Expansion Analysis
{json.dumps(score_data, indent=2)}

## Generate
1. Email template to open the conversation
2. Call script with discovery questions
3. Objection handling for likely concerns
4. ROI calculation framework
5. Timeline and next steps

Make it specific to this customer's signals, not generic.
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=3000,
messages=[{"role": "user", "content": playbook_prompt}]
)

return {
"score_data": score_data,
"playbook": response.content[0].text
}

Step 5: Automated Alerting​

# expansion_alerts.py
class ExpansionAlertSystem:
def __init__(self):
self.scorer = ExpansionScorer()
self.alert_threshold = 70 # Score threshold for alerts

def run_daily_scan(self):
"""Scan all customers for expansion opportunities."""

customers = self.get_all_active_customers()
opportunities = []

for customer in customers:
try:
score_data = self.scorer.calculate_expansion_score(customer['id'])

if score_data['total_score'] >= self.alert_threshold:
opportunities.append({
"customer": customer,
"score_data": score_data,
"playbook": self.scorer.generate_expansion_playbook(customer['id'])
})
except Exception as e:
self.log_error(f"Failed to score {customer['id']}: {e}")

# Sort by score and notify
opportunities.sort(key=lambda x: x['score_data']['total_score'], reverse=True)

self.send_alerts(opportunities)

return opportunities

def send_alerts(self, opportunities: list):
"""Send alerts to CSMs/AEs for each opportunity."""

for opp in opportunities:
customer = opp['customer']
score = opp['score_data']

# Format alert message
alert = f"""
🎯 *EXPANSION OPPORTUNITY*

🏒 {customer['name']}
πŸ“Š Score: {score['total_score']}/100
πŸ’° Potential: {score['primary_opportunity']['estimated_value']}
🎲 Win Probability: {score['primary_opportunity']['win_probability']}

*Top Signal:* {score['primary_opportunity']['key_signal']}

*Recommended Approach:*
{score['recommended_approach']}

*Talking Points:*
{chr(10).join('β€’ ' + p for p in score['talking_points'][:3])}

*Best Timing:* {score['ideal_timing']}

[View Full Playbook]
"""

# Route to appropriate owner
owner = self.get_account_owner(customer['id'])
self.send_notification(owner, alert, opp['playbook'])

def send_notification(self, owner: dict, alert: str, playbook: dict):
"""Send via appropriate channel (Slack, email, WhatsApp)."""
# Implementation depends on your notification setup
pass

Running It with OpenClaw​

Set up automated scanning with OpenClaw cron:

# openclaw.yaml
cron:
jobs:
# Daily expansion opportunity scan
- name: "expansion-scan"
schedule: "0 7 * * 1-5" # 7am weekdays
payload:
kind: systemEvent
text: |
Run the expansion opportunity scan:
1. Score all active customers
2. Identify those above threshold
3. Generate playbooks for top 10
4. Alert account owners via Slack
5. Log results to dashboard
sessionTarget: main

# Weekly expansion summary
- name: "expansion-summary"
schedule: "0 9 * * 1" # Monday 9am
payload:
kind: systemEvent
text: |
Generate weekly expansion pipeline report:
- Total expansion opportunities identified
- Opportunities by segment
- This week's priority accounts
- Progress on last week's opportunities
Send to #sales-leadership channel.
sessionTarget: main

What This Looks Like in Practice​

Without AI:

  • CSM notices customer asked about SSO (3 months ago)
  • CSM mentions to AE at quarterly review
  • AE reaches out... customer already evaluated competitors

With AI:

  • Day 1: Customer asks about SSO in support ticket
  • Day 1: AI flags enterprise feature interest, scores opportunity
  • Day 2: AE receives alert with context and talking points
  • Day 3: AE reaches out, references their specific need
  • Day 14: Deal closedβ€”customer upgraded to Enterprise

The difference:

  • Opportunity identified 90 days faster
  • Context preserved (no telephone game)
  • Personalized outreach (not generic "time for a review?")
  • Higher conversion (right message, right time)

Measuring Success​

Track these metrics:

def calculate_expansion_metrics(period_days: int = 30) -> dict:
return {
# Detection metrics
"opportunities_identified": 47,
"avg_score_of_opportunities": 78,
"false_positive_rate": 0.12, # Flagged but no real opportunity

# Conversion metrics
"opportunities_acted_on": 38,
"opportunities_converted": 24,
"conversion_rate": 0.63,

# Revenue metrics
"expansion_arr_identified": 890000,
"expansion_arr_closed": 540000,
"avg_deal_size": 22500,

# Efficiency metrics
"time_from_signal_to_outreach_days": 1.8,
"time_from_outreach_to_close_days": 18,

# Comparison (AI vs manual)
"ai_identified_conversion_rate": 0.63,
"manually_identified_conversion_rate": 0.34
}
Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Getting Started​

Week 1: Connect Data Sources​

  • Connect usage analytics (Mixpanel, Amplitude, custom)
  • Connect support system (Zendesk, Intercom)
  • Connect CRM (HubSpot, Salesforce)
  • Set up Claude Code API access

Week 2: Build Core Detection​

  • Implement usage signal detection
  • Implement support ticket analysis
  • Test on 10 known expansion accounts (validate accuracy)

Week 3: Add Conversation Analysis​

  • Connect call recording system (Gong, Chorus)
  • Implement email analysis
  • Build aggregation scoring

Week 4: Deploy & Iterate​

  • Set up daily scans via OpenClaw cron
  • Route alerts to CSMs/AEs
  • Track outcomes, tune thresholds
  • Add conversation analysis as transcripts become available

Ready to find the revenue hiding in your customer base? See how MarketBetter identifies expansion opportunities automatically β†’

Automate LinkedIn Sales Navigator with Claude Code: The Complete Guide [2026]

Β· 10 min read
MarketBetter Team
Content Team, marketbetter.ai

Your SDRs spend 3-4 hours per day doing the same thing: searching Sales Navigator, copying profiles to a spreadsheet, researching each prospect, then writing "personalized" messages that still sound generic.

That's $50,000+ per year in SDR salary spent on copy-paste work.

What if an AI agent could do the research in seconds and actually write messages that reference real details from each prospect's profile?

That's exactly what we're building in this guide using Claude Code.

One thing to settle up front: Claude has no official LinkedIn integration, so every approach here is a workaround with its own risk profile. If you haven't already, read Can Claude connect to LinkedIn? β€” it ranks the three real options by account risk.

LinkedIn Sales Navigator automation workflow with AI processing

Why LinkedIn Automation Is Broken (And How AI Fixes It)​

Let's be honest about the current state of LinkedIn automation:

The Old Way: Automation Tools​

Tools like LinkedHelper, Dux-Soup, and Expandi can automate connection requests and messages. The problem? LinkedIn detects them, they send the same message to everyone, and your account gets restricted.

The Slightly Better Way: Virtual Assistants​

Hire a VA in the Philippines to manually send messages. More personalized, but expensive ($800-1500/month for dedicated), slow, and they still struggle with research depth.

The AI Way: Claude Code + Browser Automation​

Use Claude's reasoning to actually understand each prospect's profile, then generate genuinely personalized outreach. Not "Hi {first_name}, I saw you work at {company}"β€”real personalization.

The difference: Claude can read a VP of Sales' entire career history, their recent posts, their company's latest funding round, and synthesize that into an opening line that feels like you actually did your homework.

The Architecture: What We're Building​

Here's the system:

  1. Input: ICP criteria (title, industry, company size, location)
  2. Process: Claude Code researches each prospect using Sales Navigator data
  3. Output: Personalized first-touch messages ready to send

We'll use browser automation (via Playwright) to interact with Sales Navigator, and Claude Code to do the thinking.

AI analyzing LinkedIn profiles and generating personalized outreach

Prerequisites​

Before we start:

  • LinkedIn Sales Navigator account (Core or Advanced)
  • Claude API access (via Anthropic)
  • Node.js 18+ installed
  • Basic familiarity with JavaScript

Step 1: Set Up the Project​

mkdir linkedin-prospector && cd linkedin-prospector
npm init -y
npm install playwright @anthropic-ai/sdk dotenv
npx playwright install chromium

Create your environment file:

# .env
ANTHROPIC_API_KEY=sk-ant-...
LINKEDIN_EMAIL=your@email.com
LINKEDIN_PASSWORD=your_password

Step 2: Build the Browser Session Manager​

First, we need a way to maintain a logged-in LinkedIn session:

// lib/browser.js
const { chromium } = require('playwright');
const path = require('path');

const USER_DATA_DIR = path.join(__dirname, '../.browser-data');

async function getLinkedInSession() {
const browser = await chromium.launchPersistentContext(USER_DATA_DIR, {
headless: true, // Set false for debugging
viewport: { width: 1280, height: 800 },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
});

const page = await browser.newPage();

// Check if already logged in
await page.goto('https://www.linkedin.com/feed/');
await page.waitForTimeout(2000);

const isLoggedIn = await page.url().includes('/feed');

if (!isLoggedIn) {
await login(page);
}

return { browser, page };
}

async function login(page) {
await page.goto('https://www.linkedin.com/login');
await page.fill('#username', process.env.LINKEDIN_EMAIL);
await page.fill('#password', process.env.LINKEDIN_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForNavigation();

// Handle potential security checkpoint
if (page.url().includes('checkpoint')) {
console.log('⚠️ Security checkpoint detected. Please complete manually.');
await page.waitForNavigation({ timeout: 120000 });
}
}

module.exports = { getLinkedInSession };

Step 3: Build the Sales Navigator Scraper​

Now, let's extract prospect data from Sales Navigator searches:

// lib/navigator.js
async function searchProspects(page, criteria) {
const { title, industry, companySize, location } = criteria;

// Navigate to Sales Navigator search
await page.goto('https://www.linkedin.com/sales/search/people');
await page.waitForTimeout(2000);

// Apply filters
if (title) {
await page.click('[data-test-filter-button="CURRENT_TITLE"]');
await page.fill('input[placeholder="Add title"]', title);
await page.keyboard.press('Enter');
await page.waitForTimeout(1000);
}

if (companySize) {
await page.click('[data-test-filter-button="COMPANY_HEADCOUNT"]');
// Map size to LinkedIn's options
const sizeMap = {
'small': '11-50',
'medium': '51-200',
'large': '201-500',
'enterprise': '501-1000'
};
await page.click(`text="${sizeMap[companySize]}"`);
await page.waitForTimeout(1000);
}

// Wait for results
await page.waitForSelector('.search-results__result-list');

// Extract prospect data
const prospects = await page.evaluate(() => {
const results = [];
const cards = document.querySelectorAll('.search-results__result-item');

cards.forEach(card => {
const nameEl = card.querySelector('.result-lockup__name');
const titleEl = card.querySelector('.result-lockup__highlight-keyword');
const companyEl = card.querySelector('.result-lockup__position-company');
const linkEl = card.querySelector('a[href*="/sales/lead/"]');

if (nameEl && linkEl) {
results.push({
name: nameEl.textContent.trim(),
title: titleEl?.textContent.trim() || '',
company: companyEl?.textContent.trim() || '',
profileUrl: linkEl.href
});
}
});

return results;
});

return prospects;
}

async function getProspectDetails(page, profileUrl) {
await page.goto(profileUrl);
await page.waitForTimeout(2000);

const details = await page.evaluate(() => {
// Extract comprehensive profile data
const about = document.querySelector('.profile-section-card__contents')?.textContent.trim();

const experience = [];
document.querySelectorAll('.experience-item').forEach(item => {
experience.push({
title: item.querySelector('.experience-item__title')?.textContent.trim(),
company: item.querySelector('.experience-item__subtitle')?.textContent.trim(),
duration: item.querySelector('.date-range')?.textContent.trim(),
description: item.querySelector('.experience-item__description')?.textContent.trim()
});
});

const recentActivity = [];
document.querySelectorAll('.recent-activity-item').forEach(item => {
recentActivity.push(item.textContent.trim().substring(0, 200));
});

return {
about,
experience,
recentActivity,
headline: document.querySelector('.profile-topcard__title')?.textContent.trim(),
location: document.querySelector('.profile-topcard__location')?.textContent.trim()
};
});

return details;
}

module.exports = { searchProspects, getProspectDetails };

Step 4: The Claude Personalization Engine​

This is where the magic happens. Claude reads the profile data and generates truly personalized outreach:

// lib/personalize.js
const Anthropic = require('@anthropic-ai/sdk');

const client = new Anthropic();

async function generatePersonalizedMessage(prospect, details, context) {
const prompt = `You are an SDR writing a LinkedIn connection request. Your goal is to get a response, not make a sale.

PROSPECT DATA:
Name: ${prospect.name}
Current Title: ${prospect.title}
Company: ${prospect.company}
Headline: ${details.headline}
Location: ${details.location}

ABOUT SECTION:
${details.about || 'Not available'}

RECENT EXPERIENCE:
${details.experience.slice(0, 3).map(e =>
`- ${e.title} at ${e.company} (${e.duration}): ${e.description?.substring(0, 100) || 'No description'}`
).join('\n')}

RECENT ACTIVITY:
${details.recentActivity.slice(0, 3).join('\n') || 'No recent activity visible'}

YOUR CONTEXT:
Company: ${context.yourCompany}
What you sell: ${context.valueProposition}
Why this prospect might care: ${context.relevance}

INSTRUCTIONS:
1. Write a LinkedIn connection request (300 char max for the note)
2. Reference something SPECIFIC from their profile - not generic
3. Don't pitch. Ask a question or share an insight.
4. Sound like a human, not a sales bot
5. If they have recent activity, reference it naturally

Output format:
CONNECTION_NOTE: [the 300 char note]
FOLLOW_UP_MESSAGE: [a longer message to send after they accept, 500 chars max]
TALKING_POINTS: [3 bullet points for if they respond]`;

const response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1000,
messages: [{ role: 'user', content: prompt }]
});

// Parse the response
const text = response.content[0].text;
const connectionNote = text.match(/CONNECTION_NOTE:\s*([\s\S]*?)(?=FOLLOW_UP|$)/)?.[1]?.trim();
const followUp = text.match(/FOLLOW_UP_MESSAGE:\s*([\s\S]*?)(?=TALKING|$)/)?.[1]?.trim();
const talkingPoints = text.match(/TALKING_POINTS:\s*([\s\S]*?)$/)?.[1]?.trim();

return {
connectionNote,
followUp,
talkingPoints,
rawResponse: text
};
}

module.exports = { generatePersonalizedMessage };

Step 5: Putting It All Together​

Now let's create the main automation script:

// index.js
require('dotenv').config();
const { getLinkedInSession } = require('./lib/browser');
const { searchProspects, getProspectDetails } = require('./lib/navigator');
const { generatePersonalizedMessage } = require('./lib/personalize');
const fs = require('fs');

const ICP_CRITERIA = {
title: 'VP Sales',
industry: 'SaaS',
companySize: 'medium',
location: 'United States'
};

const CONTEXT = {
yourCompany: 'MarketBetter',
valueProposition: 'AI-powered SDR playbook that tells your team exactly who to contact and what to say',
relevance: 'They manage SDR teams and care about efficiency and pipeline'
};

async function main() {
console.log('πŸš€ Starting LinkedIn prospector...');

const { browser, page } = await getLinkedInSession();

try {
// Search for prospects
console.log('πŸ” Searching Sales Navigator...');
const prospects = await searchProspects(page, ICP_CRITERIA);
console.log(`Found ${prospects.length} prospects`);

const results = [];

// Process each prospect (limit to 10 for rate limiting)
for (const prospect of prospects.slice(0, 10)) {
console.log(`\nπŸ“‹ Processing: ${prospect.name}`);

// Get detailed profile data
const details = await getProspectDetails(page, prospect.profileUrl);

// Generate personalized message with Claude
console.log('πŸ€– Generating personalized message...');
const personalization = await generatePersonalizedMessage(
prospect,
details,
CONTEXT
);

results.push({
...prospect,
details,
personalization
});

console.log(`βœ… Connection note: ${personalization.connectionNote?.substring(0, 50)}...`);

// Rate limiting - be respectful
await page.waitForTimeout(5000);
}

// Save results
const outputFile = `prospects_${Date.now()}.json`;
fs.writeFileSync(outputFile, JSON.stringify(results, null, 2));
console.log(`\nπŸ’Ύ Saved ${results.length} prospects to ${outputFile}`);

} finally {
await browser.close();
}
}

main().catch(console.error);

Sample Output: What You Get​

Here's what the personalized output looks like:

{
"name": "Sarah Chen",
"title": "VP of Sales",
"company": "TechStartup Inc",
"personalization": {
"connectionNote": "Sarah - saw your post about SDR burnout last week. We're seeing the same thing at MarketBetter and built something that might help. Would love to swap notes on what's working for your team.",
"followUp": "Thanks for connecting! Your point about SDRs spending 70% of their day on research really resonated. We've been testing an AI approach that cuts that to about 20 minutes - not replacing the human touch, just the copy-paste work. Happy to share what we've learned if useful.",
"talkingPoints": "- Ask about her current SDR tech stack\n- Share the 70% research time stat from her post\n- Mention the ROI calculator we have"
}
}

That's not a template. That's Claude actually reading her recent LinkedIn post about SDR burnout and crafting a relevant opener.

Time savings comparison showing AI automation versus manual prospecting

The Numbers: Why This Matters​

Let's do the math on a 5-person SDR team:

MetricManual ProspectingAI-Assisted
Time per prospect15 minutes2 minutes
Prospects researched/day20100+
Message personalization depthSurface-levelDeep (career, posts, context)
SDR time on research60% of day15% of day
Response rate5-8%15-25%

The ROI: If your SDRs currently book 4 meetings/week and this doubles their response rate, you're looking at 8 meetings/week. That's 208 extra meetings per year per SDR.

At a $10K average deal size and 20% close rate, that's $416K in additional pipeline per SDR.

Important: Stay Compliant​

A few rules to keep you out of trouble:

  1. Respect LinkedIn's rate limits β€” Don't send more than 100 connection requests per week
  2. Don't automate sending β€” Use this for research and drafting, send manually
  3. Personalization is protection β€” Generic automated messages get flagged; personalized ones don't
  4. Use your real account β€” Sales Navigator is meant for this; sketchy tactics aren't

The goal isn't to game LinkedIn. It's to do better research faster so your human-sent messages actually land.

Taking It Further: Integration with Your Stack​

Once you've validated this works, consider:

  1. HubSpot integration β€” Auto-create contacts with the personalization data
  2. Slack alerts β€” Get notified when high-value prospects are found
  3. A/B testing β€” Track which Claude-generated openers perform best
  4. CRM sync β€” Push talking points to the contact record for sales calls

Claude's 200K context window means you can even include company 10-K filings, recent news, and competitive intel in the personalization prompt.

The Bigger Picture​

LinkedIn prospecting is changing. The "spray and pray" era is over. Buyers can smell automation from a mile away.

The future belongs to teams that can combine AI research speed with human authenticity. Claude does the homework; your SDRs bring the human touch.

That's not "AI replacing salespeople." That's AI making salespeople better.


Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Want to See This in Action?​

MarketBetter's AI SDR playbook uses similar intelligence to tell your team exactly who to reach out to and what to say. No more guessing. No more 20 browser tabs.

Book a demo β†’


Related reading:

Claude Code for Sales Email A/B Testing at Scale [2026]

Β· 8 min read
Sunder Iyer
Founder, marketbetter.ai

You're A/B testing emails wrong.

Most sales teams test two variants. Maybe three if they're ambitious. They wait 2 weeks for statistical significance. Then they pick a winner and move on.

That's not optimization. That's guessing slowly.

In 2026, Claude Code can generate hundreds of email variants, test them across segments in days not weeks, and continuously optimize based on actual reply dataβ€”not open rates, which mean nothing since iOS 15.

This guide shows you how to build an AI-powered email testing system that actually moves the needle.

A/B testing email variants with AI

Why Traditional A/B Testing Fails for Sales Emails​

The Math Problem​

Traditional A/B testing requires statistical significance. For sales emails with typical reply rates (2-5%), you need:

  • Sample size per variant: 500-1000 sends minimum
  • Test duration: 2-4 weeks to collect enough data
  • Variants testable: 2-3 (more = longer tests)

If you send 1,000 emails per month and test 2 variants:

  • You can run 6 tests per year
  • Each test improves reply rate by ~10-15%
  • Annual improvement: ~90% (compounding)

Not bad. But AI can do 10x better.

The Real Problem: You're Testing the Wrong Things​

Most teams test:

  • ❌ Subject line A vs B
  • ❌ CTA button color
  • ❌ First name vs full name

What actually matters:

  • βœ… Value proposition framing
  • βœ… Pain point emphasis
  • βœ… Social proof specificity
  • βœ… Opening hook angle
  • βœ… Call-to-action clarity
  • βœ… Tone match to persona

You can't test these manually at scale. But Claude can.

The AI-Powered Testing Framework​

Here's how to test at 10x the speed:

1. Generate Variant Clusters, Not Individual Emails​

Instead of writing 2 emails, generate clusters of variants that test specific hypotheses:

# variant_generator.py
from anthropic import Anthropic
import json

class EmailVariantGenerator:
def __init__(self):
self.client = Anthropic()

def generate_variant_cluster(
self,
base_context: dict,
hypothesis: str,
num_variants: int = 10
) -> list:
"""Generate a cluster of variants testing a specific hypothesis."""

prompt = f"""
You are an expert B2B sales copywriter. Generate {num_variants} email variants
that test this hypothesis: {hypothesis}

## Context
- Target persona: {base_context['persona']}
- Company: {base_context['company']}
- Pain points: {base_context['pain_points']}
- Value prop: {base_context['value_prop']}
- Goal: {base_context['goal']}

## Requirements
- Each variant should be meaningfully different (not just word swaps)
- Keep emails under 150 words (nobody reads long cold emails)
- Include a clear, single CTA
- Sound human, not AI-generated

## Output Format
Return a JSON array with each variant:
[
{{
"variant_id": "v1",
"hypothesis_element": "what this variant tests",
"subject": "subject line",
"body": "email body",
"cta": "call to action",
"key_differentiator": "what makes this unique"
}}
]
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4000,
messages=[{"role": "user", "content": prompt}]
)

return json.loads(response.content[0].text)

# Example usage
generator = EmailVariantGenerator()

context = {
"persona": "VP of Sales at B2B SaaS company, 50-200 employees",
"company": "MarketBetter",
"pain_points": ["SDR productivity", "lead response time", "data quality"],
"value_prop": "AI-powered SDR workflow automation",
"goal": "Book a demo call"
}

# Generate variants testing different opening hooks
hook_variants = generator.generate_variant_cluster(
context,
hypothesis="Question-based openings outperform statement openings",
num_variants=10
)

# Generate variants testing pain point emphasis
pain_variants = generator.generate_variant_cluster(
context,
hypothesis="Emphasizing time savings beats emphasizing revenue gains",
num_variants=10
)

# Generate variants testing social proof types
proof_variants = generator.generate_variant_cluster(
context,
hypothesis="Specific metrics outperform named customer logos",
num_variants=10
)

Now you have 30 variants testing 3 distinct hypothesesβ€”generated in minutes.

2. Smart Segmentation for Faster Results​

Don't send all variants to everyone. Match variants to micro-segments:

# segment_matcher.py
class SegmentMatcher:
def __init__(self, anthropic_client):
self.client = anthropic_client

def match_variants_to_segments(
self,
variants: list,
segments: list
) -> dict:
"""Use Claude to match variants to segments they're most likely to resonate with."""

prompt = f"""
Match email variants to prospect segments based on likely resonance.

## Variants
{json.dumps(variants, indent=2)}

## Segments
{json.dumps(segments, indent=2)}

For each variant, identify:
1. Primary segment (best fit)
2. Secondary segment (good fit)
3. Avoid segment (poor fit)

Return JSON mapping variant_id to segment recommendations.
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
)

return json.loads(response.content[0].text)

# Define your segments
segments = [
{
"id": "growth_stage",
"description": "Series A-B companies, scaling fast, care about speed",
"typical_pain": "Can't hire SDRs fast enough"
},
{
"id": "enterprise_efficiency",
"description": "Large companies, cost-conscious, care about ROI",
"typical_pain": "SDR team is expensive and underperforming"
},
{
"id": "founder_led",
"description": "Founder still doing sales, limited time",
"typical_pain": "No time for manual prospecting"
},
{
"id": "revops_driven",
"description": "Data-focused teams, care about metrics",
"typical_pain": "Can't measure what's working"
}
]

# Match variants to segments
matcher = SegmentMatcher(Anthropic())
variant_segment_map = matcher.match_variants_to_segments(hook_variants, segments)

Email performance analytics dashboard

3. Continuous Learning Loop​

The real power is in the feedback loop:

# learning_loop.py
class EmailLearningLoop:
def __init__(self):
self.client = Anthropic()
self.results_db = ResultsDatabase()

def analyze_results(self, test_id: str) -> dict:
"""Analyze test results and generate insights."""

results = self.results_db.get_test_results(test_id)

analysis_prompt = f"""
Analyze these email A/B test results and provide actionable insights.

## Test Results
{json.dumps(results, indent=2)}

## Analysis Required
1. Which variants performed best and why?
2. What patterns emerge across winning variants?
3. What should we test next based on these learnings?
4. Any surprising results that warrant investigation?
5. Recommended changes to our email playbook

Be specific. Reference actual data from the results.
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": analysis_prompt}]
)

return {
"analysis": response.content[0].text,
"raw_results": results
}

def generate_next_iteration(
self,
winning_variants: list,
insights: str
) -> list:
"""Generate next round of variants based on learnings."""

prompt = f"""
Based on our A/B test learnings, generate the next iteration of email variants.

## Winning Variants from Last Round
{json.dumps(winning_variants, indent=2)}

## Key Insights
{insights}

## Your Task
Generate 10 new variants that:
1. Build on what worked in the winning variants
2. Test new hypotheses suggested by the insights
3. Push the boundaries of what we've learned

Don't just remix winnersβ€”evolve them.

Return JSON array of new variants.
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4000,
messages=[{"role": "user", "content": prompt}]
)

return json.loads(response.content[0].text)

def run_learning_cycle(self, test_id: str):
"""Complete one learning cycle."""

# 1. Analyze completed test
analysis = self.analyze_results(test_id)

# 2. Identify winners
winners = [r for r in analysis['raw_results']
if r['reply_rate'] > analysis['avg_reply_rate'] * 1.2]

# 3. Generate evolved variants
next_variants = self.generate_next_iteration(
winners,
analysis['analysis']
)

# 4. Queue next test
new_test_id = self.queue_test(next_variants)

return {
"completed_test": test_id,
"insights": analysis['analysis'],
"winners": winners,
"next_test": new_test_id
}

Production System Architecture​

Here's the full system for production:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Variant Generation β”‚
β”‚ Claude generates variant clusters per hypothesis β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Segment Matching β”‚
β”‚ AI matches variants to micro-segments β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Test Execution β”‚
β”‚ Email platform sends variants, tracks engagement β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Results Collection β”‚
β”‚ Reply tracking (not opens!), sentiment analysis β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ AI Analysis β”‚
β”‚ Claude analyzes results, identifies patterns β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Next Iteration β”‚
β”‚ Generate evolved variants, repeat cycle β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Reply Tracking (The Only Metric That Matters)​

Opens are meaningless. Track replies:

# reply_tracker.py
class ReplyTracker:
def __init__(self):
self.client = Anthropic()

def classify_reply(self, reply_text: str) -> dict:
"""Classify reply sentiment and intent."""

prompt = f"""
Classify this email reply from a sales prospect:

"{reply_text}"

Return JSON:
{{
"sentiment": "positive|neutral|negative",
"intent": "interested|not_interested|asking_questions|objection|out_of_office|unsubscribe",
"buying_signal_strength": 0-10,
"next_action": "book_call|send_info|nurture|disqualify",
"key_insight": "what we learned from this reply"
}}
"""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
messages=[{"role": "user", "content": prompt}]
)

return json.loads(response.content[0].text)

def calculate_variant_score(self, variant_id: str) -> dict:
"""Calculate comprehensive score for a variant."""

replies = self.get_replies_for_variant(variant_id)
sends = self.get_sends_for_variant(variant_id)

classified_replies = [self.classify_reply(r['text']) for r in replies]

return {
"variant_id": variant_id,
"total_sends": len(sends),
"total_replies": len(replies),
"reply_rate": len(replies) / len(sends) if sends else 0,
"positive_reply_rate": len([r for r in classified_replies
if r['sentiment'] == 'positive']) / len(sends) if sends else 0,
"avg_buying_signal": sum(r['buying_signal_strength']
for r in classified_replies) / len(classified_replies) if classified_replies else 0,
"meetings_booked": len([r for r in classified_replies
if r['next_action'] == 'book_call']),
"conversion_to_meeting": len([r for r in classified_replies
if r['next_action'] == 'book_call']) / len(sends) if sends else 0
}

What This Looks Like in Practice​

Month 1 (Traditional Testing):

  • Test 2 subject lines
  • Winner: "Quick question about [Company]"
  • Improvement: 12%

Month 1 (AI-Powered Testing):

  • Generate 30 variants across 3 hypothesis clusters
  • Test across 4 segments simultaneously
  • Discover: Question hooks work for growth-stage, but enterprise prefers metrics
  • Discover: Pain-focused body copy beats benefit-focused
  • Discover: Social proof with specific numbers outperforms logos 3:1
  • Cumulative improvement: 47%

Month 3 (AI-Powered, 3 Cycles):

  • 90 variants tested
  • Segment-specific playbooks developed
  • Reply rate: up 180% from baseline
  • Meetings booked: up 220%

The compound effect of continuous learning is massive.

Implementation Checklist​

Week 1: Foundation​

  • Set up Claude Code with Anthropic API
  • Define your 4-6 prospect segments
  • Document your current best-performing email
  • Set up reply tracking (not just opens)

Week 2: First Test Cycle​

  • Generate first variant cluster (10 variants)
  • Define hypothesis being tested
  • Deploy through your email platform
  • Wait for 100+ replies (not sends)

Week 3: Analysis & Iteration​

  • Run AI analysis on results
  • Identify winning patterns
  • Generate evolved variants
  • Launch next test cycle

Ongoing​

  • Run 2-3 test cycles per month
  • Update segment-specific playbooks
  • Document learnings in team wiki
  • Review quarterly for strategic shifts
Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

The Competitive Advantage​

While your competitors are debating whether to test "Quick question" vs "Quick thought" subject lines, you're running 30-variant tests that discover:

  • Enterprise CFOs respond 3x better to ROI framing
  • Startup founders want speed, not savings
  • Mentioning a mutual connection in the first line doubles reply rates
  • Tuesday 10am sends outperform all other times by 40%

This isn't marginal improvement. This is systematic optimization that compounds over time.


Ready to stop guessing and start optimizing? See how MarketBetter automates email sequences with AI β†’

Codex for CRM Pipeline Cleanup: Automate Your Data Hygiene [2026]

Β· 7 min read
MarketBetter Team
Content Team, marketbetter.ai

Your CRM is lying to you. Right now, your pipeline shows deals that will never close, contacts with outdated info, and duplicates inflating your numbers. GPT-5.3 Codex can fix this automatically.

CRM pipeline cleanup automation

The Hidden Cost of Dirty CRM Data​

Every sales org has the same problem:

  • 42% of pipeline is stale deals nobody's touched in 90+ days
  • 30% of contacts have outdated job titles or emails
  • 15% of records are duplicates or near-duplicates
  • Forecast accuracy suffers because the numbers are fiction

RevOps teams spend entire quarters on "data cleanup initiatives" that never fully succeed. Reps hate data entry, so the problem just grows back.

Here's a different approach: let Codex handle it continuously.

Why Codex for Data Cleanup?​

OpenAI released GPT-5.3-Codex on February 5, 2026, with significant improvements for exactly this use case:

  1. Mid-turn steering: Direct the cleanup while it's running. "Skip deals with activity in the last 30 days" without restarting.

  2. Multi-file understanding: Codex can read your CRM schema, understand relationships, and make intelligent decisions about what to clean.

  3. 25% faster than GPT-5.2: Large-scale data operations complete faster, important when processing thousands of records.

  4. Better at edge cases: The new model handles ambiguous situations betterβ€”like deciding if two contacts are duplicates when the data is slightly different.

The Pipeline Cleanup Architecture​

Here's how to build an automated cleanup system:

Component 1: Data Assessment Agent​

First, understand the scope of the problem:

TASK: Analyze CRM data quality

SCHEMA:
[Your HubSpot/Salesforce schema]

RULES:
1. Identify deals with no activity > 60 days
2. Find contacts with bounced emails or invalid phones
3. Detect potential duplicates (same email or similar name + company)
4. Flag deals stuck in same stage > 45 days
5. Identify orphan records (no associated company or deal)

OUTPUT: JSON report with counts and sample records for each category

This gives you a dashboard of data quality issues before any cleanup begins.

Component 2: Deduplication Engine​

Duplicates are the most damaging data quality issue. Here's how Codex handles them:

TASK: Identify and merge duplicate records

MATCHING CRITERIA:
- Exact email match β†’ definite duplicate
- Same company + similar name (Levenshtein distance < 3) β†’ probable duplicate
- Same phone number β†’ probable duplicate
- Same name + same city + same title β†’ possible duplicate

MERGE RULES:
- Keep most recent email
- Keep most recent phone
- Keep earliest created date
- Merge all notes and activities
- Keep the record with more data points as primary

REVIEW THRESHOLD:
- Definite duplicates: auto-merge
- Probable duplicates: auto-merge with audit log
- Possible duplicates: flag for human review

CRM data quality before and after

Component 3: Stale Deal Handler​

Deals that haven't moved in months clog your pipeline and destroy forecast accuracy:

TASK: Process stale deals

ASSESSMENT CRITERIA:
- No activity in 90+ days AND deal created > 120 days ago β†’ Move to Lost
- No activity in 60-90 days β†’ Send "Should we close this?" email sequence
- No activity in 30-60 days AND deal value > $50K β†’ Alert rep
- Deal in same stage > 45 days β†’ Request stage update from rep

ACTIONS:
For deals marked Lost:
1. Update close_lost_reason = "Stale - No engagement"
2. Add note with last activity date
3. Move associated contacts to re-engagement nurture
4. Notify rep of closure

Component 4: Contact Enrichment Refresh​

Job titles change. People switch companies. Emails go stale:

TASK: Refresh contact data

FOR EACH CONTACT with last_enrichment > 180 days:
1. Query enrichment API (Clearbit, Apollo, etc.)
2. Compare new data to existing data
3. If job title changed β†’ update and notify assigned rep
4. If company changed β†’ create new contact, archive old, notify rep
5. If email bounced β†’ try to find new email, else flag for manual research

FREQUENCY: Weekly, prioritizing contacts on active deals first

Mid-Turn Steering in Action​

This is Codex's killer feature for data cleanup. You don't have to plan everything upfront.

Scenario: Cleanup is running, processing stale deals. You realize you want to exclude deals from enterprise accounts.

Old approach: Stop the job. Modify the rules. Restart from the beginning.

With Codex mid-turn steering:

> Pause current processing
> Add filter: exclude deals where account.tier = 'Enterprise'
> Resume processing with new filter

No restart. No reprocessing. The cleanup continues with your new requirement.

This is especially powerful when you're running cleanup for the first time and discovering edge cases you hadn't anticipated.

Integration Setup​

HubSpot Integration​

// hubspot-cleanup.js
const hubspot = require('@hubspot/api-client');
const { OpenAI } = require('openai');

const client = new hubspot.Client({ accessToken: process.env.HUBSPOT_TOKEN });
const openai = new OpenAI();

async function runCleanup() {
// Fetch all deals
const deals = await client.crm.deals.getAll();

// Send to Codex for analysis
const analysis = await openai.chat.completions.create({
model: 'gpt-5.3-codex',
messages: [
{ role: 'system', content: CLEANUP_SYSTEM_PROMPT },
{ role: 'user', content: JSON.stringify(deals) }
]
});

// Process recommended actions
const actions = JSON.parse(analysis.choices[0].message.content);

for (const action of actions) {
if (action.type === 'close_deal') {
await client.crm.deals.basicApi.update(action.dealId, {
properties: { dealstage: 'closedlost', close_reason: action.reason }
});
}
// ... handle other action types
}
}

Salesforce Integration​

Similar pattern with jsforce or the Salesforce REST API. The key is batching your updates to stay within API limits.

Results: What Clean Data Gets You​

After running continuous cleanup for one quarter, our customers typically see:

MetricBeforeAfter
Pipeline accuracy45%82%
Forecast varianceΒ±35%Β±12%
Rep time on data entry6 hrs/week1 hr/week
Duplicate records15%<2%
Stale deals in pipeline42%8%

The forecast improvement alone is worth the setup. When your pipeline reflects reality, you can actually plan.

Running Cleanup Continuously​

Don't run cleanup as a quarterly initiative. Run it continuously:

Daily:

  • Process new duplicates from yesterday's data entry
  • Check for bounced emails
  • Update stale deal flags

Weekly:

  • Full duplicate scan
  • Contact enrichment refresh for active deal contacts
  • Generate data quality report

Monthly:

  • Historical data audit
  • Review auto-close actions
  • Refine rules based on false positives/negatives

Common Questions​

Q: Won't this mess up our historical reporting? A: Keep an audit log of every change. You can always restore or exclude from historical analysis.

Q: What about deals that look stale but are actually active? A: Start with notifications to reps before auto-closing. Track how often reps override. Adjust thresholds based on real patterns.

Q: How do we handle merges when both records have important data? A: Define clear merge rules upfront. When in doubt, concatenate notes and keep both phone numbers. Data is cheap, context is expensive.

Free Tool

Try our AI Lead Generator β€” find verified LinkedIn leads for any company instantly. No signup required.

Get Started​

You can build this yourself with Codex + your CRM's API. Or you can use MarketBetter, where pipeline hygiene is built into the platform.

Our AI continuously monitors your CRM, flags data quality issues, and handles routine cleanup automatically. Reps get prompts to update stale deals. Duplicates get merged. Bad data gets fixed.

Want to see what clean pipeline data looks like? Book a demo and we'll run a data quality assessment on your CRM.


Related reading: