Skip to main content

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:

GPT-5.3 Codex Mid-Turn Steering: The Game-Changer for Sales Ops Automation [2026]

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

Released February 5, 2026. This changes everything.

OpenAI's GPT-5.3-Codex isn't just 25% faster than its predecessor. It introduces a capability that fundamentally changes how we think about AI automation: mid-turn steering.

For the first time, you can redirect an AI agent while it's working—without starting over, without losing context, without waiting for it to finish a wrong approach.

For sales ops teams, this means AI that adapts in real-time to changing requirements. Let me show you why this matters.

Mid-turn steering concept showing human directing AI agent mid-task with course correction arrows

What Is Mid-Turn Steering?

Traditional AI workflows look like this:

Prompt → AI Works → Output → Human Reviews → New Prompt → AI Works Again

Every time you want to adjust direction, you restart the process. For complex tasks—like building a report, analyzing a pipeline, or generating personalized outreach—this creates a painful loop of:

  1. Wait for AI to finish
  2. Realize it went the wrong direction
  3. Craft a new prompt
  4. Wait again
  5. Repeat

Mid-turn steering breaks this pattern:

Prompt → AI Works → Human Steers → AI Adapts → Human Steers → Final Output
↑ ↑
"Focus more on enterprise" "Skip the APAC region"

You're co-piloting instead of backseat driving.

Why This Matters for Sales Ops

Sales operations is full of tasks that require judgment calls mid-stream:

Pipeline Analysis

Without mid-turn steering:

"Analyze our pipeline and identify at-risk deals"

[AI analyzes for 3 minutes]

Output: Lists 47 deals, mostly based on stage duration

You: "No, I meant deals where the champion went dark"

[Start over]

With mid-turn steering:

"Analyze our pipeline and identify at-risk deals"

[AI starts analyzing]

You (mid-turn): "Weight communication gaps heavily"

[AI adjusts, continues]

You (mid-turn): "Actually, focus on deals over $50K only"

[AI filters, continues]

Output: Exactly what you needed, first try

Lead List Building

Without mid-turn steering:

"Build a list of 50 target accounts in fintech"

[AI builds list]

Output: Includes crypto companies, payment processors, neobanks

You: "I meant traditional banks adopting fintech, not fintech startups"

[Start over with clearer prompt]

With mid-turn steering:

"Build a list of 50 target accounts in fintech"

[AI starts building]

You (mid-turn): "Traditional banks only, not startups"

[AI adjusts filters]

You (mid-turn): "Prioritize ones with recent digital transformation announcements"

[AI adds signal filter]

Output: Perfectly targeted list, one pass

Competitive Intelligence

Without mid-turn steering:

"Research what Competitor X announced this quarter"

[AI researches]

Output: Product updates, funding news, executive hires

You: "I need their pricing changes and new integrations specifically"

[Start over]

With mid-turn steering:

"Research what Competitor X announced this quarter"

[AI starts researching]

You (mid-turn): "Focus on pricing and integrations only"

[AI narrows scope]

You (mid-turn): "Compare their new HubSpot integration to ours"

[AI adds competitive angle]

Output: Actionable competitive intel

GPT-5.3 vs previous versions showing 25% speed improvement with benchmark visualization

Practical Applications for GTM Teams

1. Real-Time Report Building

Instead of specifying every detail upfront, collaborate:

// Start the report
const session = await codex.startTask(`
Generate a weekly pipeline report for the executive team.
Include: stage progression, new opportunities, closed deals.
`);

// Steer as it works
await session.steer("Add win/loss reasons for closed deals");
await session.steer("Break down new opps by source");
await session.steer("Highlight any deals that skipped stages");

// Get final output
const report = await session.complete();

2. Dynamic Territory Planning

const session = await codex.startTask(`
Rebalance sales territories based on Q1 performance data.
`);

// Adjust criteria in real-time
await session.steer("Account for the new Austin rep starting Monday");
await session.steer("Keep enterprise accounts with existing reps");
await session.steer("Show me the impact on each rep's quota");

const territories = await session.complete();

3. Personalized Outreach at Scale

const session = await codex.startTask(`
Generate personalized emails for 50 conference attendees.
`);

// Refine the approach
await session.steer("Make them shorter - 3 sentences max");
await session.steer("Reference specific sessions they attended");
await session.steer("Skip anyone who's already a customer");

const emails = await session.complete();

4. Live Deal Analysis

const session = await codex.startTask(`
Analyze the Acme Corp opportunity and recommend next steps.
`);

// Add context as you think of it
await session.steer("They mentioned budget concerns in the last call");
await session.steer("Their competitor just signed with us");
await session.steer("The CFO is the real decision maker, not the VP");

const analysis = await session.complete();

The Technical Advantage

How Mid-Turn Steering Works

GPT-5.3-Codex maintains a live working context that you can modify:

┌─────────────────────────────────────┐
│ WORKING CONTEXT │
├─────────────────────────────────────┤
│ Original prompt │
│ + Steering input 1 │
│ + Steering input 2 │
│ + Current progress state │
│ + Intermediate results │
└─────────────────────────────────────┘

[Continues work with
full accumulated context]

Previous models would lose intermediate work when you interrupted. GPT-5.3 preserves everything and integrates your steering naturally.

Speed Improvements

The 25% speed improvement compounds with steering:

TaskGPT-5.2 (No Steering)GPT-5.3 (With Steering)Total Improvement
Pipeline report180s + 120s redo140s (steered)53% faster
Lead list (50)90s + 60s redo70s (steered)46% faster
Competitive brief120s + 90s redo95s (steered)55% faster
Territory rebalance240s + 180s redo180s (steered)57% faster

The real win isn't raw speed—it's eliminating the redo cycle.

Implementation Patterns

Pattern 1: Progressive Refinement

Start broad, narrow down:

async function buildTargetList(criteria) {
const session = await codex.startTask(`
Build a target account list matching: ${criteria.initial}
`);

// Watch progress and refine
session.onProgress(async (progress) => {
if (progress.accounts > 100) {
await session.steer("Limit to top 50 by revenue");
}
if (progress.includesCompetitorCustomers) {
await session.steer("Exclude known competitor customers");
}
});

return session.complete();
}

Pattern 2: Exception Handling

Catch issues before they compound:

async function analyzeDeals(pipeline) {
const session = await codex.startTask(`
Analyze pipeline health for Q1 forecast.
`);

// Handle edge cases as they appear
session.onAnomaly(async (anomaly) => {
if (anomaly.type === 'missing_data') {
await session.steer(`Skip ${anomaly.deal} - incomplete record`);
}
if (anomaly.type === 'outlier') {
await session.steer(`Flag ${anomaly.deal} for manual review`);
}
});

return session.complete();
}

Pattern 3: Collaborative Building

Multiple stakeholders contribute:

async function buildForecast() {
const session = await codex.startTask(`
Generate Q2 revenue forecast based on current pipeline.
`);

// Sales leader input
await session.steer("Use 60% close rate for enterprise, not 40%");

// Finance input
await session.steer("Apply 10% churn assumption to renewals");

// CEO input
await session.steer("Add scenario for if the big deal slips");

return session.complete();
}

Pattern 4: Learning Loop

Capture steering patterns for future automation:

async function buildWithLearning(task, userId) {
const session = await codex.startTask(task);
const steerings = [];

session.onSteer((input) => {
steerings.push({
trigger: session.currentState(),
steering: input,
userId: userId
});
});

const result = await session.complete();

// Store patterns for future prompts
await saveSteerings(task.type, steerings);

return result;
}

Getting Started with Codex

Installation

npm install -g @openai/codex
codex auth login

Basic Steering Example

const { Codex } = require('@openai/codex');

const codex = new Codex({ model: 'gpt-5.3-codex' });

async function steerableTask() {
const session = await codex.createSession();

// Start task
await session.send(`
Analyze our CRM data and identify upsell opportunities.
Data source: HubSpot
`);

// Wait for initial processing
await session.waitForProgress(0.3); // 30% complete

// Steer based on early results
const preliminary = await session.getProgress();
if (preliminary.includesSmallAccounts) {
await session.steer("Focus on accounts with ARR > $50K only");
}

// Wait for more progress
await session.waitForProgress(0.7); // 70% complete

// Final refinement
await session.steer("Rank by expansion likelihood, not just ARR");

// Get final output
return session.complete();
}

Common Steering Scenarios

Scenario: Report Is Too Long

Steer: "Summarize to one page, keep only top 5 items per section"

Scenario: Missing Context

Steer: "The deal values are in EUR, convert to USD using 1.08"

Scenario: Wrong Focus

Steer: "This is for the board, focus on strategic metrics not operational"

Scenario: Data Quality Issue

Steer: "Ignore any records from before January 2025, data is unreliable"

Scenario: Stakeholder Request

Steer: "CFO wants to see margin impact, add that column"
Free Tool

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

The Competitive Edge

Mid-turn steering gives you a compounding advantage:

  1. Faster iteration - No restart penalty for course corrections
  2. Better outputs - Human judgment applied at the right moments
  3. Lower frustration - No more "that's not what I meant" loops
  4. Captured knowledge - Steering patterns become future automation

Your competitors are still in prompt → wait → redo → wait cycles. You're collaborating with AI in real-time.

That efficiency gap compounds across every task, every day, every deal.


Ready to see AI-powered sales ops in action? Book a demo to see how MarketBetter leverages the latest AI capabilities for GTM teams.

Related reading:

Build an Inbound Lead Qualification Bot with OpenClaw [2026 Guide]

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

A website visitor fills out your "Book a Demo" form at 11 PM. Your SDR sees it at 9 AM. By then, they've already talked to two competitors.

Speed-to-lead is the #1 predictor of conversion. Respond within 5 minutes and you're 21x more likely to qualify the lead than if you wait 30 minutes.

But you can't afford to have SDRs working 24/7. So what do you do?

You build a qualification bot that:

  • Responds instantly to every inbound lead
  • Asks the right qualifying questions
  • Scores leads based on your ICP
  • Books meetings directly on your AE's calendar
  • Passes unqualified leads to nurture sequences

And with OpenClaw, you can build this for free.

Inbound lead form flowing to AI qualification to calendar booking

Why Most Chatbots Fail at Lead Qualification

Before we build, let's understand why existing solutions fall short:

Drift, Intercom, Qualified: The $$$$ Problem

These tools work. But they cost $10K-50K+ per year. For early-stage B2B companies, that's your entire marketing budget.

Generic Chatbot Builders: The Dumb Bot Problem

Tools like Chatfuel or ManyChat are designed for e-commerce FAQs, not B2B qualification. They follow rigid scripts and can't handle nuanced sales conversations.

DIY with GPT: The Context Problem

Building a chatbot with raw GPT API calls works—until you need it to remember the conversation, access your CRM, and actually book meetings.

OpenClaw solves all three:

  • Free and open source (no $50K/year)
  • Claude's reasoning handles nuanced conversations
  • Built-in memory, CRM integrations, and calendar access

The Architecture

Here's what we're building:

[Website Chat Widget]

[Webhook to OpenClaw]

[Lead Qualification Agent]

┌───┴───┐
↓ ↓
[Qualified] [Nurture]
↓ ↓
[Book Meeting] [Add to Sequence]

OpenClaw agent processing lead qualification with scoring

The agent:

  1. Receives the initial form data
  2. Engages in real-time chat to qualify
  3. Scores against your ICP criteria
  4. Routes to booking or nurture based on score

Prerequisites

  • OpenClaw installed and running (see setup guide)
  • A website with a chat widget (we'll use Crisp, but any webhook-capable chat works)
  • HubSpot or your CRM of choice
  • Calendly or Cal.com for booking

Step 1: Define Your Qualification Framework

First, write down your ICP criteria. Example for a B2B SaaS selling to sales teams:

# qualification-framework.yaml
qualification_criteria:
must_have:
- company_size: "50+ employees"
- role: ["VP Sales", "Director Sales", "Head of Sales", "SDR Manager", "RevOps"]
- budget_authority: true

nice_to_have:
- industry: ["SaaS", "Tech", "B2B Services"]
- current_tools: ["Outreach", "Salesloft", "Apollo", "HubSpot"]
- pain_points: ["SDR efficiency", "lead quality", "pipeline", "personalization"]

disqualifiers:
- company_size: "<20 employees"
- role: ["Intern", "Student", "Job Seeker"]
- intent: ["Competitor research", "Job inquiry"]

scoring:
qualified_threshold: 70
weights:
company_size: 25
role_match: 25
budget_authority: 20
pain_fit: 15
timeline: 15

Step 2: Create the OpenClaw Agent

Create your qualification agent configuration:

# agents/lead-qualifier.yaml
name: LeadQualifier
description: Qualifies inbound leads and books meetings for qualified prospects

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

system_prompt: |
You are an AI sales development representative for MarketBetter.
Your job is to qualify inbound leads and book meetings with our sales team.

## YOUR PERSONALITY
- Friendly but professional
- Curious about their challenges (not interrogating)
- Helpful even if they're not a fit
- Never pushy or aggressive

## QUALIFICATION CRITERIA
MUST HAVE:
- Company has 50+ employees
- They're in a sales leadership role (VP, Director, Manager, RevOps)
- They have authority to evaluate new tools

NICE TO HAVE:
- B2B company
- Currently using sales tools (Outreach, Salesloft, Apollo)
- Active pain around SDR efficiency or lead quality

DISQUALIFIERS:
- Fewer than 20 employees
- Students/job seekers/interns
- Competitor research (handle gracefully)

## SCORING
Award points (0-100 total):
- Company size match: 25 pts
- Role match: 25 pts
- Budget authority: 20 pts
- Pain fit: 15 pts
- Timeline (< 3 months): 15 pts

Qualified = 70+ points

## CONVERSATION FLOW
1. Thank them for reaching out
2. Ask what prompted them to check out MarketBetter today
3. Learn about their current setup (team size, tools, challenges)
4. Probe on specific pain points naturally
5. If qualified (70+), offer to book a meeting
6. If not qualified, offer relevant resources and add to nurture

## TOOLS AVAILABLE
- check_company(domain) - Get company info from Clearbit
- calculate_score(criteria) - Calculate qualification score
- book_meeting(email, name) - Send Calendly link and create HubSpot contact
- add_to_nurture(email, reason) - Add to email nurture sequence

## KEY BEHAVIORS
- Don't ask more than 3 questions in a row without providing value
- If they mention a specific pain, acknowledge it specifically
- Never say "I'm just a bot" - you're an SDR named Alex
- If you're not sure about something, ask

tools:
- name: check_company
description: Enriches company data from their domain
endpoint: /api/clearbit/company

- name: book_meeting
description: Creates HubSpot contact and sends Calendly link
endpoint: /api/hubspot/book

- name: add_to_nurture
description: Adds lead to nurture sequence
endpoint: /api/hubspot/nurture

Step 3: Build the Tool Endpoints

Create the API endpoints that your agent can call:

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

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

async function bookMeeting(email, name, qualificationData) {
// Create or update contact in HubSpot
const contact = await hubspot.crm.contacts.basicApi.create({
properties: {
email,
firstname: name.split(' ')[0],
lastname: name.split(' ').slice(1).join(' '),
lead_source: 'Inbound Chat',
qualification_score: qualificationData.score,
qualification_notes: qualificationData.notes,
lifecyclestage: 'salesqualifiedlead'
}
});

// Generate Calendly link with pre-filled info
const calendlyLink = `https://calendly.com/your-team/demo?name=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`;

// Log activity in HubSpot
await hubspot.crm.timeline.eventsApi.create({
eventTemplateId: 'chat_qualified',
objectId: contact.id,
tokens: {
score: qualificationData.score,
summary: qualificationData.notes
}
});

return {
calendlyLink,
contactId: contact.id,
message: `Great! Here's a link to book directly with our team: ${calendlyLink}`
};
}

module.exports = { bookMeeting };
// api/clearbit/company.js
const Clearbit = require('clearbit')(process.env.CLEARBIT_KEY);

async function checkCompany(domain) {
try {
const company = await Clearbit.Company.find({ domain });

return {
name: company.name,
size: company.metrics?.employees || 'Unknown',
industry: company.category?.industry || 'Unknown',
description: company.description,
tech: company.tech || [],
sizeCategory: categorizeSiz(company.metrics?.employees)
};
} catch (error) {
return {
name: domain,
size: 'Unknown',
industry: 'Unknown',
sizeCategory: 'Unknown'
};
}
}

function categorizeSize(employees) {
if (!employees) return 'Unknown';
if (employees < 20) return 'too_small';
if (employees < 50) return 'small';
if (employees < 200) return 'mid_market';
if (employees < 1000) return 'enterprise';
return 'large_enterprise';
}

module.exports = { checkCompany };

Step 4: Connect Your Chat Widget

Most chat widgets support webhooks. Here's how to connect Crisp:

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

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

// Crisp webhook endpoint
app.post('/webhooks/crisp', async (req, res) => {
const { event, data } = req.body;

if (event === 'message:send') {
// Visitor sent a message
const { website_id, session_id, content } = data;

// Get or create OpenClaw session for this chat
const session = await openclaw.getSession(`crisp:${session_id}`);

// Process message through qualification agent
const response = await session.send(content, {
agent: 'lead-qualifier',
metadata: {
source: 'crisp',
website_id,
visitor_data: data.visitor
}
});

// Send response back to Crisp
await sendCrispMessage(website_id, session_id, response.message);
}

res.sendStatus(200);
});

async function sendCrispMessage(websiteId, sessionId, message) {
await fetch(`https://api.crisp.chat/v1/website/${websiteId}/conversation/${sessionId}/message`, {
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(CRISP_ID + ':' + CRISP_KEY).toString('base64')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'text',
content: message,
from: 'operator'
})
});
}

Step 5: Sample Conversation

Here's what a qualification conversation looks like:

Visitor: Hi, I'm interested in learning more about MarketBetter

Bot (Alex): Hey! Thanks for reaching out 👋 I'm Alex from the MarketBetter team. What got you interested in checking us out today?

Visitor: We're looking for ways to make our SDR team more efficient. Currently spending too much time on manual research.

Bot: Oh yeah, manual research is a killer. How big is your SDR team currently?

Visitor: We have 8 SDRs. They probably spend 2-3 hours a day just researching accounts before outreach.

Bot: [Internal: check_company called for visitor domain → 150 employees, SaaS industry]

150-person SaaS company with 8 SDRs doing 2-3 hours of research each—that's basically 2 full headcounts worth of time just on research. Are you all using any tools for that currently, or mostly LinkedIn + Google?

Visitor: We have ZoomInfo for data but the research part is still manual. We tried Apollo's sequences but the personalization was too generic.

Bot: Makes sense—data is table stakes, but the what to do with it part is where most tools fall short.

Quick question: are you the one evaluating tools for the team, or is there someone else involved in that?

Visitor: I'm the VP of Sales, so yeah, my call.

Bot: [Internal: Score calculation → Company: 25, Role: 25, Pain: 15, Authority: 20, Timeline: TBD = 85+ points]

Perfect. Based on what you're describing—8 SDRs, manual research eating into selling time, need for real personalization—this is exactly what MarketBetter was built for.

Would you be up for a 20-minute call with one of our sales engineers? They can show you specifically how teams like yours are cutting that research time down to like 20 minutes a day per rep.

Visitor: Sure, that sounds good.

Bot: Awesome! Here's a link to grab time directly: [Calendly link]

I'll also send you a quick case study from a similar team (B2B SaaS, 10 SDRs) that saw 3x more meetings after switching. Look for it in your inbox!


Result: Qualified lead, meeting booked, case study sent—all within 4 minutes at 11 PM on a Sunday.

Speed comparison showing qualified leads booking fast versus delays

Advanced: Handling Edge Cases

The "Just Browsing" Visitor

# In your agent prompt
If visitor seems early-stage or "just browsing":
1. Don't push for a meeting
2. Offer a relevant resource (ebook, case study, calculator)
3. Ask if they'd like occasional updates
4. Add to top-of-funnel nurture

The Competitor Researcher

# In your agent prompt
If you suspect competitor research:
1. Don't be defensive or hostile
2. Offer publicly available info
3. Politely suggest they book a demo if genuinely interested
4. Log in CRM as "Competitor - [Company Name]"

The Unqualified But Enthusiastic

# In your agent prompt
If they're excited but don't meet criteria (too small, wrong role):
1. Be honest: "We typically work with teams of 5+ SDRs..."
2. Offer free resources that might help them
3. Suggest checking back when they scale
4. Add to nurture for later

Metrics to Track

Once live, monitor these:

MetricTargetWhy It Matters
Response time<30 secondsSpeed-to-lead
Qualification accuracy>85%Are meetings actually qualified?
Conversion to meeting>40% of qualifiedBot effectiveness
No-show rate<20%Lead quality
Pipeline from botTrack $ROI

The Cost Breakdown

ComponentCost
OpenClawFree (self-hosted)
Claude API~$0.01-0.05 per conversation
Crisp (chat widget)$25/month (Starter)
Clearbit (optional)$99/month
Total~$125/month

Compare that to Drift ($2,500/month minimum) or Qualified ($10,000/month+).

Why This Beats Human SDRs for First Response

Let me be clear: this doesn't replace SDRs. It augments them.

Bot does:

  • 24/7 instant response
  • Consistent qualification questions
  • Zero fatigue or bad days
  • Perfect CRM hygiene
  • Immediate scoring and routing

SDRs do:

  • Complex objection handling
  • Relationship building
  • Creative problem-solving
  • Negotiation
  • Closing

The bot handles the 80% of conversations that are straightforward so your SDRs can focus on the 20% that need human touch.

Taking It Live

  1. Start with a pilot — Run bot alongside human SDRs for 2 weeks
  2. Review conversations — Check for failure patterns
  3. Iterate the prompt — Refine based on real conversations
  4. Measure conversion — Compare bot-qualified vs. human-qualified
  5. Scale up — Expand to more pages/higher traffic

Free Tool

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

Ready to Never Miss a Lead Again?

MarketBetter's AI SDR playbook takes this further—not just qualifying inbound, but telling your entire team exactly who to contact and what to say, every single day.

Book a demo →


Related reading:

Multi-CRM Sync Automation with Codex: HubSpot + Salesforce Unified [2026]

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

Your company runs HubSpot for marketing. Sales uses Salesforce. The two don't talk.

Every week, someone manually exports contacts from HubSpot, cleans the data in Excel, and imports to Salesforce. Deals created in Salesforce never make it back to HubSpot. Marketing can't see which MQLs became opportunities.

The result: Marketing thinks their campaigns are working. Sales thinks marketing sends garbage leads. Nobody can prove anything because the data lives in two places.

Sound familiar? You're not alone. This is one of the most common problems in B2B GTM.

Enterprise solutions like Workato, Tray.io, or HubSpot's native Salesforce sync cost $20K-50K per year and still require massive configuration.

What if you could build a smarter sync for $100/month?

HubSpot and Salesforce connected through AI orchestration layer

The Problem with Traditional Sync Tools

Native HubSpot-Salesforce Sync

HubSpot offers a native Salesforce integration. It works... sort of. Problems:

  • One-way sync for many fields
  • Limited conflict resolution
  • No custom logic for edge cases
  • Breaks when you have custom objects

iPaaS Tools (Workato, Tray, Zapier)

These are better but:

  • $20K-50K+/year for enterprise plans
  • Complex visual builders that become unmaintainable
  • No intelligence—just "if this, then that"
  • Can't handle nuanced business rules

The Real Challenge: Business Logic

The hard part isn't syncing fields. It's answering questions like:

  • If a contact exists in both systems with different emails, which is correct?
  • When a deal stage changes in Salesforce, how does that map to HubSpot lifecycle stages?
  • If marketing updates a lead score in HubSpot, should it overwrite the Salesforce score?
  • How do you handle records created by integrations vs. humans?

This is where AI shines.

The Architecture

Here's what we're building:

[HubSpot Webhook] ←→ [Sync Engine] ←→ [Salesforce Webhook]

[Codex Agent]

[Conflict Resolution]

[Bidirectional Updates]

The Codex agent handles:

  1. Field mapping — Translates between CRM schemas
  2. Conflict resolution — Decides which record is authoritative
  3. Validation — Ensures data quality before sync
  4. Logging — Tracks every change for debugging

AI agent resolving CRM data conflicts

Prerequisites

  • OpenAI Codex CLI installed
  • HubSpot API key (Private App)
  • Salesforce Connected App credentials
  • Node.js 18+
  • A database for sync state (PostgreSQL recommended)

Step 1: Define Your Sync Map

First, document how fields map between systems:

// config/field-map.js
module.exports = {
contact: {
// HubSpot field -> Salesforce field
'email': 'Email',
'firstname': 'FirstName',
'lastname': 'LastName',
'phone': 'Phone',
'company': 'Company',
'jobtitle': 'Title',
'lifecyclestage': 'Status__c', // Custom field
'hs_lead_score': 'Lead_Score__c',

// Complex mappings
'hs_analytics_source': {
salesforce_field: 'LeadSource',
transform: (value) => {
const sourceMap = {
'ORGANIC_SEARCH': 'Web',
'PAID_SEARCH': 'Paid Search',
'SOCIAL_MEDIA': 'Social',
'EMAIL_MARKETING': 'Email',
'DIRECT_TRAFFIC': 'Direct',
'REFERRALS': 'Referral'
};
return sourceMap[value] || 'Other';
}
}
},

deal: {
'dealname': 'Name',
'amount': 'Amount',
'closedate': 'CloseDate',
'dealstage': {
salesforce_field: 'StageName',
transform: (stage) => {
const stageMap = {
'appointmentscheduled': 'Discovery',
'qualifiedtobuy': 'Qualification',
'presentationscheduled': 'Demo',
'decisionmakerboughtin': 'Proposal',
'contractsent': 'Negotiation',
'closedwon': 'Closed Won',
'closedlost': 'Closed Lost'
};
return stageMap[stage] || 'Unknown';
}
}
},

company: {
'name': 'Name',
'domain': 'Website',
'industry': 'Industry',
'numberofemployees': 'NumberOfEmployees',
'annualrevenue': 'AnnualRevenue'
}
};

Step 2: Build the Sync Engine Core

// lib/sync-engine.js
const HubSpot = require('@hubspot/api-client');
const jsforce = require('jsforce');
const fieldMap = require('../config/field-map');

class SyncEngine {
constructor() {
this.hubspot = new HubSpot.Client({ accessToken: process.env.HUBSPOT_TOKEN });
this.salesforce = new jsforce.Connection({
loginUrl: process.env.SF_LOGIN_URL,
accessToken: process.env.SF_ACCESS_TOKEN,
instanceUrl: process.env.SF_INSTANCE_URL
});
this.syncState = new SyncStateDB(); // Your state store
}

async syncHubSpotToSalesforce(objectType, hubspotId) {
// Get HubSpot record
const hsRecord = await this.getHubSpotRecord(objectType, hubspotId);

// Check if already synced
const existingSfId = await this.syncState.getSalesforceId(objectType, hubspotId);

if (existingSfId) {
// Update existing record
return this.updateSalesforceRecord(objectType, existingSfId, hsRecord);
} else {
// Find or create in Salesforce
const sfRecord = await this.findSalesforceMatch(objectType, hsRecord);

if (sfRecord) {
// Link existing and update
await this.syncState.linkRecords(objectType, hubspotId, sfRecord.Id);
return this.updateSalesforceRecord(objectType, sfRecord.Id, hsRecord);
} else {
// Create new
return this.createSalesforceRecord(objectType, hsRecord, hubspotId);
}
}
}

async findSalesforceMatch(objectType, hsRecord) {
// Use email for contacts, domain for companies, etc.
if (objectType === 'contact') {
const email = hsRecord.properties.email;
if (!email) return null;

const results = await this.salesforce.sobject('Contact')
.find({ Email: email })
.limit(1);

return results[0] || null;
}

if (objectType === 'company') {
const domain = hsRecord.properties.domain;
if (!domain) return null;

const results = await this.salesforce.sobject('Account')
.find({ Website: { $like: `%${domain}%` } })
.limit(1);

return results[0] || null;
}

return null;
}

transformHubSpotToSalesforce(objectType, hsProperties) {
const mapping = fieldMap[objectType];
const sfFields = {};

for (const [hsField, sfConfig] of Object.entries(mapping)) {
const value = hsProperties[hsField];
if (value === undefined || value === null) continue;

if (typeof sfConfig === 'string') {
// Simple mapping
sfFields[sfConfig] = value;
} else {
// Complex mapping with transform
sfFields[sfConfig.salesforce_field] = sfConfig.transform(value);
}
}

return sfFields;
}
}

module.exports = { SyncEngine };

Step 3: Intelligent Conflict Resolution with Codex

Here's where the AI magic comes in. When records conflict, Codex decides:

// lib/conflict-resolver.js
const { Codex } = require('@openai/codex');

const codex = new Codex({ apiKey: process.env.OPENAI_API_KEY });

async function resolveConflict(hubspotRecord, salesforceRecord, fieldName) {
const prompt = `You are a CRM data quality expert. Resolve this field conflict:

FIELD: ${fieldName}

HUBSPOT VALUE:
Value: ${hubspotRecord[fieldName]}
Last Modified: ${hubspotRecord.updatedAt}
Modified By: ${hubspotRecord.updatedBy || 'Unknown'}
Source: ${hubspotRecord.source || 'Unknown'}

SALESFORCE VALUE:
Value: ${salesforceRecord[fieldName]}
Last Modified: ${salesforceRecord.LastModifiedDate}
Modified By: ${salesforceRecord.LastModifiedById || 'Unknown'}

BUSINESS RULES:
1. More recent updates generally win
2. Human edits beat automated updates
3. Sales-owned fields (title, phone) prefer Salesforce
4. Marketing-owned fields (lead source, score) prefer HubSpot
5. Email should match—if different, flag for review

OUTPUT FORMAT:
{
"winner": "hubspot" | "salesforce" | "review_needed",
"reason": "brief explanation",
"confidence": 0.0-1.0
}`;

const response = await codex.complete({
model: 'gpt-5.3-codex',
prompt,
max_tokens: 200,
response_format: { type: 'json_object' }
});

return JSON.parse(response.choices[0].text);
}

async function resolveRecordConflicts(hsRecord, sfRecord, mapping) {
const resolutions = {};
const reviewNeeded = [];

for (const [hsField, sfConfig] of Object.entries(mapping)) {
const sfField = typeof sfConfig === 'string' ? sfConfig : sfConfig.salesforce_field;

const hsValue = hsRecord.properties[hsField];
const sfValue = sfRecord[sfField];

// Skip if same
if (hsValue === sfValue) {
resolutions[hsField] = { value: hsValue, source: 'same' };
continue;
}

// Skip if one is empty
if (!hsValue && sfValue) {
resolutions[hsField] = { value: sfValue, source: 'salesforce' };
continue;
}
if (hsValue && !sfValue) {
resolutions[hsField] = { value: hsValue, source: 'hubspot' };
continue;
}

// Conflict! Let Codex decide
const resolution = await resolveConflict(
{ [hsField]: hsValue, updatedAt: hsRecord.updatedAt },
{ [sfField]: sfValue, LastModifiedDate: sfRecord.LastModifiedDate },
hsField
);

if (resolution.winner === 'review_needed') {
reviewNeeded.push({
field: hsField,
hubspotValue: hsValue,
salesforceValue: sfValue,
reason: resolution.reason
});
} else {
resolutions[hsField] = {
value: resolution.winner === 'hubspot' ? hsValue : sfValue,
source: resolution.winner,
confidence: resolution.confidence,
reason: resolution.reason
};
}
}

return { resolutions, reviewNeeded };
}

module.exports = { resolveConflict, resolveRecordConflicts };

Step 4: Webhook Handlers

Set up webhooks for real-time sync:

// webhooks/hubspot.js
const express = require('express');
const { SyncEngine } = require('../lib/sync-engine');
const { resolveRecordConflicts } = require('../lib/conflict-resolver');

const router = express.Router();
const syncEngine = new SyncEngine();

router.post('/hubspot', async (req, res) => {
const events = req.body;

for (const event of events) {
const { subscriptionType, objectId, propertyName, propertyValue } = event;

// Handle contact updates
if (subscriptionType === 'contact.propertyChange') {
console.log(`📥 HubSpot contact ${objectId} updated: ${propertyName}`);

try {
await syncEngine.syncHubSpotToSalesforce('contact', objectId);
console.log(`✅ Synced to Salesforce`);
} catch (error) {
console.error(`❌ Sync failed: ${error.message}`);
// Queue for retry
await retryQueue.add({ type: 'contact', id: objectId, source: 'hubspot' });
}
}

// Handle deal updates
if (subscriptionType === 'deal.propertyChange') {
console.log(`📥 HubSpot deal ${objectId} updated: ${propertyName}`);
await syncEngine.syncHubSpotToSalesforce('deal', objectId);
}
}

res.sendStatus(200);
});

module.exports = router;
// webhooks/salesforce.js
const express = require('express');
const { SyncEngine } = require('../lib/sync-engine');

const router = express.Router();
const syncEngine = new SyncEngine();

// Salesforce Outbound Message handler
router.post('/salesforce', async (req, res) => {
// Parse SOAP envelope (Salesforce uses SOAP for outbound messages)
const notification = parseOutboundMessage(req.body);

const { objectType, recordId, changedFields } = notification;

console.log(`📥 Salesforce ${objectType} ${recordId} updated`);

try {
await syncEngine.syncSalesforceToHubSpot(objectType, recordId);
console.log(`✅ Synced to HubSpot`);
} catch (error) {
console.error(`❌ Sync failed: ${error.message}`);
}

// Return ACK
res.type('text/xml').send(`
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<notificationsResponse xmlns="http://soap.sforce.com/2005/09/outbound">
<Ack>true</Ack>
</notificationsResponse>
</soapenv:Body>
</soapenv:Envelope>
`);
});

module.exports = router;

Step 5: Sync Dashboard

Build a simple UI to monitor syncs:

// api/sync-status.js
router.get('/status', async (req, res) => {
const stats = await db.query(`
SELECT
object_type,
COUNT(*) as total_synced,
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful,
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
COUNT(CASE WHEN status = 'pending_review' THEN 1 END) as pending_review,
MAX(synced_at) as last_sync
FROM sync_log
WHERE synced_at > NOW() - INTERVAL '24 hours'
GROUP BY object_type
`);

const conflicts = await db.query(`
SELECT *
FROM sync_conflicts
WHERE resolved = false
ORDER BY created_at DESC
LIMIT 50
`);

res.json({
stats,
conflicts,
health: conflicts.length === 0 ? 'healthy' : 'needs_attention'
});
});

Cost comparison: DIY sync vs enterprise iPaaS tools

Cost Comparison

SolutionMonthly CostFeatures
Native HubSpot-SF SyncIncludedBasic, one-way many fields
Workato$2,000-5,000Full iPaaS, complex workflows
Tray.io$1,500-3,000Visual builder, good for non-devs
Syncari$1,000-2,500CRM-focused, good conflict resolution
DIY with Codex$50-150Full control, AI conflict resolution

The DIY approach costs ~95% less while giving you MORE intelligence in conflict resolution.

When This Makes Sense

Build your own when:

  • You have engineering resources
  • Your sync logic is complex/custom
  • Cost is a factor
  • You want full control and ownership

Use enterprise tools when:

  • No engineering bandwidth
  • Need compliance certifications
  • Want vendor support
  • Syncing many systems (not just 2)

Mid-Turn Steering for Bulk Syncs

When doing initial bulk sync (thousands of records), use Codex's mid-turn steering:

# Start bulk sync
codex run bulk-sync --source hubspot --target salesforce --object contacts

# Mid-run, adjust conflict rules
codex steer "For company size conflicts, prefer Salesforce values since sales updates those"

# Or pause problematic records
codex steer "Skip records from marketing@example.com domain - those are test records"

This lets you fine-tune the sync without starting over.

Common Gotchas

  1. Infinite loops — Mark records as "sync in progress" to prevent HubSpot→SF→HubSpot cycles
  2. Rate limits — Both APIs have limits; implement exponential backoff
  3. Timezone hell — Store all dates in UTC, convert on display
  4. Lookup fields — Sync Account before Contact (parent before child)
  5. Deleted records — Decide: soft delete or hard sync?

Free Tool

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

Want Unified Data Without the Sync Headaches?

MarketBetter aggregates signals from across your stack—CRM, website, email, intent—into one unified view. No more wondering which system has the truth.

Book a demo →


Related reading:

Automate Event & Webinar Lead Follow-Up with OpenClaw [2026]

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

You ran a webinar. 500 people registered. 200 attended. Now comes the hard part: following up with every single lead before they forget who you are.

Most teams send a generic "Thanks for attending" email and call it a day. The leads go cold. The webinar ROI tanks.

Here's how to build an automated follow-up system with OpenClaw that scores attendees, sends personalized sequences, and books meetings on autopilot.

Webinar lead follow-up automation

Why Webinar Follow-Up Fails

The math is brutal:

  • Within 24 hours: Lead interest drops 50%
  • Within 48 hours: Lead interest drops 80%
  • After 72 hours: You're basically cold calling again

Most teams don't even start follow-up until 48 hours post-event. By then, attendees have forgotten the content and moved on.

The solution isn't "follow up faster." The solution is "follow up instantly and intelligently."

The OpenClaw Event Follow-Up Architecture

OpenClaw runs 24/7, which makes it perfect for event automation. Here's the system:

Component 1: Attendee Scoring Agent

Not all attendees are equal. Before sending any follow-up, score each lead:

Scoring Criteria:

SignalPoints
Attended live (vs. replay)+20
Stayed >75% of session+15
Asked a question+25
Clicked poll/CTA during webinar+15
Visited pricing page after+30
Downloaded resources+10
Already in CRM as lead/opportunity+20
ICP company size+10-25
ICP industry+10-25

Score Tiers:

  • Hot (80+): Immediate SDR outreach + personalized email
  • Warm (50-79): Automated nurture sequence with meeting CTA
  • Cool (20-49): Content nurture, resurface for next event
  • Cold (<20): Newsletter only

Component 2: The Follow-Up Sequences

Hot Lead Sequence (OpenClaw executes automatically):

T+0 (immediately post-webinar):
- Email: "Thanks for your question about [specific topic]"
- Slack alert to assigned SDR
- Calendar hold suggestion for rep

T+4 hours:
- If no rep action: Send meeting link email
- Include relevant case study based on their industry

T+24 hours:
- LinkedIn connection request with personalized note
- Reference their company and webinar topic

T+48 hours:
- If no meeting booked: Rep phone call task
- Email: "Did our [topic] discussion answer your questions?"

Warm Lead Sequence:

T+0:
- Email: "Here's the [webinar] recording and key takeaways"
- Include personalized insight based on their role

T+24 hours:
- Email: Related blog post or case study
- Soft meeting CTA

T+72 hours:
- Email: "3 things you might have missed" with timestamps
- Direct meeting link CTA

T+7 days:
- Email: "Other [persona] found this valuable"
- Social proof + meeting CTA

Event lead scoring workflow

Component 3: OpenClaw Configuration

Here's how to set this up in OpenClaw:

# openclaw.yaml
agents:
event-followup:
model: claude-sonnet-4-20250514
schedule:
- cron: "*/15 * * * *" # Check every 15 minutes

context:
- path: /context/webinar-templates.md
- path: /context/scoring-rules.md
- path: /context/company-voice.md

integrations:
- hubspot:
lists:
- webinar-attendees-feb-2026
actions:
- create_contact
- send_email
- create_task

- slack:
channel: "#sales-alerts"
alerts: true

- calendar:
check_availability: true
suggest_times: true

memory:
- attendee-interactions.md
- sequence-progress.md

The agent checks for new webinar registrations and attendees every 15 minutes, scores them, and initiates the appropriate sequence.

Component 4: Personalization Engine

Generic follow-ups get ignored. OpenClaw personalizes each touchpoint:

For the "Thanks for your question" email:

  1. Pull the attendee's actual question from webinar Q&A
  2. Reference their company's situation (from enrichment data)
  3. Connect their question to a relevant feature or case study
  4. Include a specific insight they might have missed

For the case study selection:

  1. Match attendee's industry to available case studies
  2. Match their company size tier
  3. Match their likely pain point (inferred from webinar topic + questions)

For the LinkedIn connection:

  1. Reference a specific moment from the webinar
  2. Mention something from their LinkedIn profile
  3. Keep it casual, not salesy

Real Example: SaaS Company's Results

A B2B SaaS client implemented this system for their monthly product webinars:

Before (manual follow-up):

  • Follow-up start: 48-72 hours post-event
  • Emails sent: Generic blast to all attendees
  • Meetings booked: 3-5 per webinar
  • Pipeline generated: $15K-25K

After (OpenClaw automation):

  • Follow-up start: Immediately (within minutes)
  • Emails sent: Personalized based on engagement and ICP fit
  • Meetings booked: 18-22 per webinar
  • Pipeline generated: $85K-120K

The 4x increase in meetings came from three factors:

  1. Speed (reaching leads while interest is hot)
  2. Relevance (personalized content based on engagement)
  3. Persistence (automated multi-touch sequence that humans would abandon)

Handling No-Shows

200 people attended, but 300 registered and didn't show. Don't ignore them:

No-Show Sequence:

T+1 hour post-event:
- Email: "We missed you! Here's the recording"
- Include a 2-minute highlight reel

T+24 hours:
- Email: "The one thing everyone asked about" (teaser)
- CTA to watch a specific segment

T+3 days:
- If watched: Move to warm sequence
- If not watched: One more email with different angle

T+7 days:
- Add to general nurture
- Invite to next relevant event

No-shows registered for a reason. Some had conflicts, some forgot, some lost interest. The recording follow-up recaptures many of them.

Integration with Event Platforms

OpenClaw connects to your webinar platform via webhooks or API polling:

Zoom Webinar:

  • Webhook for attendee join/leave events
  • API for Q&A and poll responses
  • Attendee duration tracking

Webex Events:

  • Similar webhook structure
  • Engagement scoring from platform

ON24:

  • Rich engagement data via API
  • Content consumption tracking

Custom Events (in-person with badge scans):

  • Import badge scan data via CSV or API
  • Session attendance tracking
  • Booth visit recording

Advanced: Multi-Event Attribution

When someone attends multiple webinars, your follow-up should reflect that:

IF attendee.event_count > 1:
- Reference their attendance history
- "You've been exploring [topic area] with us..."
- Escalate to warmer sequence regardless of engagement score
- Suggest a consolidated conversation about their interests

OpenClaw's memory system tracks all interactions across events, so you never send "Thanks for attending your first webinar" to someone who's been to five.

Getting Started

Here's your implementation timeline:

Week 1:

  • Set up OpenClaw with your webinar platform integration
  • Create scoring criteria based on your ICP
  • Draft email templates for each tier

Week 2:

  • Build and test sequences in staging
  • Connect to CRM for contact creation and tasks
  • Set up Slack alerts for hot leads

Week 3:

  • Run with your next webinar
  • Monitor and adjust scoring thresholds
  • Refine personalization based on response rates

Week 4+:

  • Optimize sequences based on conversion data
  • Add more personalization variables
  • Expand to handle in-person events
Free Tool

Try our Conference Scraper — scrape exhibitor lists from any conference website in seconds. No signup required.

The Easier Path

OpenClaw is powerful, but there's a learning curve. If you want event follow-up automation without the setup, MarketBetter includes it out of the box.

Connect your webinar platform, set your scoring criteria, and our AI handles the rest. Personalized sequences that adapt based on engagement. Automatic meeting booking for hot leads. Complete visibility into what's working.

Running events but struggling with follow-up? Book a demo and we'll show you how to turn your next webinar into pipeline.


Related reading:

Building a Sales Territory Bot with OpenAI Codex: Automated Lead Routing That Actually Works [2026]

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

The average lead sits unassigned for 2.5 hours after hitting your CRM.

In that time, your competitor has already responded, built rapport, and scheduled a demo. And 78% of buyers go with the vendor who responds first.

Territory management is the unglamorous backbone of sales operations—and it's broken at most companies. Manual assignment, outdated territory maps, capacity blindness, and constant rep complaints about "unfair" distribution.

GPT-5.3 Codex, released just last week, changes what's possible. Here's how to build an intelligent territory bot that routes leads instantly, balances workload automatically, and adapts to your business in real-time.

Sales territory architecture with AI agent icons, territory boundaries, and lead distribution arrows

Why Traditional Territory Management Fails

Before building the solution, let's diagnose the problem:

The Manual Assignment Trap

Most companies assign territories once a year, then spend the rest of the year fighting fires:

  • Rep leaves → territory chaos for 2-4 weeks
  • New product launch → existing territories don't match buyer profile
  • Geographic expansion → manual carve-outs and reassignments
  • Lead volume spikes → some reps drowning, others starving

The "Fair" Distribution Myth

Equal territory size ≠ equal opportunity:

  • 1,000 accounts in enterprise segment ≠ 1,000 accounts in SMB
  • West Coast tech hub ≠ Midwest manufacturing
  • Fortune 500 HQ territory ≠ field office territory

Your top performers end up subsidizing poor territory design.

The Response Time Problem

When a hot lead comes in at 4:55 PM on a Friday:

  1. Round-robin assigns to rep who's OOO
  2. Lead sits until Monday
  3. Competitor responded Friday at 5:01 PM
  4. Deal lost before it started

The AI Territory Bot Architecture

Here's what we're building:

Inbound Lead → Territory Bot → Intelligent Assignment → Instant Response

[Considers:]
- Territory rules
- Rep capacity
- Lead quality score
- Time zone/availability
- Historical performance
- Current workload

Automated territory assignment workflow showing lead intake, AI analysis, and routing to correct rep

Building with GPT-5.3 Codex

The new Codex model brings three capabilities that make this project practical:

  1. 25% faster execution - Real-time routing at scale
  2. Mid-turn steering - Adjust logic while processing
  3. Multi-file context - Understands your entire territory structure

Step 1: Define Your Territory Logic

First, codify your territory rules in a format Codex can understand:

const territoryRules = {
// Geographic territories
regions: {
west: {
states: ['CA', 'WA', 'OR', 'NV', 'AZ'],
reps: ['sarah.west@company.com', 'mike.pacific@company.com'],
capacity: { sarah: 50, mike: 45 } // max active opportunities
},
midwest: {
states: ['IL', 'OH', 'MI', 'IN', 'WI'],
reps: ['john.midwest@company.com'],
capacity: { john: 60 }
}
// ... more regions
},

// Segment overrides
segments: {
enterprise: {
minEmployees: 1000,
reps: ['enterprise.team@company.com'],
override: true // takes precedence over geography
},
strategic: {
accounts: ['ACME Corp', 'Globex Inc', 'Initech'],
reps: ['vp.sales@company.com'],
override: true
}
},

// Industry specializations
industries: {
healthcare: {
reps: ['healthcare.specialist@company.com'],
override: false // falls back to geography if at capacity
}
}
};

Step 2: Build the Assignment Logic

Using Codex, generate the routing engine:

Build a lead routing function that:

1. Accepts a lead object with: company, state, employee_count, industry, source
2. Checks segment overrides first (enterprise, strategic accounts)
3. Falls back to industry specialization if applicable
4. Falls back to geographic territory
5. Within each territory, selects rep with:
- Lowest current workload (% of capacity)
- Best historical conversion rate for this lead type
- Availability (not OOO, within working hours)
6. If all reps at capacity, route to overflow queue with alert
7. Returns assigned rep + reasoning for the assignment

Handle edge cases:
- Lead matches multiple territories (use priority order)
- No reps available (queue + alert)
- Unknown state/region (default territory)

Codex generates production-ready code:

async function assignLead(lead) {
// Check strategic accounts first
if (territoryRules.segments.strategic.accounts
.includes(lead.company)) {
return assignToRep(
territoryRules.segments.strategic.reps[0],
lead,
'Strategic account override'
);
}

// Check enterprise segment
if (lead.employee_count >=
territoryRules.segments.enterprise.minEmployees) {
const rep = await findAvailableRep(
territoryRules.segments.enterprise.reps,
lead
);
if (rep) {
return assignToRep(rep, lead, 'Enterprise segment');
}
}

// Check industry specialization
if (lead.industry &&
territoryRules.industries[lead.industry]) {
const industryConfig = territoryRules.industries[lead.industry];
const rep = await findAvailableRep(industryConfig.reps, lead);
if (rep || industryConfig.override) {
return rep
? assignToRep(rep, lead, `${lead.industry} specialist`)
: queueLead(lead, 'Industry specialist at capacity');
}
}

// Geographic fallback
const region = findRegion(lead.state);
if (region) {
const rep = await findBestRep(region.reps, lead, region.capacity);
if (rep) {
return assignToRep(rep, lead, `Geographic: ${region.name}`);
}
}

// Overflow handling
return queueLead(lead, 'No available reps in territory');
}

Step 3: Add Intelligence Layer

Here's where Codex shines—adding context-aware decisions:

Enhance the routing function to consider:

1. Lead quality signals:
- Visited pricing page → higher priority
- Downloaded case study → match to relevant industry rep
- Requested demo → fastest responder

2. Rep performance matching:
- Small company leads → reps with high SMB close rates
- Technical buyers → reps with engineering backgrounds
- Fast-moving deals → reps with shortest sales cycles

3. Timing optimization:
- Route to rep whose working hours start soonest
- Consider rep's meeting schedule from calendar
- Factor in typical response time by rep

4. Fair distribution:
- Track assignments over rolling 7-day window
- Balance quality scores, not just quantity
- Flag if any rep consistently gets lower-quality leads

Step 4: Implement Mid-Turn Steering

GPT-5.3's killer feature—adjust the bot while it's working:

// During lead processing, you can steer the decision
async function assignWithSteering(lead, steeringInput = null) {
const initialAssignment = await assignLead(lead);

if (steeringInput) {
// Manager can override mid-process
// "Actually, give this to Sarah - she has context"
return applySteeringOverride(initialAssignment, steeringInput);
}

return initialAssignment;
}

In practice, this means your sales ops team can:

  • Watch assignments in real-time
  • Inject context the bot doesn't have
  • Correct routing without stopping the system

Real-World Implementation

Integration Points

Connect your territory bot to:

CRM (HubSpot/Salesforce):

// Webhook triggered on new lead
app.post('/webhooks/new-lead', async (req, res) => {
const lead = req.body;
const assignment = await assignLead(lead);

// Update CRM
await crm.updateLead(lead.id, {
owner: assignment.rep,
assignment_reason: assignment.reason,
assigned_at: new Date()
});

// Notify rep
await slack.sendMessage(assignment.rep,
`New lead assigned: ${lead.company} - ${assignment.reason}`
);

res.json({ success: true, assignment });
});

Slack Notifications:

// Real-time assignment alerts
const formatAssignmentAlert = (assignment) => ({
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: '🎯 New Lead Assigned' }
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Company:* ${assignment.lead.company}` },
{ type: 'mrkdwn', text: `*Assigned To:* ${assignment.rep}` },
{ type: 'mrkdwn', text: `*Reason:* ${assignment.reason}` },
{ type: 'mrkdwn', text: `*Quality Score:* ${assignment.lead.score}/100` }
]
},
{
type: 'actions',
elements: [
{ type: 'button', text: { type: 'plain_text', text: 'View in CRM' }, url: assignment.crmUrl },
{ type: 'button', text: { type: 'plain_text', text: 'Reassign' }, action_id: 'reassign_lead' }
]
}
]
});

Monitoring Dashboard

Track your territory bot's performance:

MetricTargetAlert Threshold
Assignment time&lt; 30 seconds> 2 minutes
Rep capacity utilization70-85%&lt; 50% or > 95%
Lead distribution fairness&lt; 10% variance> 20% variance
Overflow queue size0> 5 leads
First response time&lt; 5 minutes> 30 minutes

Advanced Patterns

Dynamic Territory Rebalancing

Build a weekly territory rebalancing report that:

1. Analyzes lead distribution over past 30 days
2. Compares conversion rates by territory
3. Identifies reps consistently at capacity
4. Identifies reps consistently underutilized
5. Suggests boundary adjustments
6. Calculates impact of proposed changes

Output as executive summary + detailed recommendations.

Predictive Capacity Planning

Using historical lead flow data, predict:

1. Expected leads per territory next week
2. Which reps will hit capacity and when
3. Recommended proactive reassignments
4. Hiring needs by territory

Factor in seasonality, marketing campaigns, and
industry trends.

Self-Healing Territories

Build a system that automatically adjusts when:

1. Rep goes OOO → redistribute to backup
2. Lead volume spikes → activate overflow handling
3. New rep onboards → gradual ramp-up schedule
4. Rep leaves → immediate territory redistribution

Log all automatic adjustments and alert management.

Results to Expect

Teams implementing AI territory bots typically see:

MetricBeforeAfterImpact
Lead response time2.5 hours4 minutes97% faster
Assignment errors15%2%87% reduction
Rep utilization variance40%12%70% fairer
Leads lost to slow response12%3%75% saved
Territory disputes/month8187% fewer

The biggest win isn't efficiency—it's predictability. When every lead routes correctly, your forecasting improves, your reps trust the system, and you stop firefighting.

Getting Started

  1. Document your current territory rules - Even if they're in someone's head
  2. Identify the edge cases - What causes routing errors today?
  3. Define fair distribution - What does balanced actually mean?
  4. Start with manual review - Run the bot in shadow mode first
  5. Iterate on the logic - Use mid-turn steering to refine

Ready to build intelligent territory management? Book a demo to see how MarketBetter handles lead routing and territory optimization out of the box.

Related reading:

Building a WhatsApp Sales Bot for Real-Time Deal Notifications with OpenClaw [2026]

· 9 min read
Sunder Iyer
Founder, marketbetter.ai

Your CRM sends email notifications. You check email twice a day. A hot lead came in at 9am. You saw it at 3pm. They already booked with a competitor.

The notification problem is a channel problem.

Sales leaders check WhatsApp 50+ times per day. They check email 2-3 times. If you want real-time awareness of your pipeline, put alerts where you actually look.

OpenClaw makes this trivially easy. In this guide, I'll show you how to build a WhatsApp bot that:

  • Alerts you when high-value leads come in
  • Notifies you of deal stage changes
  • Sends daily pipeline summaries
  • Lets you query your CRM conversationally

All running 24/7. All delivered to the app you already have open.

WhatsApp deal notification system

Why WhatsApp for Sales Notifications

The Engagement Numbers

ChannelAvg. Time to SeeOpen RateResponse Rate
Email6-24 hours21%2%
Slack30-60 min65%15%
WhatsApp&lt; 5 min98%45%
SMS3-10 min90%25%

WhatsApp wins because:

  1. It's always open - Most professionals keep it running
  2. Notifications are prominent - You see them immediately
  3. It's conversational - You can reply/query naturally
  4. It's personal - Feels more important than work tools

What Should Come Through WhatsApp

Not everything. Be selective:

High Priority (Immediate WhatsApp):

  • New leads over $50K estimated value
  • Deal stage changes (especially late stage)
  • At-risk deals (no activity 7+ days)
  • Competitor mentions detected
  • Key account activity (target list)
  • Urgent meeting requests

Medium Priority (Daily Summary):

  • Pipeline additions
  • Forecast changes
  • Activity metrics
  • Team performance

Low Priority (Keep in CRM/Email):

  • Routine activity logging
  • System notifications
  • Bulk updates

Setting Up OpenClaw for WhatsApp

OpenClaw connects to WhatsApp through its built-in WhatsApp channel. Setup takes about 10 minutes.

Step 1: Configure OpenClaw

# openclaw.yaml
channels:
whatsapp:
enabled: true
# Your personal WhatsApp gets connected via QR code scan

agents:
deal-alerts:
model: claude-sonnet-4-20250514
prompt: |
You are a sales intelligence assistant. Monitor the CRM for important events
and send timely, actionable notifications to the sales team via WhatsApp.

Be concise. Every message should be scannable in 5 seconds.
Include: What happened, why it matters, what to do next.

tools:
- hubspot # or salesforce
- web_search
- memory

When you start OpenClaw, it will prompt you to scan a QR code with WhatsApp:

openclaw gateway start
# Scan QR code with WhatsApp when prompted

Once linked, OpenClaw can send and receive WhatsApp messages.

Step 3: Build the Alert System

# deal_alerts.py
import os
from datetime import datetime, timedelta
from hubspot import HubSpot

class DealAlertSystem:
def __init__(self):
self.hubspot = HubSpot(access_token=os.environ['HUBSPOT_TOKEN'])
self.alert_thresholds = {
"high_value_deal": 50000,
"stale_deal_days": 7,
"hot_lead_score": 80
}

def check_new_high_value_deals(self) -> list:
"""Find deals created in last hour over threshold."""

one_hour_ago = datetime.now() - timedelta(hours=1)

deals = self.hubspot.crm.deals.search(
filter_groups=[{
"filters": [
{
"propertyName": "createdate",
"operator": "GTE",
"value": int(one_hour_ago.timestamp() * 1000)
},
{
"propertyName": "amount",
"operator": "GTE",
"value": self.alert_thresholds["high_value_deal"]
}
]
}]
)

return [self.format_deal_alert(d) for d in deals.results]

def check_stage_changes(self) -> list:
"""Find deals that changed stage in last hour."""

# Query deal history for stage changes
# (Implementation depends on your CRM's activity tracking)
pass

def check_stale_deals(self) -> list:
"""Find open deals with no activity in X days."""

stale_date = datetime.now() - timedelta(
days=self.alert_thresholds["stale_deal_days"]
)

deals = self.hubspot.crm.deals.search(
filter_groups=[{
"filters": [
{
"propertyName": "dealstage",
"operator": "NEQ",
"value": "closedwon"
},
{
"propertyName": "dealstage",
"operator": "NEQ",
"value": "closedlost"
},
{
"propertyName": "notes_last_updated",
"operator": "LT",
"value": int(stale_date.timestamp() * 1000)
}
]
}]
)

return [self.format_stale_alert(d) for d in deals.results]

def format_deal_alert(self, deal) -> str:
"""Format a deal into a scannable WhatsApp message."""

amount = f"${deal.properties.get('amount', 0):,.0f}"
company = deal.properties.get('dealname', 'Unknown')
stage = deal.properties.get('dealstage', 'Unknown')
owner = deal.properties.get('hubspot_owner_id', 'Unassigned')

return f"""
🔥 *NEW HIGH-VALUE DEAL*

💰 \{amount\}
🏢 {company}
📍 Stage: \{stage\}
👤 Owner: {owner}

→ Check HubSpot: [link]
"""

def format_stale_alert(self, deal) -> str:
"""Format a stale deal warning."""

company = deal.properties.get('dealname', 'Unknown')
days_stale = self.calculate_days_stale(deal)
amount = f"${deal.properties.get('amount', 0):,.0f}"

return f"""
⚠️ *DEAL GOING COLD*

🏢 {company}
💰 \{amount\}
📅 No activity: {days_stale} days

Action needed: Log an activity or update stage.
"""

WhatsApp sales bot conversation

Scheduling Alerts with OpenClaw Cron

OpenClaw has built-in cron support. Here's how to schedule different alert types:

# openclaw.yaml - Cron jobs for deal alerts
cron:
jobs:
# Check for hot leads every 15 minutes
- name: "hot-lead-check"
schedule: "*/15 * * * *"
payload:
kind: systemEvent
text: |
Check CRM for new high-value leads (score > 80 or amount > $50K).
If found, send WhatsApp alert with lead details and recommended action.
sessionTarget: main

# Check for stale deals twice daily
- name: "stale-deal-check"
schedule: "0 9,15 * * *" # 9am and 3pm
payload:
kind: systemEvent
text: |
Check for open deals with no activity in 7+ days.
Send WhatsApp summary if any found, grouped by owner.
sessionTarget: main

# Daily pipeline summary
- name: "daily-pipeline-summary"
schedule: "0 8 * * 1-5" # 8am weekdays
payload:
kind: systemEvent
text: |
Generate daily pipeline summary:
- Total pipeline value
- New deals (24h)
- Stage changes
- Deals closing this week
- At-risk deals
Send formatted summary to WhatsApp.
sessionTarget: main

# Weekly forecast update
- name: "weekly-forecast"
schedule: "0 9 * * 1" # Monday 9am
payload:
kind: systemEvent
text: |
Generate weekly forecast report:
- Commit vs. best case
- Week-over-week change
- Key deals to watch
- Recommended focus areas
Send to WhatsApp with action items.
sessionTarget: main

Conversational CRM Queries

The real power is bidirectional. Ask questions, get answers:

Example Conversations

You: "What's my pipeline looking like this quarter?"

Bot:

📊 Q1 Pipeline Summary

Total: $2.4M (68 deals)
├── Qualified: $890K (32 deals)
├── Demo: $720K (18 deals)
├── Proposal: $540K (12 deals)
└── Negotiation: $250K (6 deals)

🎯 vs. Target: $2.4M / $3M (80%)
📈 WoW Change: +$180K (+8%)

Top 3 Deals:
1. Acme Corp - $180K - Proposal
2. TechStart Inc - $120K - Demo
3. Global Systems - $95K - Negotiation

You: "Any updates on the Acme deal?"

Bot:

🏢 Acme Corp | $180K | Proposal Stage

Last Activity: 2 days ago
- Sarah sent revised proposal
- Waiting on procurement review

Next Steps:
- Follow-up scheduled for Thursday
- Need to connect with CFO (budget holder)

⚠️ Risk: Competitor (Warmly) mentioned in last call

You: "Who should I call today?"

Bot:

📞 Priority Calls for Today

1. 🔥 Global Systems - $95K
Negotiation stage, contract review
Contact: Mike Chen (VP Sales)

2. ⚠️ DataFlow Inc - $65K
No activity 5 days, was hot
Contact: Lisa Park (CRO)

3. 📅 TechStart Inc - $120K
Demo follow-up due
Contact: James Wilson (CEO)

Shall I prep call briefs for any of these?

Implementation

# conversational_crm.py
from anthropic import Anthropic

class ConversationalCRM:
def __init__(self):
self.client = Anthropic()
self.hubspot = HubSpot(access_token=os.environ['HUBSPOT_TOKEN'])

def process_query(self, user_message: str) -> str:
"""Process natural language CRM queries."""

# First, understand the intent
intent = self.classify_intent(user_message)

# Fetch relevant data based on intent
if intent['type'] == 'pipeline_summary':
data = self.get_pipeline_data()
elif intent['type'] == 'deal_detail':
data = self.get_deal_detail(intent['deal_name'])
elif intent['type'] == 'priority_tasks':
data = self.get_priority_tasks()
elif intent['type'] == 'forecast':
data = self.get_forecast_data()
else:
data = self.general_crm_query(user_message)

# Format response for WhatsApp
return self.format_whatsapp_response(data, intent)

def classify_intent(self, message: str) -> dict:
"""Use Claude to understand what the user wants."""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=200,
messages=[{
"role": "user",
"content": f"""
Classify this CRM query intent:
"{message}"

Return JSON:
{{
"type": "pipeline_summary|deal_detail|priority_tasks|forecast|activity_log|general",
"deal_name": "if specific deal mentioned",
"time_period": "if time mentioned",
"filters": ["any filters mentioned"]
}}
"""
}]
)

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

def format_whatsapp_response(self, data: dict, intent: dict) -> str:
"""Format CRM data for WhatsApp (concise, scannable)."""

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
messages=[{
"role": "user",
"content": f"""
Format this CRM data for WhatsApp. Keep it:
- Scannable in 5 seconds
- Using emojis for visual hierarchy
- Under 300 words
- Action-oriented

Data: {json.dumps(data)}
User Intent: {json.dumps(intent)}
"""
}]
)

return response.content[0].text

Security Considerations

What to Protect

  1. Deal values: Don't send exact amounts to shared groups
  2. Contact info: Keep personal details in CRM, not messages
  3. Competitive intel: Sensitive mentions stay private
  4. Access control: Only authorized users get alerts

Implementation

# security_filter.py
class AlertSecurityFilter:
def __init__(self):
self.private_fields = ['contact_phone', 'contact_email', 'notes']
self.sensitive_keywords = ['competitor', 'pricing', 'discount']

def filter_for_channel(self, alert: dict, channel_type: str) -> dict:
"""Filter alert content based on channel sensitivity."""

if channel_type == 'group':
# Remove specific amounts
alert['amount'] = self.round_amount(alert.get('amount', 0))
# Remove private fields
for field in self.private_fields:
alert.pop(field, None)
# Flag if contains sensitive info
if self.contains_sensitive(alert):
return self.create_private_redirect(alert)

return alert

def round_amount(self, amount: float) -> str:
"""Round amounts for public channels."""
if amount >= 100000:
return f"${int(amount/100000)}00K+"
elif amount >= 10000:
return f"${int(amount/10000)}0K+"
else:
return "< $10K"

def contains_sensitive(self, alert: dict) -> bool:
"""Check if alert contains sensitive content."""
text = json.dumps(alert).lower()
return any(kw in text for kw in self.sensitive_keywords)

def create_private_redirect(self, alert: dict) -> dict:
"""Create a redacted alert that points to private channel."""
return {
"type": "private_redirect",
"message": f"🔒 Sensitive update on {alert.get('deal_name', 'a deal')}. Check DM for details."
}

Results You Can Expect

Teams using WhatsApp-based deal alerts report:

  • Response time to hot leads: Down from 4 hours to 8 minutes
  • Stale deal intervention: Up 3x (catches problems faster)
  • Pipeline accuracy: Up 40% (reps update more when it's easy)
  • Manager awareness: "I know what's happening without asking"

The ROI isn't complicated: faster response = more deals closed.

Free Tool

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

Getting Started

Prerequisites

  • OpenClaw installed and running
  • HubSpot or Salesforce API access
  • WhatsApp on your phone

Quick Start

  1. Add WhatsApp to OpenClaw config:
channels:
whatsapp:
enabled: true
  1. Scan QR code when prompted:
openclaw gateway start
  1. Add your first cron alert:
cron:
jobs:
- name: "hot-lead-alert"
schedule: "*/15 * * * *"
payload:
kind: systemEvent
text: "Check for new leads over $50K. Alert me on WhatsApp if found."
sessionTarget: main
  1. Test it: Create a test deal in your CRM over the threshold. Wait 15 minutes. Get the alert.

  2. Iterate: Add more alerts, tune thresholds, build conversational queries.


Want this without building it yourself? MarketBetter includes real-time deal alerts + AI-powered playbooks →

Account Prioritization with AI: Claude Code vs Spreadsheets [2026]

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

Ask any sales rep: "How do you decide who to call first?" You'll get answers like:

  • "I work alphabetically through my list"
  • "Whatever came in most recently"
  • "Gut feeling based on company size"
  • "Whoever my manager tells me to"

None of these are strategies. They're coping mechanisms for a broken system.

The best accounts—the ones with the highest likelihood to close and the highest deal value—are often buried in a spreadsheet, never contacted. Meanwhile, reps waste hours on accounts that were never going to buy.

AI Account Prioritization System

This guide shows you how to build an AI-powered account scoring system with Claude Code that identifies your highest-potential accounts automatically. Stop guessing. Start knowing.

The Real Cost of Poor Prioritization

Here's what happens when sales teams prioritize badly:

Time Waste:

  • Average SDR spends 2+ hours daily deciding who to contact
  • 67% of time is spent on accounts that will never convert
  • Best accounts get the same attention as worst accounts

Revenue Loss:

  • 35-50% of deals go to the vendor that responds first
  • High-fit accounts that go uncontacted convert at competitor sites
  • Reps hit quota on volume, miss it on value

Burnout:

  • Calling dead accounts kills morale
  • "Spray and pray" feels pointless (because it is)
  • Top performers leave for companies with better systems

Spreadsheet Chaos vs AI Organization

The data is clear: teams that score and prioritize accounts effectively see 30% higher conversion rates and 20% shorter sales cycles.

Why Traditional Lead Scoring Fails

Most lead scoring systems are built on two flawed premises:

Flaw 1: Static Rules

"Companies with 500+ employees get 10 points."

This ignores:

  • Industry context (500 at a tech startup vs. 500 at a hospital = totally different)
  • Current buying signals
  • Relationship history
  • Market timing

Flaw 2: Incomplete Data

You score what you can measure, but the most predictive signals are often qualitative:

  • "They mentioned they're evaluating competitors"
  • "Their CTO attended our webinar AND read our pricing page"
  • "They just raised a Series B and need to scale sales"

Claude Code can synthesize both structured and unstructured data to create scoring that actually predicts conversions.

The Architecture of AI Account Scoring

Here's how an intelligent prioritization system works:

1. Data Aggregation

Pull from every source: CRM, enrichment tools, website behavior, email engagement, social signals.

2. ICP Matching

Score firmographic fit against your ideal customer profile.

3. Intent Detection

Identify behavioral signals that indicate active buying.

4. Relationship Mapping

Account for existing touchpoints and engagement history.

5. Timing Analysis

Factor in buying cycles, budget periods, and urgency signals.

6. Composite Scoring

Combine all factors into a single prioritization score.

Building the System with Claude Code

Step 1: Define Your ICP Criteria

First, codify what makes an account "ideal":

const ICP_CRITERIA = {
firmographic: {
employeeRange: { min: 50, max: 1000, weight: 0.2 },
revenueRange: { min: 5000000, max: 100000000, weight: 0.15 },
industries: {
include: ['SaaS', 'Technology', 'Financial Services', 'Healthcare'],
exclude: ['Government', 'Education'],
weight: 0.15
},
geographies: {
include: ['US', 'Canada', 'UK', 'Germany'],
weight: 0.05
}
},

technographic: {
required: ['Salesforce', 'HubSpot'],
positive: ['Outreach', 'SalesLoft', 'Gong'],
negative: ['Competitor X', 'Legacy CRM'],
weight: 0.15
},

departmentSignals: {
hasSalesTeam: { minSize: 5, weight: 0.1 },
hasMarketingTeam: { minSize: 2, weight: 0.05 },
hasRevOps: { weight: 0.1 }
}
};

Step 2: Aggregate Data Sources

Pull everything you know about each account:

async function aggregateAccountData(companyId) {
// CRM data
const crmData = await crm.getCompany(companyId);
const contacts = await crm.getContacts({ companyId });
const deals = await crm.getDeals({ companyId });
const activities = await crm.getActivities({ companyId });

// Enrichment data
const enrichment = await clearbit.enrich(crmData.domain);
const techStack = await builtwith.getTechStack(crmData.domain);

// Website behavior
const webActivity = await analytics.getCompanyActivity(companyId, {
days: 30
});

// Email engagement
const emailEngagement = await emailPlatform.getEngagement(companyId);

// Social signals
const linkedInActivity = await linkedin.getCompanySignals(crmData.domain);

// News and events
const recentNews = await newsApi.getCompanyNews(crmData.name, { days: 90 });

// Competitor mentions
const competitorSignals = await detectCompetitorActivity(companyId);

return {
company: crmData,
contacts,
deals,
activities,
enrichment,
techStack,
webActivity,
emailEngagement,
linkedInActivity,
recentNews,
competitorSignals
};
}

Step 3: Score with Claude Code

Now use Claude to synthesize all signals into a comprehensive score:

async function scoreAccount(accountData) {
// Calculate structured scores
const icpScore = calculateICPScore(accountData, ICP_CRITERIA);
const engagementScore = calculateEngagementScore(accountData);
const intentScore = calculateIntentScore(accountData);

// Use Claude for qualitative analysis
const qualitativeAnalysis = await claude.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1000,
system: `You are a B2B sales strategist analyzing accounts for
prioritization. You excel at identifying hidden buying signals and
assessing account quality beyond basic metrics.

Provide:
1. OPPORTUNITY_SCORE (0-100): Likelihood to close
2. VALUE_SCORE (0-100): Potential deal size relative to effort
3. TIMING_SCORE (0-100): Urgency/readiness to buy
4. KEY_INSIGHTS: 2-3 critical observations
5. RECOMMENDED_APPROACH: Best first touch strategy`,
messages: [{
role: 'user',
content: `Analyze this account for prioritization:

COMPANY: ${accountData.company.name}
INDUSTRY: ${accountData.enrichment.industry}
SIZE: ${accountData.enrichment.employeeCount} employees
REVENUE: $${accountData.enrichment.annualRevenue}
TECH STACK: ${accountData.techStack.join(', ')}

RECENT ACTIVITY:
- Website visits: ${accountData.webActivity.pageviews} (${accountData.webActivity.uniqueVisitors} unique)
- Pages viewed: ${accountData.webActivity.topPages.join(', ')}
- Email engagement: ${accountData.emailEngagement.openRate}% open, ${accountData.emailEngagement.clickRate}% click
- Last activity: ${accountData.webActivity.lastActivity}

CONTACTS:
${accountData.contacts.map(c => `- ${c.name} (${c.title}): ${c.engagementScore} engagement`).join('\n')}

RECENT NEWS:
${accountData.recentNews.map(n => `- ${n.headline}`).join('\n')}

COMPETITOR SIGNALS:
${accountData.competitorSignals.length > 0 ? accountData.competitorSignals.join('\n') : 'None detected'}

RELATIONSHIP HISTORY:
- Previous deals: ${accountData.deals.length}
- Total activities: ${accountData.activities.length}
- Last touch: ${accountData.activities[0]?.date || 'Never'}

Provide your analysis as JSON.`
}],
response_format: { type: 'json_object' }
});

const aiAnalysis = JSON.parse(qualitativeAnalysis.content[0].text);

// Combine all scores
return {
companyId: accountData.company.id,
companyName: accountData.company.name,
scores: {
icp: icpScore,
engagement: engagementScore,
intent: intentScore,
opportunity: aiAnalysis.OPPORTUNITY_SCORE,
value: aiAnalysis.VALUE_SCORE,
timing: aiAnalysis.TIMING_SCORE
},
composite: calculateComposite({
icp: icpScore,
engagement: engagementScore,
intent: intentScore,
...aiAnalysis
}),
insights: aiAnalysis.KEY_INSIGHTS,
recommendedApproach: aiAnalysis.RECOMMENDED_APPROACH,
tier: determineTier(/* composite score */)
};
}

function calculateComposite(scores) {
// Weighted combination
return (
scores.icp * 0.2 +
scores.engagement * 0.15 +
scores.intent * 0.25 +
scores.OPPORTUNITY_SCORE * 0.2 +
scores.VALUE_SCORE * 0.1 +
scores.TIMING_SCORE * 0.1
);
}

Step 4: Create the Daily Prioritized List

Generate a ranked list for each rep every morning:

async function generateDailyPrioritization(repId) {
// Get rep's assigned accounts
const accounts = await crm.getAccountsByRep(repId);

// Score all accounts (parallelize for speed)
const scoredAccounts = await Promise.all(
accounts.map(async account => {
const data = await aggregateAccountData(account.id);
return scoreAccount(data);
})
);

// Sort by composite score
const ranked = scoredAccounts.sort((a, b) => b.composite - a.composite);

// Assign daily tiers
const dailyList = {
mustTouch: ranked.slice(0, 5).map(addContactReason),
highPriority: ranked.slice(5, 15).map(addContactReason),
standard: ranked.slice(15, 50).map(addContactReason),
nurture: ranked.slice(50).map(addContactReason)
};

// Push to CRM and Slack
await crm.updateDailyPriorities(repId, dailyList);
await slack.sendDM(repId, formatPriorityList(dailyList));

return dailyList;
}

function addContactReason(account) {
return {
...account,
whyNow: generateWhyNow(account),
suggestedAction: getSuggestedAction(account),
talkingPoints: getTalkingPoints(account)
};
}

Account Scoring Dashboard

Real-World Example: Tech Company Prioritization

Input: 500 accounts assigned to an SDR

AI Analysis Output (top 3):

[
{
"companyName": "CloudScale Inc",
"composite": 94,
"scores": {
"icp": 92,
"engagement": 88,
"intent": 96,
"timing": 98
},
"insights": [
"CEO visited pricing page 3x this week",
"Currently using Competitor X (known pain: data accuracy)",
"Just closed Series B—scaling sales team is top priority"
],
"recommendedApproach": "Reference Series B news, position as infrastructure for scaling sales team. CEO is actively evaluating—this is hot.",
"whyNow": "Series B + active pricing page visits = buying now"
},
{
"companyName": "DataFlow Systems",
"composite": 87,
"scores": {
"icp": 95,
"engagement": 75,
"intent": 89,
"timing": 82
},
"insights": [
"VP Sales attended our webinar last week",
"Hiring 5 SDRs according to LinkedIn",
"No current solution in place"
],
"recommendedApproach": "Reference webinar attendance, offer to help structure their new SDR team. Timing is good with their hiring push.",
"whyNow": "Building SDR team from scratch = greenfield opportunity"
},
{
"companyName": "NextGen Analytics",
"composite": 84,
"scores": {
"icp": 88,
"engagement": 91,
"intent": 78,
"timing": 75
},
"insights": [
"3 different people from the company have downloaded content",
"Tech stack includes Salesforce + Outreach",
"Last contacted 6 months ago—went dark after demo"
],
"recommendedApproach": "Re-engage with new angle. Multiple stakeholders engaged now vs. single contact before. Ask what's changed.",
"whyNow": "Re-engagement opportunity with broader buying committee"
}
]

Continuous Learning: The Feedback Loop

The system improves by tracking outcomes:

async function logPrioritizationOutcome(accountId, outcome) {
const originalScore = await getHistoricalScore(accountId);

await analyticsDb.log({
accountId,
scoredAt: originalScore.timestamp,
composite: originalScore.composite,
outcome: outcome, // 'converted', 'stalled', 'lost', 'disqualified'
daysToOutcome: daysBetween(originalScore.timestamp, new Date()),
dealValue: outcome === 'converted' ? await getDealValue(accountId) : null
});

// Quarterly: Retrain weights based on what actually converted
if (isQuarterEnd()) {
await retrainScoringWeights();
}
}

async function retrainScoringWeights() {
const outcomes = await analyticsDb.getOutcomes({ months: 6 });

// Analyze which factors actually predicted conversions
const analysis = await claude.messages.create({
model: 'claude-3-5-sonnet-20241022',
messages: [{
role: 'user',
content: `Analyze these prioritization outcomes and recommend
weight adjustments:

CONVERSIONS:
${outcomes.filter(o => o.outcome === 'converted').map(summarize).join('\n')}

LOSSES:
${outcomes.filter(o => o.outcome === 'lost').map(summarize).join('\n')}

Current weights: ${JSON.stringify(currentWeights)}

What factors were most predictive? Recommend new weights.`
}]
});

// Update scoring algorithm
await updateScoringWeights(analysis);
}

Integration with Daily Workflow

Make prioritization seamless:

Morning Slack Notification

// 7am daily
cron.schedule('0 7 * * *', async () => {
const reps = await crm.getActiveReps();

for (const rep of reps) {
const priorities = await generateDailyPrioritization(rep.id);

await slack.sendDM(rep.slackId, {
blocks: [
{
type: 'header',
text: `🎯 Your Priority Accounts for Today`
},
{
type: 'section',
text: `*Must Touch (5 accounts)*\n${priorities.mustTouch.map(a =>
`• *${a.companyName}* (Score: ${a.composite}) — ${a.whyNow}`
).join('\n')}`
},
{
type: 'actions',
elements: [
{
type: 'button',
text: 'View Full List',
url: `https://crm.com/priorities/${rep.id}`
}
]
}
]
});
}
});

CRM Priority Field Updates

async function syncToCRM(priorities) {
for (const account of [...priorities.mustTouch, ...priorities.highPriority]) {
await crm.updateCompany(account.companyId, {
priority_tier: account.tier,
ai_score: account.composite,
last_scored: new Date(),
recommended_action: account.suggestedAction,
score_reasoning: account.insights.join(' | ')
});

// Create task if high priority
if (account.tier === 'mustTouch') {
await crm.createTask({
companyId: account.companyId,
subject: `Priority Touch: ${account.companyName}`,
notes: account.whyNow,
dueDate: new Date()
});
}
}
}

Measuring Prioritization ROI

Track these metrics:

MetricBefore AIAfter AIImprovement
Time deciding who to call2.1 hrs/day0.2 hrs/day-90%
Contact rate on Tier 1 accounts24%41%+71%
Conversion rate (all)2.8%4.6%+64%
Average deal size$28K$36K+29%
Quota attainment78%94%+21%

The compound effect: If better prioritization increases conversions by 64% and deal size by 29%, and you're running 1,000 qualified accounts/quarter at a $30K baseline ACV, that's an additional $620K in ARR quarterly.

Advanced: Dynamic Reprioritization

Don't just score once—reprioritize throughout the day:

// Real-time triggers
async function handleSignificantEvent(event) {
const { accountId, eventType, data } = event;

const significantEvents = [
'pricing_page_visit',
'competitor_search',
'demo_request',
'executive_engagement',
'funding_announcement'
];

if (significantEvents.includes(eventType)) {
// Immediately rescore
const newScore = await scoreAccount(await aggregateAccountData(accountId));

// If jumped to Tier 1, alert immediately
if (newScore.tier === 'mustTouch' && (await getPreviousTier(accountId)) !== 'mustTouch') {
await sendUrgentAlert(accountId, newScore, event);
}
}
}

async function sendUrgentAlert(accountId, score, triggerEvent) {
const rep = await crm.getAccountOwner(accountId);

await slack.sendDM(rep.slackId, {
text: `🚨 *HOT ACCOUNT ALERT*\n\n*${score.companyName}* just jumped to Tier 1!\n\nTrigger: ${triggerEvent.eventType}\n${score.whyNow}\n\nDrop what you're doing. This one's live.`
});
}

Getting Started with MarketBetter

Building AI account prioritization from scratch is powerful but complex. MarketBetter provides the complete solution:

  • Daily SDR Playbook — Every rep gets their prioritized list each morning
  • Real-time scoring — Accounts reprioritize based on live signals
  • AI-powered reasoning — Not just a score, but why and what to do
  • CRM integration — HubSpot, Salesforce out of the box
  • Learning loop — Improves automatically based on your conversion data

Stop letting your best accounts go unworked. Stop wasting time on accounts that were never going to buy. Let AI tell you exactly where to focus.

Book a Demo →

Free Tool

Try our Lookalike Company Finder — find companies similar to your best customers in seconds. No signup required.

Key Takeaways

  1. Poor prioritization costs deals — 67% of rep time goes to accounts that won't convert
  2. Static lead scoring fails — Rules can't capture qualitative buying signals
  3. Claude Code enables intelligent scoring — Synthesize structured + unstructured data
  4. Make it actionable — Daily ranked lists with clear reasoning and suggested actions
  5. Continuous learning — Track outcomes and retrain weights quarterly

Your CRM is full of gold. The problem is it's mixed in with thousands of accounts that look the same on the surface. AI-powered prioritization separates signal from noise—so your team spends 100% of their time on accounts that can actually close.