Rex Automaton
All posts
AI Voice & Chat AgentsJuly 27, 202612 min read

How to Run Outbound AI Calling at Scale for Lead Backlogs

We built a production AI dialer that handled a 3,100+ lead backlog, qualified leads, left voicemails, and booked or disqualified contacts while preventing false bookings and vendor overage. This guide covers the engineering, checks, and gotchas.

By Jacky Lei

We built and shipped a production AI outbound dialer that worked a large lead backlog end-to-end: it polled a staged lead pool, ran a calling scheduler, executed humanlike call sessions, used an LLM to classify outcomes, and either booked, left a voicemail, or marked a lead disqualified. In production it handled thousands of calls without human dialing, and operational guards prevented false bookings and vendor overage billing.

This guide is for operators who need to migrate or replace a manual dialer and for engineering teams who must make an AI dialer safe for real-world use. It covers the manual baseline, a side-by-side table, architecture, a step-by-step build, platform-specific gotchas, production results we saw, and the operational controls that saved the deployment.

The problem it solves

Answer-first: An outbound AI dialer lets you convert a stale or large lead backlog into qualified appointments or clean rejections without tying up human dialers on low-yield calls.

Manual baseline: teams exported CSVs, gave reps spreadsheets, and ran manual calling sprints. Typical workflow: agent opens a row, calls until voicemail or live answer, writes a short disposition, and moves to the next lead. It scales linearly with headcount and produces high human-hours on low-yield rows. Common failures: stale contact info, duplicate dialing, false bookings created by misheard intents, and surprise vendor billing for TTS overage or per-minute charges.

Automated outcome: the automation schedules safe, humanlike sessions, enforces caps, uses a classifier plus deterministic guards to accept a booking, and records an audit trail for every call. The system reduces human-hours by automating the low-lift portion of qualification while surfacing only true opportunities to humans.

Manual processAutomated AI dialer
Agent-led calling sprints, spreadsheets, manual dispositionsScheduled dialer, queued sessions, automatic dispositions and booking handoffs
Prone to re-call duplicates and stale dataIdempotent upsert and evict-on-sync logic prevents duplicates and re-dials
Human judgement used for every call, even low-value onesLLM + deterministic guard handles routine classification; humans only review edge cases
Risk of vendor overage not surfaced until billedQuota checks and auto-pause prevent silent TTS or vendor overages
Bookings sometimes created incorrectly and left to clean upDeterministic booking guards and a retract endpoint avoid false bookings

How the automation works

Answer-first: The dialer polls a lead pool, schedules a bounded, randomized session that mimics human cadence, runs a single-call attempt per lead per session, classifies the outcome with an LLM, and then either creates a booking, leaves a voicemail, or marks the lead disqualified. Operational controls pause campaigns on vendor quota warnings and when rate or behavioral thresholds are hit.

Architecture overview: at a high level the system has five components: lead source and state ledger, scheduler, browser or telephony runner, LLM classifier plus deterministic guards, and the audit + ops dashboard.

  • Lead source and state ledger: a single canonical store holds leads, last-attempt timestamp, and disposition ledger. We used a local better-sqlite3 store for quick reads and a source-of-truth sync with the CRM. The ledger enforces idempotency so re-runs do not duplicate sends.

  • Session scheduler: sessions run on a human-like cadence window, with randomized start offsets and per-session caps. Each session processes a small batch to avoid bursts and to preserve natural timing signals.

  • Call runtime (browser harness or telephony): the runner executes one-call interactions. For headful browsers we used human-paced typing and click timing to reduce automation signals; for telephony integrations we enforced per-minute and per-call delays to mimic human pacing.

  • LLM classifier + deterministic guard: the LLM drafts or classifies the call result, but deterministic server-side guards must approve any booking. The guard checks for explicit confirmation phrases, matched calendar availability, and a low hallucination score before committing a booking.

  • Audit, ops dashboard, and quota guard: every call writes a compact audit row. A quota checker reads vendor quota and pauses campaigns when thresholds are near. The ops dashboard shows queue depth, quota state, and recent false-booking retractions.

AI dialer to qualify lead backlogs workflow: Lead pool feeds scheduler, sessions run the call runtime, LLM classifies, deterministic guards decide booking, outcomes go to calendar or human review.

Step-by-step: how to build it

Step 1: pick a canonical lead sink and implement idempotent write

Start by choosing the single canonical lead store (a CRM, a Postgres table, or a simple SQLite ledger). Each lead row needs an immutable id and a last_attempt timestamp so the runner can avoid duplicates.

Key config example (Node.js, SQLite):

// connect.js
const Database = require('better-sqlite3');
const db = new Database('./dialer-state.db');
 
// schema (run once)
db.exec(`
CREATE TABLE IF NOT EXISTS leads (
  id TEXT PRIMARY KEY,
  phone TEXT,
  email TEXT,
  status TEXT DEFAULT 'new',
  last_attempt INTEGER DEFAULT 0,
  attempts INTEGER DEFAULT 0,
  metadata_json TEXT
);
`);
 
module.exports = db;

Gotcha: ensure your import path is idempotent. When you backfill, use an upsert by id to avoid creating duplicates that will be dialed twice.

Step 2: build a session scheduler that mimics human cadence

The scheduler picks a small batch at randomized intervals and assigns a session token. Keep session length short, cap the number of calls per session, and add jitter.

Scheduler snippet:

// scheduler.js
const db = require('./connect');
function pickForSession(limit = 12) {
  const rows = db.prepare(`SELECT * FROM leads WHERE status = 'new' AND attempts < 3 ORDER BY last_attempt ASC LIMIT ?`).all(limit);
  return rows;
}
 
module.exports = { pickForSession };

Key gotcha: do not blast all leads at once. We randomized start times by 1-15 minutes and capped sessions at 12 leads to preserve natural cadence.

Step 3: implement the call runtime with humanlike pacing

Call runtime is either a browser-harnessed flow (for web dialers) or a telephony dialer client. The runtime must: open the call context, wait randomized pre-answer times, record audio or transcription, and return the raw transcript + metadata.

Call runner pseudocode (real code in your stack):

// runner.js
async function runCall(session, lead) {
  // place call via your vendor client
  const call = await vendor.call(lead.phone);
  const transcript = await call.waitForTranscript({timeoutMs: 45_000});
  return { transcript, duration: call.durationSec, answerType: call.answerType };
}

Gotcha: when automating web dialers, run in headed mode from a residential-like host to avoid anti-bot signals. For telephony, throttle to avoid repeated quick-dial patterns.

Step 4: classify with an LLM, then run deterministic booking guards

Send the call transcript and a short context to an LLM to extract intent, name, and booking confirmation phrases. Do not let the model alone create bookings. Implement deterministic guards that verify explicit confirmation tokens and calendar slot availability.

Example classification flow:

// classify.js
async function classify(transcript) {
  const prompt = `Classify this call result: ${transcript}`;
  const result = await llmClient.run(prompt);
  return { intent: result.intent, confidence: result.confidence, bookingCandidate: result.bookingSlot };
}
 
function bookingGuard(candidate, lead) {
  if (!candidate) return false;
  // deterministic checks
  if (candidate.confirmationPhrase !== 'i will be there' && candidate.confirmationPhrase !== 'yes book') return false;
  // calendar availability check
  if (!calendar.isAvailable(candidate.slot)) return false;
  return true;
}

Gotcha: classifiers make mistakes. The guard must be strict enough to require explicit affirmative language and calendar matching before committing a booking.

Step 5: write the disposition, audit row, and fallback routing

Every call writes a single audit row with: lead id, session id, raw transcript, classification result, guard decision, and any created booking id. If the guard fails but the LLM suggests interest, route to a human review queue.

Audit example:

// audit.js
db.prepare(`INSERT INTO audits (id, lead_id, session_id, transcript, classification, guard_passed, booking_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(uuid(), lead.id, session.id, transcript, JSON.stringify(classification), guardPassed ? 1 : 0, bookingId || null, Date.now());

Gotcha: store the raw transcript and a hash only, or encrypt if you handle PII. The audit row is your recovery and dispute trail.

Step 6: implement quota checks and an auto-pause

Before sending a TTS chunk or dialing a batch, the runner queries the cached vendor quota. If usage is near quota, pause the campaign and notify ops.

Quota pseudo-check:

async function ensureQuota() {
  const quota = await vendorClient.getQuota();
  if (quota.used / quota.limit > 0.95) {
    await campaign.pause();
    notifyOps('vendor quota exceeded, campaign paused');
    return false;
  }
  return true;
}

Gotcha: vendor quotas can accrue while the system is paused. Use a cached check and a 10-minute cache window to avoid rate-check storms.

Step 7: build retract and reconcile endpoints for false bookings

Provide an endpoint that retracts a booking and marks a lead for re-verification. This is the final safety net for misbookings.

Retract sketch:

// retract.js
app.post('/api/retract', async (req, res) => {
  const { bookingId } = req.body;
  await calendar.delete(bookingId);
  db.prepare(`UPDATE leads SET status='reverify' WHERE booking_id = ?`).run(bookingId);
  res.send({ ok: true });
});

Gotcha: make retract idempotent and auditable. Never silently delete the audit trail.

Where it gets complicated

LLM classification errors. LLMs misclassify confirmations when speakers use hedged language. The combination that worked was LLM extraction plus strict deterministic phrase matching before any booking is created.

Stale leads and upsert semantics. Upsert-only syncs left stale leads in the local DB. We shipped an evict-on-sync strategy that deactivates rows missing from a complete WATCH import. That retired 640 stale leads in production and reduced noise.

Vendor TTS or quota overage. TTS services can continue to accept work while billing overage accrues. We observed an overage event during early runs and it produced a modest bill before we paused. The fix is a quota API check, short cache, and a campaign auto-pause when thresholds are hit.

False bookings from classifier hallucinations. Early classifier runs produced 32 false booking classifications before we tightened prompts and added a server-side guard. Fixes eliminated new false bookings after deployment.

Authentication and key rotation. A stale partner API key produced 401s on writebacks mid-campaign. Add write-error tracking and an ops alert on persistent 401s to avoid blind failure.

Regulatory and phone-provider risk. High-frequency outbound calling, warmed accounts, and message content all affect provider signals. We implemented per-account daily caps and randomized session windows to reduce flagging risk.

What this actually changes

Answer-first: a production AI dialer shifts the effort from repetitive dialing to exception handling and human follow-up on interested contacts, while making lead lists actionable on day one.

Real deployment results we observed running the system for a real estate client: coverage snapshot showed 3,130 callable leads processed end-to-end. The system recorded 5,573 total calls. After we tightened classification and added deterministic guards, the deployment reported roughly 12 real bookings credited through the pipeline and no new false bookings after the May 14 fixes. We evicted 640 stale leads and shrank a retry queue from 86 to 7, making campaign pacing reliable. We also detected vendor TTS overage during early runs and added a quota guard: the system observed vendor usage that exceeded a 500k character soft threshold and accrued a small overage before we paused; the auto-pause guard prevented further unbilled usage.

Why the value is structural: this is repeatable labor recovery. Instead of adding dialer headcount to work a backlog, the automation processes low-signal calls at scale and surfaces only qualified opportunities to reps. The ongoing costs are engineering maintenance and model usage, while human attention focuses on high-value, live conversations.

One external statistic that matters for ROI: leads contacted within an hour are dramatically more likely to convert. In practice that means reducing lag from days to hours has an outsized impact on yield. See Harvard Business Review: "The Short Life of Online Sales Leads" for the original study and conversion framing: https://hbr.org/2011/03/the-short-life-of-online-sales-leads

Frequently asked questions

Can an AI dialer replace my human dialers? No. Answer-first: it replaces the low-value, repetitive portion of dialing and qualification, not full human salesmanship. Humans remain necessary for complex objections, negotiated bookings, and high-value closes. We reduce routine calls so your reps spend time on conversations where human judgement matters.

How do you prevent false bookings created by an LLM? Answer-first: never allow the LLM alone to commit bookings. We require deterministic server-side checks: explicit confirmation phrases, availability verification against the calendar, and a confidence threshold. If any check fails, the call routes to a human review queue instead of creating a booking.

Can this run in real time or must it be batched? Answer-first: both patterns work. For a backlog you run scheduled, bounded sessions. For near-real-time follow-up you shrink session size and reduce start jitter, but you must balance provider rate limits and provider-signal risk. We recommended hourly sessions for pilots and increasing cadence only after monitoring provider signals and deliverability.

What does a safe rollout look like? Answer-first: run the AI in shadow mode first, compare LLM classifications to human dispositions, tune prompts, then enable guarded actions (bookings only after guard pass). Pilot on a small subset of warmed inboxes and monitor vendor quotas and false-booking rate for 1, 2 weeks.

What are the typical monthly costs? Answer-first: model usage and vendor telephony/TTS usage dominate. Costs vary by volume and vendor pricing. The right move is to model usage in a staged pilot, cap daily spend, and only remove caps after you validate booking quality. We can scope this for your lead volume during a discovery call.

Can a non-technical owner run this? Answer-first: the operations model is approachable, but initial setup requires engineering: idempotent sink, scheduler, runner integration, and guard logic. After delivery the owner can manage campaign state via the ops dashboard and change cadence or caps without code.

We built and operated this pattern for a live real estate outbound pipeline that processed thousands of leads and taught us the operational controls that matter: idempotency, evict-on-sync, strict booking guards, quota auto-pause, and a retract/reconcile endpoint. If you have a backlog you want converted to qualified bookings, book a 15-minute discovery call: we will tell you in five minutes whether your leads map to this pattern and what the safe pilot looks like. See also our AI voice agents service: /services#ai-voice-agents, and our prior follow-up-boss dialer case study: /blog/automate-follow-up-boss-ai-calling.

Want us to build this for you?

15-minute discovery call. No pitch. We tell you what to automate first.

Book a Discovery Call

Related reading