An AI dialer migration replaces your legacy power dialer with an orchestrated engine that runs scheduled calling campaigns, classifies outcomes with an LLM classifier, and writes structured dispositions back into the CRM so reps only handle high-value callbacks. We built and shipped this pattern in production for a real-estate brokerage where the dialer worked a 3,000+ lead backlog and reduced false bookings through deterministic guards and data hygiene. This guide shows what we built, how it works in production, migration steps, and the gotchas that tripped us up.
The problem it solves
Answer-first: migrating to an AI dialer solves scale and human-cost problems. It lets you process large lead backlogs automatically, surface genuine booking opportunities, and reduce wasted human dialer time.
Manual process. A team exports a lead list, uploads to a dialer, waits for reps to call, copies dispositions to the CRM, and chases missed numbers. This is repeated every campaign. It costs rep hours, introduces stale-lead risk, and produces noisy booking data when classification is imperfect.
Automated process. The AI dialer polls a single source-of-truth list, attempts calls on a human-like cadence, classifies outcomes, applies deterministic guards to prevent false bookings, and writes only verified opportunities to the CRM for a human to finalize.
| Manual | Automated |
|---|---|
| Export CSV, manual uploads, and repeated re-keys | Single source-of-truth, scheduled automated runs, and idempotent writes |
| Reps spend hours dialing marginal leads | AI processes bulk lead backlog overnight and surfaces qualified callbacks |
| High rate of false bookings and stale dials | Deterministic guards, stale-lead eviction, and pause-on-quota protect costs |
| No easy audit trail of calls vs dispositions | Append-only audit log and per-call tracebacks for compliance |
How the automation works
Answer-first: the migration is an orchestration that moves leads into a controlled queue, runs AI-powered call sessions, classifies outcomes with layered checks, and updates the CRM while keeping a human approval path for sensitive cases.
Architecture overview: a scheduled importer normalizes and deduplicates leads into a database; a scheduler enqueues calls with per-tenant pacing; a dialer runtime runs human-like sessions and streams transcript + classifier outputs to an orchestration layer; deterministic guards and quota checks prevent bad writes; the CRM or campaign system receives idempotent upserts and a lightweight reply queue surfaces hand-offs for human review.
Components we ran in production:
-
Source-of-truth lead store: A single Postgres table (or managed datastore) that holds canonical lead rows, call-state, and a last-seen fingerprint. We treated this table as the true master so re-runs did not duplicate sends.
-
Scheduler / Wedge engine: The scheduler builds short human-like sessions: batch of 40 leads, 12, 18 minute session duration, randomized inter-call delays. This prevents high cadence that looks automated.
-
Dialer runtime (accented engine): The AI dialer emulated a human session: click-to-call or vendor call-init, media playback, voice STT transcript capture, and an LLM classifier that suggested a disposition. The engine recorded audio and the final structured disposition.
-
Deterministic guard layer: A rule engine that prevents borderline or model-hallucinated bookings from writing to the CRM. It requires a classification threshold and at least one deterministic signal: explicit verbal confirmation, call length minimum, or human approval if the confidence is low.
-
Pause and quota monitor: Tracks vendor TTS/STT budget and pauses campaigns automatically when overage risks appear, then resumes after a scheduled refresh.
-
CRM sink with idempotency: Upserts were idempotent and ledgered. Each write included the call UUID and a checksum so retries never created duplicate bookings.
Step-by-step: how to build it
Step 1: freeze a single source-of-truth
We moved all candidate lists into one Postgres table and rejected CSV uploads that were not reconciled to that table. The table holds lead_id, contact fields, fingerprint hash, and state columns. This eliminated duplicate sends when backfills were re-run.
-- small example: canonical leads table
CREATE TABLE leads (
id serial PRIMARY KEY,
external_id text,
email text,
phone text,
fingerprint text UNIQUE,
status text DEFAULT 'queued',
last_seen timestamptz DEFAULT now()
);
-- upsert helper used by the importer
INSERT INTO leads (external_id,email,phone,fingerprint)
VALUES ($1,$2,$3,$4)
ON CONFLICT (fingerprint) DO UPDATE SET last_seen = now();Key gotcha: accept only one owner of the connection that performs scheduled imports so you do not accidentally swap credentials and create duplicate sessions.
Step 2: build a scheduler that runs human sessions
We wrote a small scheduler that picks a wedge of leads per tenant, schedules a session, and marks each lead as in_progress before dialing. The scheduler randomized inter-call pauses and enforced per-tenant caps.
// scheduler excerpt: pick leads and enqueue
const leads = await db.query("SELECT id,phone FROM leads WHERE status='queued' LIMIT $1", [batchSize]);
for (const l of leads.rows) {
await db.query("UPDATE leads SET status='in_progress' WHERE id=$1", [l.id]);
await queue.enqueue('call', { leadId: l.id, phone: l.phone });
}Key gotcha: mark rows in-progress under the same DB transaction that enqueues the job to avoid races that cause double-enqueue.
Step 3: implement the dialer runtime and classifier
The runtime handled call lifecycle: dial, connect, record, transcribe, classify. We kept the classifier call-out generic: send transcript to the model and return a structured disposition. The runtime always produced a call UUID and wrote a full audio+transcript artifact to object storage for audit.
// call run outline
const call = await callClient.dial(phoneNumber);
if (!call.connected) return markFailed(leadId, 'no_answer');
await call.recordStart();
const transcript = await call.transcribe();
const disposition = await classifier.classify(transcript);
await call.recordStop();
await storeCallArtifacts(callId, transcript, recordingUrl);Key gotcha: always persist call artifacts before making any CRM writes. If a downstream write fails, you still have the evidence for later reconciliation.
Step 4: add deterministic guards and a write policy
We layered deterministic rules on top of model output. A booking only wrote to the CRM when: classifier_confidence >= X and (explicit_confirmation_detected OR call_length >= Y). If confidence was low but a potential booking existed, we routed that call to a human queue instead of creating a booking.
-- simple ledger write with guard check
BEGIN;
SELECT confidence INTO conf FROM call_meta WHERE call_id = $1 FOR UPDATE;
IF conf >= 0.85 AND exists_confirmation THEN
INSERT INTO crm_upserts (lead_id,call_id,created_at) VALUES ($2,$1,now());
UPDATE leads SET status='booked' WHERE id=$2;
ELSE
UPDATE leads SET status='needs_review' WHERE id=$2;
END IF;
COMMIT;Key gotcha: guard thresholds must be tuned on real traffic. Too low and false bookings appear. Too high and you create manual workload.
Step 5: implement quota and overage protection
We added a quota checker that periodically queries vendor usage and pauses the scheduler when projected monthly spend or character counts hit a threshold. Pause is an automated stop that writes campaign state and notifies ops.
# cron: daily quota check
node scripts/checkQuota.js --projectId=acme
# checkQuota will POST /control/pause if usage > thresholdKey gotcha: vendor quotas can overrun while a session is mid-flight. We wrote a short cached quota check with a 10 minute TTL and a mid-session guard to pause new sessions if usage looks risky.
Where it gets complicated
Classifier misclassifications. Early runs produced false-positive bookings. We fixed this with a two-layer approach: tighten classifier prompts, then add deterministic guards that require explicit verbal confirmation or a minimum call length. The classifier changes alone did not solve the problem.
Stale leads and upsert semantics. The local dialer cache sometimes dialed leads that were archived in the CRM. We implemented an evict-on-sync strategy that removed rows missing from a full WATCH import. Evicting stale leads retired 640 leads in one real deployment and prevented wasted calls.
Vendor TTS/STT overage. One deployment accrued nontrivial TTS overage before guard logic ran. We added a pause/resume flow and a cached quota endpoint. The pause must be reversible by ops and transparent to clients.
False booking rollbacks. Before adding deterministic guards we saw classifier-driven false bookings. We added a retract endpoint that the CRM can call to remove a booking and mark the lead for human review. Planning for retracts upfront saved hours of cleanup.
TCPA and DNC compliance. Call cadence, opt-out handling, and call recording laws vary by region. We enforced per-tenant DNC filters at import time and recorded all opt-out events in the audit log. Compliance is not optional.
Auth and credential ownership. Credential ownership confusion caused silent failures during rollout. Decide who owns the sending connection and who rotates keys before you flip production; if the scheduler runs under a user account, their token expiry can quietly stop calls.
Human-in-the-loop UX. The hand-off UI must be fast and contextual: link to call audio, transcript highlight, and a one-click accept/reject for booking confirmations. Slow or incomplete context made human reviewers derail the flow.
What this actually changes
Answer-first: you move from manual dialing and re-keying to an automated, auditable funnel that runs large lead backlogs overnight, surfaces verified booking opportunities, and pauses safely on vendor and compliance risk.
Real results we observed in production for a real-estate ISA dialer we built and shipped:
- Coverage snapshot and throughput: the system processed a call universe of 3,130 callable leads and processed them end-to-end.
- Call volume and outcomes: total calls recorded in the deployment were 5,573 and the dialer produced approximately 12 verified bookings after iterative tuning and guard fixes.
- Data hygiene wins: evict-on-sync retired 640 stale leads and shrank retry queues dramatically, which removed repeated calls to inactive numbers.
- Accuracy fixes: classifier and server-side deterministic guards eliminated a high rate of misclassified bookings after fixes deployed 2026-05-14.
These changes were structural: instead of burning rep hours on low-yield dialing, we forwarded human attention only to high-confidence opportunities, and we added automated defenses against vendor overage and false bookings.
One external benchmark that explains the urgency to act: leads are time sensitive. Responding faster dramatically improves odds of qualification. See a practical summary on lead response timing: https://www.leadresponsemanagement.org/how-long-does-it-take-to-respond-to-a-lead/
Frequently asked questions
How long does a migration take? A focused migration to an AI dialer typically took us 3 to 6 weeks from kickoff to safe pilot for a single tenant. That includes importing and deduping the canonical lead table, a limited scheduler and session runtime, guard rules, and a monitored dry-run. The exact time depends on CRM complexity and compliance gaps.
What does this cost to run monthly? Recurring costs split into vendor voice/TTS/STT usage and modest hosting. The dialer we ran had nontrivial vendor usage early during training runs; protecting against overage with automatic pause logic kept the recurring bill predictable. We review billing in the first two weeks of live traffic and platform-tune quota thresholds.
How do you avoid false bookings? We layered classifiers with deterministic signals: explicit confirmation phrases, minimum connected time, and match of caller-stated booking details to lead fields. Low-confidence results go to a human review queue. That two-layer approach removed the majority of false bookings in our deployment.
Can you keep humans in the loop? Yes. The migration pattern we used always retains human approval for money or contract events. You can route only low-friction confirmations for auto-upsert and leave holds for higher-risk dispositions.
Do I need a developer to set this up? Yes, the migration requires engineering: scheduler, dialer runtime, classifier tuning, guard rules, and CRM idempotent upserts. Non-technical owners can run the configuration and review UI once the system is deployed, but the build needs an engineer or partner.
How do you handle compliance like TCPA and DNC? DNC and consent lists are applied at import time. We keep append-only audit logs for each call and expose opt-out webhooks to update the lead store instantly. Compliance planning is part of the migration scope, not an afterthought.
We built and shipped this migration pattern for a production real-estate ISA dialer and saw the benefits described above. If you are planning to replace a legacy dialer and want to avoid the common traps: consolidate to one source-of-truth, run human-like sessions, add deterministic write guards, and protect against vendor overage with an automated pause. Book a short discovery call and we will tell you in the first five minutes whether your setup maps to this pattern or needs a custom approach.
Related: our live case study on Follow Up Boss AI calling is here: /blog/automate-follow-up-boss-ai-calling. See our AI voice agent services at /services#ai-voice-agents. To schedule a discovery call, go to /book.
Want us to build this for you?
15-minute discovery call. No pitch. We tell you what to automate first.
Book a Discovery Call