Tools: ViciDial Lead Recycling & List Management Best Practices - Complete Guide

Tools: ViciDial Lead Recycling & List Management Best Practices - Complete Guide

ViciDial Lead Recycling & List Management Best Practices

Prerequisites

Understanding Lead Lifecycle in ViciDial

Lead Status Codes and Their Meanings

The Recycling Decision Tree

Database Schema for Recycling Management

Key Tables

Core Recycling Tables

Configuring Lead Recycling Strategies

Method 1: Campaign-Level Automatic Recycling

Method 2: Status-Based Recycling Rules

Method 3: Custom Recycling with AGI Scripts

Implementing Intelligent List Management

List Segmentation Strategy

Automatic List Purging and Archive

Dynamic Callback Scheduling

Monitoring and Reporting Lead Recycling

Real-Time Recycling Dashboard Query

Recycling Effectiveness Metrics

Lead Aging Report

Configuring Recycling via CLI Commands

Bulk Recycle Operations

Check Recycling Queue Status

Force Immediate Recycle

Advanced: Time-Zone Aware Recycling

Best Practices and Optimization

Lead Fatigue Prevention

Quality Assurance

Database Maintenance

Troubleshooting Lead Recycling Issues

Issue: Leads Not Recycling

Issue: Recycled Leads Not Dialing

Issue: Excessive Recycling / Lead Fatigue

Issue: Recycling Causing Database Lock

Summary Master advanced lead recycling strategies, implement intelligent list management workflows, and optimize your ViciDial contact center performance with production-tested configurations and real-world examples. Before implementing the strategies in this tutorial, ensure you have: Verify your installation: Lead recycling is the process of requeuing contacts after specific outcomes, allowing multiple attempts with different agents, strategies, or time intervals. Understanding the lead lifecycle is foundational to effective recycling. ViciDial uses standardized status codes stored in the vicidial_log table. Key statuses relevant to recycling: Understanding the database structure is essential for custom recycling logic. vicidial_list — Contains lead master records: Key fields for recycling: vicidial_log — Call attempt history: vicidial_campaign — Campaign configuration: Critical recycling fields: Access the campaign configuration and enable recycling globally: In the database, verify campaign recycling configuration: Update recycling method: Create granular recycling behavior through status configuration in vicidial_closer_log: This table determines which statuses trigger recycling. Insert recycling rules: For advanced recycling logic, implement AGI (Asterisk Gateway Interface) scripts. Create a custom recycling script: Call this AGI from your dialplan: Separate leads into logical groups for targeted recycling: Prevent list bloat by archiving old, unrecyclable leads: Implement time-zone aware callbacks for optimal contact: Monitor active recycling in real-time: Measure recycling performance: Identify leads requiring urgency or removal: Recycle entire batches of leads using command-line operations: Verify pending recycled leads: Manually trigger recycle for specific leads: Handle multi-zone campaigns properly: Avoid over-calling contacts: Monitor recycling quality metrics: Regular optimization for recycling performance: Symptom: Status remains unchanged after expected recycle interval. Symptom: RECYCLE status leads remain in queue but don't get dialed. Symptom: Leads dialed repeatedly in short intervals, agents complaining about repetition. Symptom: Database queries slow, connection timeout errors, agents report lag. ViciDial lead recycling is a sophisticated system requiring careful planning and ongoing optimization. The key takeaways: Understand Status Flow: Different statuses recycle at different intervals; configure per vicidial_closer_log. Database-Driven Configuration: Recycling behavior lives in vicidial_list, vicidial_log, and vicidial_campaign tables—customize via SQL. Time-Zone Awareness: Use gmt_offset_now and last_local_call_time to avoid off-hours contact. Prevent Lead Fatigue: Set attempt limits, monitor call frequency, and archive over-contacted leads. Monitor Performance: Track recycle-to-sale conversion rates, queue depths, and recycling effectiveness. Automate Maintenance: Use cron jobs and stored procedures to archive old leads, optimize tables, and maintain database health. Test Before Deploying: Always back up your database and test recycling changes in a staging environment first. Document Custom Logic: If implementing AGI scripts or complex procedures, maintain clear documentation for future maintenance. Effective lead recycling balances outreach frequency with lead quality, agent productivity, and customer satisfaction. Use the SQL queries and scripts provided here as templates, adjusting parameters for your specific campaign requirements. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

mysql -u root -p asterisk -e "SELECT VERSION();" asterisk -v grep "vicidial" /etc/asterisk/extensions-vicidial.conf | head -5 mysql -u root -p asterisk -e "SELECT VERSION();" asterisk -v grep "vicidial" /etc/asterisk/extensions-vicidial.conf | head -5 mysql -u root -p asterisk -e "SELECT VERSION();" asterisk -v grep "vicidial" /etc/asterisk/extensions-vicidial.conf | head -5 Lead Called ├─ Answer │ ├─ Sale → No recycle │ ├─ Not Interested → Recycle (optional) │ └─ Wrong Number → No recycle (flag DNC) ├─ No Answer │ ├─ Retry ≤ 3 → Recycle NOW │ ├─ Retry 4-5 → Recycle in 24h │ └─ Retry > 5 → Archive └─ Busy ├─ Retry ≤ 2 → Recycle in 30 min └─ Retry > 2 → Recycle in 2h Lead Called ├─ Answer │ ├─ Sale → No recycle │ ├─ Not Interested → Recycle (optional) │ └─ Wrong Number → No recycle (flag DNC) ├─ No Answer │ ├─ Retry ≤ 3 → Recycle NOW │ ├─ Retry 4-5 → Recycle in 24h │ └─ Retry > 5 → Archive └─ Busy ├─ Retry ≤ 2 → Recycle in 30 min └─ Retry > 2 → Recycle in 2h Lead Called ├─ Answer │ ├─ Sale → No recycle │ ├─ Not Interested → Recycle (optional) │ └─ Wrong Number → No recycle (flag DNC) ├─ No Answer │ ├─ Retry ≤ 3 → Recycle NOW │ ├─ Retry 4-5 → Recycle in 24h │ └─ Retry > 5 → Archive └─ Busy ├─ Retry ≤ 2 → Recycle in 30 min └─ Retry > 2 → Recycle in 2h -- View current lead status and recycle information SELECT vl.lead_id, vl.phone_number, vl.status, vl.call_date, vl.length_in_sec, vl.user, vl.campaign_id, COUNT(*) as attempt_count FROM vicidial_log vl WHERE vl.campaign_id = 'TESTCAMP' AND vl.phone_number = '5551234567' GROUP BY vl.lead_id ORDER BY vl.call_date DESC; -- View current lead status and recycle information SELECT vl.lead_id, vl.phone_number, vl.status, vl.call_date, vl.length_in_sec, vl.user, vl.campaign_id, COUNT(*) as attempt_count FROM vicidial_log vl WHERE vl.campaign_id = 'TESTCAMP' AND vl.phone_number = '5551234567' GROUP BY vl.lead_id ORDER BY vl.call_date DESC; -- View current lead status and recycle information SELECT vl.lead_id, vl.phone_number, vl.status, vl.call_date, vl.length_in_sec, vl.user, vl.campaign_id, COUNT(*) as attempt_count FROM vicidial_log vl WHERE vl.campaign_id = 'TESTCAMP' AND vl.phone_number = '5551234567' GROUP BY vl.lead_id ORDER BY vl.call_date DESC; DESC vicidial_list; DESC vicidial_list; DESC vicidial_list; DESC vicidial_log; DESC vicidial_log; DESC vicidial_log; DESC vicidial_campaign; DESC vicidial_campaign; DESC vicidial_campaign; # Log into ViciDial web interface # Navigate to: Admin → Campaigns → [Your Campaign] # Look for these settings: # Log into ViciDial web interface # Navigate to: Admin → Campaigns → [Your Campaign] # Look for these settings: # Log into ViciDial web interface # Navigate to: Admin → Campaigns → [Your Campaign] # Look for these settings: SELECT campaign_id, lead_recycle_method, auto_dial_level, voicemail_ext, hopper_level FROM vicidial_campaign WHERE campaign_id = 'TESTCAMP'; SELECT campaign_id, lead_recycle_method, auto_dial_level, voicemail_ext, hopper_level FROM vicidial_campaign WHERE campaign_id = 'TESTCAMP'; SELECT campaign_id, lead_recycle_method, auto_dial_level, voicemail_ext, hopper_level FROM vicidial_campaign WHERE campaign_id = 'TESTCAMP'; UPDATE vicidial_campaign SET lead_recycle_method = 1 WHERE campaign_id = 'TESTCAMP'; UPDATE vicidial_campaign SET lead_recycle_method = 1 WHERE campaign_id = 'TESTCAMP'; UPDATE vicidial_campaign SET lead_recycle_method = 1 WHERE campaign_id = 'TESTCAMP'; -- View current status recycling rules SELECT campaign_id, status, recyclable, rank FROM vicidial_closer_log WHERE campaign_id = 'TESTCAMP' ORDER BY status; -- View current status recycling rules SELECT campaign_id, status, recyclable, rank FROM vicidial_closer_log WHERE campaign_id = 'TESTCAMP' ORDER BY status; -- View current status recycling rules SELECT campaign_id, status, recyclable, rank FROM vicidial_closer_log WHERE campaign_id = 'TESTCAMP' ORDER BY status; -- Recycle NOANSWER leads after 30 minutes INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'NOANSWER', 'Y', 1800, 10) ON DUPLICATE KEY UPDATE recyclable = 'Y', seconds_to_next_call = 1800; -- Recycle BUSY after 15 minutes INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'BUSY', 'Y', 900, 20) ON DUPLICATE KEY UPDATE recyclable = 'Y', seconds_to_next_call = 900; -- Do NOT recycle DNCPROSPECT INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'DNCPROSPECT', 'N', 0, 5) ON DUPLICATE KEY UPDATE recyclable = 'N', seconds_to_next_call = 0; -- Recycle NOANSWER leads after 30 minutes INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'NOANSWER', 'Y', 1800, 10) ON DUPLICATE KEY UPDATE recyclable = 'Y', seconds_to_next_call = 1800; -- Recycle BUSY after 15 minutes INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'BUSY', 'Y', 900, 20) ON DUPLICATE KEY UPDATE recyclable = 'Y', seconds_to_next_call = 900; -- Do NOT recycle DNCPROSPECT INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'DNCPROSPECT', 'N', 0, 5) ON DUPLICATE KEY UPDATE recyclable = 'N', seconds_to_next_call = 0; -- Recycle NOANSWER leads after 30 minutes INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'NOANSWER', 'Y', 1800, 10) ON DUPLICATE KEY UPDATE recyclable = 'Y', seconds_to_next_call = 1800; -- Recycle BUSY after 15 minutes INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'BUSY', 'Y', 900, 20) ON DUPLICATE KEY UPDATE recyclable = 'Y', seconds_to_next_call = 900; -- Do NOT recycle DNCPROSPECT INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'DNCPROSPECT', 'N', 0, 5) ON DUPLICATE KEY UPDATE recyclable = 'N', seconds_to_next_call = 0; cat > /usr/share/astguiclient/agi-bin/recycle_custom.agi << 'EOF' #!/usr/bin/perl # Custom Lead Recycling AGI Script for ViciDial # Place in: /usr/share/astguiclient/agi-bin/ use strict; use warnings; use Asterisk::AGI; use DBI; my $agi = new Asterisk::AGI; my $lead_id = $agi->get_variable("lead_id"); my $campaign_id = $agi->get_variable("campaign_id"); my $status = $agi->get_variable("call_status"); my $attempt_count = $agi->get_variable("attempt_count"); # Connect to database my $dbh = DBI->connect( "DBI:mysql:asterisk:localhost", "asteriskuser", "DBPASSWORD", {AutoCommit => 1} ) or die "Cannot connect: $DBI::errstr"; # Recycling logic my $recycle = 0; my $recycle_seconds = 0; if ($status eq 'NOANSWER') { if ($attempt_count < 3) { $recycle = 1; $recycle_seconds = 1800; # 30 minutes } elsif ($attempt_count < 5) { $recycle = 1; $recycle_seconds = 86400; # 24 hours } } elsif ($status eq 'BUSY') { if ($attempt_count < 4) { $recycle = 1; $recycle_seconds = 900; # 15 minutes } } elsif ($status eq 'XFER' && $attempt_count < 2) { $recycle = 1; $recycle_seconds = 3600; # 1 hour } # Update vicidial_list with recycle status if ($recycle) { my $sth = $dbh->prepare( "UPDATE vicidial_list SET status = ?, last_local_call_time = DATE_ADD(NOW(), INTERVAL ? SECOND) WHERE lead_id = ?" ); $sth->execute('RECYCLE', $recycle_seconds, $lead_id); $agi->set_variable("recycled", 1); } $dbh->disconnect(); exit(0); EOF chmod +x /usr/share/astguiclient/agi-bin/recycle_custom.agi cat > /usr/share/astguiclient/agi-bin/recycle_custom.agi << 'EOF' #!/usr/bin/perl # Custom Lead Recycling AGI Script for ViciDial # Place in: /usr/share/astguiclient/agi-bin/ use strict; use warnings; use Asterisk::AGI; use DBI; my $agi = new Asterisk::AGI; my $lead_id = $agi->get_variable("lead_id"); my $campaign_id = $agi->get_variable("campaign_id"); my $status = $agi->get_variable("call_status"); my $attempt_count = $agi->get_variable("attempt_count"); # Connect to database my $dbh = DBI->connect( "DBI:mysql:asterisk:localhost", "asteriskuser", "DBPASSWORD", {AutoCommit => 1} ) or die "Cannot connect: $DBI::errstr"; # Recycling logic my $recycle = 0; my $recycle_seconds = 0; if ($status eq 'NOANSWER') { if ($attempt_count < 3) { $recycle = 1; $recycle_seconds = 1800; # 30 minutes } elsif ($attempt_count < 5) { $recycle = 1; $recycle_seconds = 86400; # 24 hours } } elsif ($status eq 'BUSY') { if ($attempt_count < 4) { $recycle = 1; $recycle_seconds = 900; # 15 minutes } } elsif ($status eq 'XFER' && $attempt_count < 2) { $recycle = 1; $recycle_seconds = 3600; # 1 hour } # Update vicidial_list with recycle status if ($recycle) { my $sth = $dbh->prepare( "UPDATE vicidial_list SET status = ?, last_local_call_time = DATE_ADD(NOW(), INTERVAL ? SECOND) WHERE lead_id = ?" ); $sth->execute('RECYCLE', $recycle_seconds, $lead_id); $agi->set_variable("recycled", 1); } $dbh->disconnect(); exit(0); EOF chmod +x /usr/share/astguiclient/agi-bin/recycle_custom.agi cat > /usr/share/astguiclient/agi-bin/recycle_custom.agi << 'EOF' #!/usr/bin/perl # Custom Lead Recycling AGI Script for ViciDial # Place in: /usr/share/astguiclient/agi-bin/ use strict; use warnings; use Asterisk::AGI; use DBI; my $agi = new Asterisk::AGI; my $lead_id = $agi->get_variable("lead_id"); my $campaign_id = $agi->get_variable("campaign_id"); my $status = $agi->get_variable("call_status"); my $attempt_count = $agi->get_variable("attempt_count"); # Connect to database my $dbh = DBI->connect( "DBI:mysql:asterisk:localhost", "asteriskuser", "DBPASSWORD", {AutoCommit => 1} ) or die "Cannot connect: $DBI::errstr"; # Recycling logic my $recycle = 0; my $recycle_seconds = 0; if ($status eq 'NOANSWER') { if ($attempt_count < 3) { $recycle = 1; $recycle_seconds = 1800; # 30 minutes } elsif ($attempt_count < 5) { $recycle = 1; $recycle_seconds = 86400; # 24 hours } } elsif ($status eq 'BUSY') { if ($attempt_count < 4) { $recycle = 1; $recycle_seconds = 900; # 15 minutes } } elsif ($status eq 'XFER' && $attempt_count < 2) { $recycle = 1; $recycle_seconds = 3600; # 1 hour } # Update vicidial_list with recycle status if ($recycle) { my $sth = $dbh->prepare( "UPDATE vicidial_list SET status = ?, last_local_call_time = DATE_ADD(NOW(), INTERVAL ? SECOND) WHERE lead_id = ?" ); $sth->execute('RECYCLE', $recycle_seconds, $lead_id); $agi->set_variable("recycled", 1); } $dbh->disconnect(); exit(0); EOF chmod +x /usr/share/astguiclient/agi-bin/recycle_custom.agi ; Add to /etc/asterisk/extensions-vicidial.conf [vicidial-handler] exten => s,1,AGI(agi-bin/recycle_custom.agi) exten => s,n,Hangup() ; Add to /etc/asterisk/extensions-vicidial.conf [vicidial-handler] exten => s,1,AGI(agi-bin/recycle_custom.agi) exten => s,n,Hangup() ; Add to /etc/asterisk/extensions-vicidial.conf [vicidial-handler] exten => s,1,AGI(agi-bin/recycle_custom.agi) exten => s,n,Hangup() -- Create a custom list segmentation view CREATE OR REPLACE VIEW lead_segments AS SELECT vl.lead_id, vl.phone_number, vl.list_id, vl.campaign_id, CASE WHEN COUNT(*) <= 1 THEN 'FRESH' WHEN COUNT(*) BETWEEN 2 AND 3 THEN 'WARM' WHEN COUNT(*) > 3 THEN 'HOT' END as segment, COUNT(*) as attempt_count, MAX(vlog.call_date) as last_attempt FROM vicidial_list vl LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id GROUP BY vl.lead_id; -- Query segmented leads SELECT * FROM lead_segments WHERE campaign_id = 'TESTCAMP' AND segment = 'FRESH'; -- Create a custom list segmentation view CREATE OR REPLACE VIEW lead_segments AS SELECT vl.lead_id, vl.phone_number, vl.list_id, vl.campaign_id, CASE WHEN COUNT(*) <= 1 THEN 'FRESH' WHEN COUNT(*) BETWEEN 2 AND 3 THEN 'WARM' WHEN COUNT(*) > 3 THEN 'HOT' END as segment, COUNT(*) as attempt_count, MAX(vlog.call_date) as last_attempt FROM vicidial_list vl LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id GROUP BY vl.lead_id; -- Query segmented leads SELECT * FROM lead_segments WHERE campaign_id = 'TESTCAMP' AND segment = 'FRESH'; -- Create a custom list segmentation view CREATE OR REPLACE VIEW lead_segments AS SELECT vl.lead_id, vl.phone_number, vl.list_id, vl.campaign_id, CASE WHEN COUNT(*) <= 1 THEN 'FRESH' WHEN COUNT(*) BETWEEN 2 AND 3 THEN 'WARM' WHEN COUNT(*) > 3 THEN 'HOT' END as segment, COUNT(*) as attempt_count, MAX(vlog.call_date) as last_attempt FROM vicidial_list vl LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id GROUP BY vl.lead_id; -- Query segmented leads SELECT * FROM lead_segments WHERE campaign_id = 'TESTCAMP' AND segment = 'FRESH'; -- Archive DEAD leads older than 60 days CREATE PROCEDURE archive_old_leads() BEGIN INSERT INTO vicidial_list_archive SELECT * FROM vicidial_list WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER') AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date; DELETE FROM vicidial_list WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER') AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date; END; -- Execute once daily via cron CALL archive_old_leads(); -- Archive DEAD leads older than 60 days CREATE PROCEDURE archive_old_leads() BEGIN INSERT INTO vicidial_list_archive SELECT * FROM vicidial_list WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER') AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date; DELETE FROM vicidial_list WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER') AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date; END; -- Execute once daily via cron CALL archive_old_leads(); -- Archive DEAD leads older than 60 days CREATE PROCEDURE archive_old_leads() BEGIN INSERT INTO vicidial_list_archive SELECT * FROM vicidial_list WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER') AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date; DELETE FROM vicidial_list WHERE status IN ('DEAD', 'NOTCALLABLE', 'BADNUMBER') AND DATE_SUB(NOW(), INTERVAL 60 DAY) > created_date; END; -- Execute once daily via cron CALL archive_old_leads(); # Run daily at 2 AM 0 2 * * * mysql -u root -p'PASSWORD' asterisk -e "CALL archive_old_leads;" # Run daily at 2 AM 0 2 * * * mysql -u root -p'PASSWORD' asterisk -e "CALL archive_old_leads;" # Run daily at 2 AM 0 2 * * * mysql -u root -p'PASSWORD' asterisk -e "CALL archive_old_leads;" -- Schedule callbacks during business hours (9 AM - 6 PM local time) UPDATE vicidial_list vl SET status = 'CALLBACK', last_local_call_time = DATE_ADD( NOW(), INTERVAL ( (9 - HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago'))) + IF(HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago')) >= 18, 24, 0) ) HOUR ) WHERE lead_id IN ( SELECT lead_id FROM vicidial_log WHERE status = 'CALLBACK' AND DATE_SUB(NOW(), INTERVAL 24 HOUR) < call_date ) AND campaign_id = 'TESTCAMP'; -- Schedule callbacks during business hours (9 AM - 6 PM local time) UPDATE vicidial_list vl SET status = 'CALLBACK', last_local_call_time = DATE_ADD( NOW(), INTERVAL ( (9 - HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago'))) + IF(HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago')) >= 18, 24, 0) ) HOUR ) WHERE lead_id IN ( SELECT lead_id FROM vicidial_log WHERE status = 'CALLBACK' AND DATE_SUB(NOW(), INTERVAL 24 HOUR) < call_date ) AND campaign_id = 'TESTCAMP'; -- Schedule callbacks during business hours (9 AM - 6 PM local time) UPDATE vicidial_list vl SET status = 'CALLBACK', last_local_call_time = DATE_ADD( NOW(), INTERVAL ( (9 - HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago'))) + IF(HOUR(CONVERT_TZ(NOW(), 'UTC', 'America/Chicago')) >= 18, 24, 0) ) HOUR ) WHERE lead_id IN ( SELECT lead_id FROM vicidial_log WHERE status = 'CALLBACK' AND DATE_SUB(NOW(), INTERVAL 24 HOUR) < call_date ) AND campaign_id = 'TESTCAMP'; SELECT COUNT(DISTINCT lead_id) as recyclable_leads, SUM(CASE WHEN status = 'RECYCLE' THEN 1 ELSE 0 END) as pending_recycle, SUM(CASE WHEN status = 'CALLBACK' THEN 1 ELSE 0 END) as scheduled_callbacks, campaign_id, MAX(last_local_call_time) as next_available FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status IN ('RECYCLE', 'CALLBACK', 'BUSY', 'NOANSWER') GROUP BY campaign_id; SELECT COUNT(DISTINCT lead_id) as recyclable_leads, SUM(CASE WHEN status = 'RECYCLE' THEN 1 ELSE 0 END) as pending_recycle, SUM(CASE WHEN status = 'CALLBACK' THEN 1 ELSE 0 END) as scheduled_callbacks, campaign_id, MAX(last_local_call_time) as next_available FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status IN ('RECYCLE', 'CALLBACK', 'BUSY', 'NOANSWER') GROUP BY campaign_id; SELECT COUNT(DISTINCT lead_id) as recyclable_leads, SUM(CASE WHEN status = 'RECYCLE' THEN 1 ELSE 0 END) as pending_recycle, SUM(CASE WHEN status = 'CALLBACK' THEN 1 ELSE 0 END) as scheduled_callbacks, campaign_id, MAX(last_local_call_time) as next_available FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status IN ('RECYCLE', 'CALLBACK', 'BUSY', 'NOANSWER') GROUP BY campaign_id; -- Calculate recycle-to-sale conversion rate SELECT vlog.campaign_id, COUNT(DISTINCT vlog.lead_id) as total_attempts, COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) as sales, ROUND( (COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) / COUNT(DISTINCT vlog.lead_id) * 100), 2 ) as conversion_rate_pct, AVG(CASE WHEN vlog.status = 'SALE' THEN vlog.length_in_sec ELSE NULL END) as avg_sale_duration FROM vicidial_log vlog WHERE vlog.campaign_id = 'TESTCAMP' AND vlog.call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY vlog.campaign_id; -- Calculate recycle-to-sale conversion rate SELECT vlog.campaign_id, COUNT(DISTINCT vlog.lead_id) as total_attempts, COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) as sales, ROUND( (COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) / COUNT(DISTINCT vlog.lead_id) * 100), 2 ) as conversion_rate_pct, AVG(CASE WHEN vlog.status = 'SALE' THEN vlog.length_in_sec ELSE NULL END) as avg_sale_duration FROM vicidial_log vlog WHERE vlog.campaign_id = 'TESTCAMP' AND vlog.call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY vlog.campaign_id; -- Calculate recycle-to-sale conversion rate SELECT vlog.campaign_id, COUNT(DISTINCT vlog.lead_id) as total_attempts, COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) as sales, ROUND( (COUNT(DISTINCT CASE WHEN vlog.status = 'SALE' THEN vlog.lead_id END) / COUNT(DISTINCT vlog.lead_id) * 100), 2 ) as conversion_rate_pct, AVG(CASE WHEN vlog.status = 'SALE' THEN vlog.length_in_sec ELSE NULL END) as avg_sale_duration FROM vicidial_log vlog WHERE vlog.campaign_id = 'TESTCAMP' AND vlog.call_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY vlog.campaign_id; -- List leads by age and status SELECT vl.lead_id, vl.phone_number, vl.status, DATEDIFF(NOW(), vl.created_date) as days_in_system, COUNT(vlog.lead_id) as total_attempts, MAX(vlog.call_date) as last_attempt, DATEDIFF(NOW(), MAX(vlog.call_date)) as days_since_last_attempt FROM vicidial_list vl LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id WHERE vl.campaign_id = 'TESTCAMP' GROUP BY vl.lead_id HAVING days_in_system > 90 ORDER BY last_attempt DESC; -- List leads by age and status SELECT vl.lead_id, vl.phone_number, vl.status, DATEDIFF(NOW(), vl.created_date) as days_in_system, COUNT(vlog.lead_id) as total_attempts, MAX(vlog.call_date) as last_attempt, DATEDIFF(NOW(), MAX(vlog.call_date)) as days_since_last_attempt FROM vicidial_list vl LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id WHERE vl.campaign_id = 'TESTCAMP' GROUP BY vl.lead_id HAVING days_in_system > 90 ORDER BY last_attempt DESC; -- List leads by age and status SELECT vl.lead_id, vl.phone_number, vl.status, DATEDIFF(NOW(), vl.created_date) as days_in_system, COUNT(vlog.lead_id) as total_attempts, MAX(vlog.call_date) as last_attempt, DATEDIFF(NOW(), MAX(vlog.call_date)) as days_since_last_attempt FROM vicidial_list vl LEFT JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id WHERE vl.campaign_id = 'TESTCAMP' GROUP BY vl.lead_id HAVING days_in_system > 90 ORDER BY last_attempt DESC; # Recycle all NOANSWER leads from a specific campaign mysql -u asteriskuser -p'PASSWORD' asterisk << EOF UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = DATE_ADD(NOW(), INTERVAL 1800 SECOND) WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' AND DATE_SUB(NOW(), INTERVAL 6 HOUR) > last_local_call_time; EOF # Recycle all NOANSWER leads from a specific campaign mysql -u asteriskuser -p'PASSWORD' asterisk << EOF UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = DATE_ADD(NOW(), INTERVAL 1800 SECOND) WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' AND DATE_SUB(NOW(), INTERVAL 6 HOUR) > last_local_call_time; EOF # Recycle all NOANSWER leads from a specific campaign mysql -u asteriskuser -p'PASSWORD' asterisk << EOF UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = DATE_ADD(NOW(), INTERVAL 1800 SECOND) WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' AND DATE_SUB(NOW(), INTERVAL 6 HOUR) > last_local_call_time; EOF # Connect to Asterisk CLI and check lead queue asterisk -rx "agi show" | grep -i recycle # Monitor ViciDial dialer process ps aux | grep -E "vici_|astguiclient" # Check for recycling script errors tail -50 /var/log/asterisk/messages | grep -i recycle # Connect to Asterisk CLI and check lead queue asterisk -rx "agi show" | grep -i recycle # Monitor ViciDial dialer process ps aux | grep -E "vici_|astguiclient" # Check for recycling script errors tail -50 /var/log/asterisk/messages | grep -i recycle # Connect to Asterisk CLI and check lead queue asterisk -rx "agi show" | grep -i recycle # Monitor ViciDial dialer process ps aux | grep -E "vici_|astguiclient" # Check for recycling script errors tail -50 /var/log/asterisk/messages | grep -i recycle # SSH to ViciDial server # Run manual recycle operation mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = NOW() WHERE lead_id = 12345 AND campaign_id = 'TESTCAMP';" # Verify update mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, last_local_call_time FROM vicidial_list WHERE lead_id = 12345;" # SSH to ViciDial server # Run manual recycle operation mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = NOW() WHERE lead_id = 12345 AND campaign_id = 'TESTCAMP';" # Verify update mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, last_local_call_time FROM vicidial_list WHERE lead_id = 12345;" # SSH to ViciDial server # Run manual recycle operation mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = NOW() WHERE lead_id = 12345 AND campaign_id = 'TESTCAMP';" # Verify update mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, last_local_call_time FROM vicidial_list WHERE lead_id = 12345;" -- Create time-zone aware recycling procedure DELIMITER $$ CREATE PROCEDURE recycle_by_timezone( IN p_campaign_id VARCHAR(20) ) BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_lead_id INT; DECLARE v_phone VARCHAR(20); DECLARE v_gmt_offset INT; DECLARE v_local_hour INT; DECLARE cur CURSOR FOR SELECT lead_id, phone_number, gmt_offset_now FROM vicidial_list WHERE campaign_id = p_campaign_id AND status IN ('NOANSWER', 'BUSY'); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_lead_id, v_phone, v_gmt_offset; IF done THEN LEAVE read_loop; END IF; -- Calculate local hour for this lead's timezone SET v_local_hour = HOUR(DATE_ADD(NOW(), INTERVAL v_gmt_offset HOUR)); -- Only recycle during business hours (9 AM - 6 PM) IF v_local_hour >= 9 AND v_local_hour < 18 THEN UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = NOW() WHERE lead_id = v_lead_id; END IF; END LOOP; CLOSE cur; END$$ DELIMITER ; -- Execute CALL recycle_by_timezone('TESTCAMP'); -- Create time-zone aware recycling procedure DELIMITER $$ CREATE PROCEDURE recycle_by_timezone( IN p_campaign_id VARCHAR(20) ) BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_lead_id INT; DECLARE v_phone VARCHAR(20); DECLARE v_gmt_offset INT; DECLARE v_local_hour INT; DECLARE cur CURSOR FOR SELECT lead_id, phone_number, gmt_offset_now FROM vicidial_list WHERE campaign_id = p_campaign_id AND status IN ('NOANSWER', 'BUSY'); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_lead_id, v_phone, v_gmt_offset; IF done THEN LEAVE read_loop; END IF; -- Calculate local hour for this lead's timezone SET v_local_hour = HOUR(DATE_ADD(NOW(), INTERVAL v_gmt_offset HOUR)); -- Only recycle during business hours (9 AM - 6 PM) IF v_local_hour >= 9 AND v_local_hour < 18 THEN UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = NOW() WHERE lead_id = v_lead_id; END IF; END LOOP; CLOSE cur; END$$ DELIMITER ; -- Execute CALL recycle_by_timezone('TESTCAMP'); -- Create time-zone aware recycling procedure DELIMITER $$ CREATE PROCEDURE recycle_by_timezone( IN p_campaign_id VARCHAR(20) ) BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_lead_id INT; DECLARE v_phone VARCHAR(20); DECLARE v_gmt_offset INT; DECLARE v_local_hour INT; DECLARE cur CURSOR FOR SELECT lead_id, phone_number, gmt_offset_now FROM vicidial_list WHERE campaign_id = p_campaign_id AND status IN ('NOANSWER', 'BUSY'); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_lead_id, v_phone, v_gmt_offset; IF done THEN LEAVE read_loop; END IF; -- Calculate local hour for this lead's timezone SET v_local_hour = HOUR(DATE_ADD(NOW(), INTERVAL v_gmt_offset HOUR)); -- Only recycle during business hours (9 AM - 6 PM) IF v_local_hour >= 9 AND v_local_hour < 18 THEN UPDATE vicidial_list SET status = 'RECYCLE', last_local_call_time = NOW() WHERE lead_id = v_lead_id; END IF; END LOOP; CLOSE cur; END$$ DELIMITER ; -- Execute CALL recycle_by_timezone('TESTCAMP'); -- Set maximum attempts per lead ALTER TABLE vicidial_list ADD COLUMN max_attempts INT DEFAULT 5; -- Prevent recycling beyond maximum UPDATE vicidial_list SET status = 'MAXED' WHERE (SELECT COUNT(*) FROM vicidial_log WHERE lead_id = vicidial_list.lead_id) >= max_attempts AND campaign_id = 'TESTCAMP'; -- Set maximum attempts per lead ALTER TABLE vicidial_list ADD COLUMN max_attempts INT DEFAULT 5; -- Prevent recycling beyond maximum UPDATE vicidial_list SET status = 'MAXED' WHERE (SELECT COUNT(*) FROM vicidial_log WHERE lead_id = vicidial_list.lead_id) >= max_attempts AND campaign_id = 'TESTCAMP'; -- Set maximum attempts per lead ALTER TABLE vicidial_list ADD COLUMN max_attempts INT DEFAULT 5; -- Prevent recycling beyond maximum UPDATE vicidial_list SET status = 'MAXED' WHERE (SELECT COUNT(*) FROM vicidial_log WHERE lead_id = vicidial_list.lead_id) >= max_attempts AND campaign_id = 'TESTCAMP'; #!/bin/bash # Script: check_recycle_health.sh CAMPAIGN="TESTCAMP" THRESHOLD=10 # Alert if recycle queue exceeds 10% TOTAL_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \ -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN';") RECYCLE_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \ -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN' AND status='RECYCLE';") PERCENTAGE=$((RECYCLE_LEADS * 100 / TOTAL_LEADS)) echo "Campaign: $CAMPAIGN" echo "Total Leads: $TOTAL_LEADS" echo "Recycled Leads: $RECYCLE_LEADS" echo "Recycle Queue: $PERCENTAGE%" if [ $PERCENTAGE -gt $THRESHOLD ]; then echo "WARNING: Recycle queue exceeds $THRESHOLD% threshold!" fi #!/bin/bash # Script: check_recycle_health.sh CAMPAIGN="TESTCAMP" THRESHOLD=10 # Alert if recycle queue exceeds 10% TOTAL_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \ -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN';") RECYCLE_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \ -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN' AND status='RECYCLE';") PERCENTAGE=$((RECYCLE_LEADS * 100 / TOTAL_LEADS)) echo "Campaign: $CAMPAIGN" echo "Total Leads: $TOTAL_LEADS" echo "Recycled Leads: $RECYCLE_LEADS" echo "Recycle Queue: $PERCENTAGE%" if [ $PERCENTAGE -gt $THRESHOLD ]; then echo "WARNING: Recycle queue exceeds $THRESHOLD% threshold!" fi #!/bin/bash # Script: check_recycle_health.sh CAMPAIGN="TESTCAMP" THRESHOLD=10 # Alert if recycle queue exceeds 10% TOTAL_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \ -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN';") RECYCLE_LEADS=$(mysql -N -u asteriskuser -p'PASSWORD' asterisk \ -e "SELECT COUNT(*) FROM vicidial_list WHERE campaign_id='$CAMPAIGN' AND status='RECYCLE';") PERCENTAGE=$((RECYCLE_LEADS * 100 / TOTAL_LEADS)) echo "Campaign: $CAMPAIGN" echo "Total Leads: $TOTAL_LEADS" echo "Recycled Leads: $RECYCLE_LEADS" echo "Recycle Queue: $PERCENTAGE%" if [ $PERCENTAGE -gt $THRESHOLD ]; then echo "WARNING: Recycle queue exceeds $THRESHOLD% threshold!" fi 0 * * * * /usr/local/bin/check_recycle_health.sh | mail -s "Recycle Health Alert" [email protected] 0 * * * * /usr/local/bin/check_recycle_health.sh | mail -s "Recycle Health Alert" [email protected] 0 * * * * /usr/local/bin/check_recycle_health.sh | mail -s "Recycle Health Alert" [email protected] #!/bin/bash # Add to crontab - Run weekly mysql -u asteriskuser -p'PASSWORD' asterisk << EOF -- Optimize tables for query performance OPTIMIZE TABLE vicidial_list; OPTIMIZE TABLE vicidial_log; OPTIMIZE TABLE vicidial_closer_log; -- Rebuild indexes ANALYZE TABLE vicidial_list; ANALYZE TABLE vicidial_log; -- Check for corrupted records CHECK TABLE vicidial_list; CHECK TABLE vicidial_log; EOF #!/bin/bash # Add to crontab - Run weekly mysql -u asteriskuser -p'PASSWORD' asterisk << EOF -- Optimize tables for query performance OPTIMIZE TABLE vicidial_list; OPTIMIZE TABLE vicidial_log; OPTIMIZE TABLE vicidial_closer_log; -- Rebuild indexes ANALYZE TABLE vicidial_list; ANALYZE TABLE vicidial_log; -- Check for corrupted records CHECK TABLE vicidial_list; CHECK TABLE vicidial_log; EOF #!/bin/bash # Add to crontab - Run weekly mysql -u asteriskuser -p'PASSWORD' asterisk << EOF -- Optimize tables for query performance OPTIMIZE TABLE vicidial_list; OPTIMIZE TABLE vicidial_log; OPTIMIZE TABLE vicidial_closer_log; -- Rebuild indexes ANALYZE TABLE vicidial_list; ANALYZE TABLE vicidial_log; -- Check for corrupted records CHECK TABLE vicidial_list; CHECK TABLE vicidial_log; EOF # 1. Check if campaign recycling is enabled mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT campaign_id, lead_recycle_method FROM vicidial_campaign WHERE campaign_id='TESTCAMP';" # Expected output: lead_recycle_method should be 1 or higher # 2. Verify vicidial_dialer process is running ps aux | grep vicidial | grep -v grep # 3. Check for errors in logs tail -100 /var/log/asterisk/messages | grep -i recycle grep "ERROR\|recycle\|RECYCLE" /var/log/asterisk/messages | tail -20 # 1. Check if campaign recycling is enabled mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT campaign_id, lead_recycle_method FROM vicidial_campaign WHERE campaign_id='TESTCAMP';" # Expected output: lead_recycle_method should be 1 or higher # 2. Verify vicidial_dialer process is running ps aux | grep vicidial | grep -v grep # 3. Check for errors in logs tail -100 /var/log/asterisk/messages | grep -i recycle grep "ERROR\|recycle\|RECYCLE" /var/log/asterisk/messages | tail -20 # 1. Check if campaign recycling is enabled mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT campaign_id, lead_recycle_method FROM vicidial_campaign WHERE campaign_id='TESTCAMP';" # Expected output: lead_recycle_method should be 1 or higher # 2. Verify vicidial_dialer process is running ps aux | grep vicidial | grep -v grep # 3. Check for errors in logs tail -100 /var/log/asterisk/messages | grep -i recycle grep "ERROR\|recycle\|RECYCLE" /var/log/asterisk/messages | tail -20 # Enable recycling in campaign mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_campaign SET lead_recycle_method = 1 WHERE campaign_id = 'TESTCAMP';" # Restart vicidial_dialer service sudo service vicidial_dialer restart # Monitor service startup tail -f /var/log/asterisk/messages | grep vicidial # Enable recycling in campaign mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_campaign SET lead_recycle_method = 1 WHERE campaign_id = 'TESTCAMP';" # Restart vicidial_dialer service sudo service vicidial_dialer restart # Monitor service startup tail -f /var/log/asterisk/messages | grep vicidial # Enable recycling in campaign mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_campaign SET lead_recycle_method = 1 WHERE campaign_id = 'TESTCAMP';" # Restart vicidial_dialer service sudo service vicidial_dialer restart # Monitor service startup tail -f /var/log/asterisk/messages | grep vicidial # Check hopper queue depth mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT COUNT(*) as hopper_count FROM vicidial_hopper WHERE campaign_id='TESTCAMP';" # View next leads to be dialed mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, last_local_call_time FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status = 'RECYCLE' ORDER BY last_local_call_time LIMIT 10;" # Check if leads meet recycle time requirement mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, TIMESTAMPDIFF(SECOND, NOW(), last_local_call_time) as seconds_until_recycle FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status = 'RECYCLE' LIMIT 5;" # Check hopper queue depth mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT COUNT(*) as hopper_count FROM vicidial_hopper WHERE campaign_id='TESTCAMP';" # View next leads to be dialed mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, last_local_call_time FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status = 'RECYCLE' ORDER BY last_local_call_time LIMIT 10;" # Check if leads meet recycle time requirement mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, TIMESTAMPDIFF(SECOND, NOW(), last_local_call_time) as seconds_until_recycle FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status = 'RECYCLE' LIMIT 5;" # Check hopper queue depth mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT COUNT(*) as hopper_count FROM vicidial_hopper WHERE campaign_id='TESTCAMP';" # View next leads to be dialed mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, last_local_call_time FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status = 'RECYCLE' ORDER BY last_local_call_time LIMIT 10;" # Check if leads meet recycle time requirement mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT lead_id, phone_number, status, TIMESTAMPDIFF(SECOND, NOW(), last_local_call_time) as seconds_until_recycle FROM vicidial_list WHERE campaign_id = 'TESTCAMP' AND status = 'RECYCLE' LIMIT 5;" # If leads show negative seconds_until_recycle, they're ready # Check if hopper is full (capacity issue) mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT * FROM vicidial_campaign WHERE campaign_id = 'TESTCAMP';" | grep -E "hopper_level|lead_recycle" # If hopper is too small, update it mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_campaign SET hopper_level = 200 WHERE campaign_id = 'TESTCAMP';" # Force immediate hopper fill asterisk -rx "vicidial hopper $CAMPAIGN" # If leads show negative seconds_until_recycle, they're ready # Check if hopper is full (capacity issue) mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT * FROM vicidial_campaign WHERE campaign_id = 'TESTCAMP';" | grep -E "hopper_level|lead_recycle" # If hopper is too small, update it mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_campaign SET hopper_level = 200 WHERE campaign_id = 'TESTCAMP';" # Force immediate hopper fill asterisk -rx "vicidial hopper $CAMPAIGN" # If leads show negative seconds_until_recycle, they're ready # Check if hopper is full (capacity issue) mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT * FROM vicidial_campaign WHERE campaign_id = 'TESTCAMP';" | grep -E "hopper_level|lead_recycle" # If hopper is too small, update it mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_campaign SET hopper_level = 200 WHERE campaign_id = 'TESTCAMP';" # Force immediate hopper fill asterisk -rx "vicidial hopper $CAMPAIGN" # Identify heavily recycled leads mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT vl.lead_id, vl.phone_number, COUNT(*) as call_attempts, MIN(vlog.call_date) as first_call, MAX(vlog.call_date) as last_call, TIMESTAMPDIFF(DAY, MIN(vlog.call_date), MAX(vlog.call_date)) as days_span FROM vicidial_list vl INNER JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id WHERE vl.campaign_id = 'TESTCAMP' GROUP BY vl.lead_id HAVING call_attempts > 10 ORDER BY call_attempts DESC LIMIT 20;" # Identify heavily recycled leads mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT vl.lead_id, vl.phone_number, COUNT(*) as call_attempts, MIN(vlog.call_date) as first_call, MAX(vlog.call_date) as last_call, TIMESTAMPDIFF(DAY, MIN(vlog.call_date), MAX(vlog.call_date)) as days_span FROM vicidial_list vl INNER JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id WHERE vl.campaign_id = 'TESTCAMP' GROUP BY vl.lead_id HAVING call_attempts > 10 ORDER BY call_attempts DESC LIMIT 20;" # Identify heavily recycled leads mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "SELECT vl.lead_id, vl.phone_number, COUNT(*) as call_attempts, MIN(vlog.call_date) as first_call, MAX(vlog.call_date) as last_call, TIMESTAMPDIFF(DAY, MIN(vlog.call_date), MAX(vlog.call_date)) as days_span FROM vicidial_list vl INNER JOIN vicidial_log vlog ON vl.lead_id = vlog.lead_id WHERE vl.campaign_id = 'TESTCAMP' GROUP BY vl.lead_id HAVING call_attempts > 10 ORDER BY call_attempts DESC LIMIT 20;" # Implement attempt limits in closer_log mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'NOANSWER', 'Y', 3600, 10) ON DUPLICATE KEY UPDATE seconds_to_next_call = 3600;" # Archive over-attempted leads mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_list vl SET status = 'ARCHIVE' WHERE vl.campaign_id = 'TESTCAMP' AND (SELECT COUNT(*) FROM vicidial_log WHERE lead_id = vl.lead_id) > 8;" # Implement attempt limits in closer_log mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'NOANSWER', 'Y', 3600, 10) ON DUPLICATE KEY UPDATE seconds_to_next_call = 3600;" # Archive over-attempted leads mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_list vl SET status = 'ARCHIVE' WHERE vl.campaign_id = 'TESTCAMP' AND (SELECT COUNT(*) FROM vicidial_log WHERE lead_id = vl.lead_id) > 8;" # Implement attempt limits in closer_log mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "INSERT INTO vicidial_closer_log (campaign_id, status, recyclable, seconds_to_next_call, rank) VALUES ('TESTCAMP', 'NOANSWER', 'Y', 3600, 10) ON DUPLICATE KEY UPDATE seconds_to_next_call = 3600;" # Archive over-attempted leads mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "UPDATE vicidial_list vl SET status = 'ARCHIVE' WHERE vl.campaign_id = 'TESTCAMP' AND (SELECT COUNT(*) FROM vicidial_log WHERE lead_id = vl.lead_id) > 8;" # Check for long-running queries mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW PROCESSLIST;" | grep -i UPDATE # Monitor table locks mysqladmin -u asteriskuser -p'PASSWORD' -i 2 processlist | grep -E "UPDATE|LOCK" # Check innodb status mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW ENGINE INNODB STATUS\G" | grep -A 20 "LATEST DETECTED DEADLOCK" # Check for long-running queries mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW PROCESSLIST;" | grep -i UPDATE # Monitor table locks mysqladmin -u asteriskuser -p'PASSWORD' -i 2 processlist | grep -E "UPDATE|LOCK" # Check innodb status mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW ENGINE INNODB STATUS\G" | grep -A 20 "LATEST DETECTED DEADLOCK" # Check for long-running queries mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW PROCESSLIST;" | grep -i UPDATE # Monitor table locks mysqladmin -u asteriskuser -p'PASSWORD' -i 2 processlist | grep -E "UPDATE|LOCK" # Check innodb status mysql -u asteriskuser -p'PASSWORD' asterisk -e "SHOW ENGINE INNODB STATUS\G" | grep -A 20 "LATEST DETECTED DEADLOCK" # Implement batching in recycling procedures mysql -u asteriskuser -p'PASSWORD' asterisk << EOF -- Update in smaller chunks to reduce lock time SET @batch_size = 100; UPDATE vicidial_list vl SET status = 'RECYCLE' WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' AND lead_id IN ( SELECT lead_id FROM ( SELECT lead_id FROM vicidial_list vl2 WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' LIMIT @batch_size ) tmp ); EOF # Add index for faster recycling queries mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "ALTER TABLE vicidial_list ADD INDEX idx_recycle (campaign_id, status, last_local_call_time);" # Implement batching in recycling procedures mysql -u asteriskuser -p'PASSWORD' asterisk << EOF -- Update in smaller chunks to reduce lock time SET @batch_size = 100; UPDATE vicidial_list vl SET status = 'RECYCLE' WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' AND lead_id IN ( SELECT lead_id FROM ( SELECT lead_id FROM vicidial_list vl2 WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' LIMIT @batch_size ) tmp ); EOF # Add index for faster recycling queries mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "ALTER TABLE vicidial_list ADD INDEX idx_recycle (campaign_id, status, last_local_call_time);" # Implement batching in recycling procedures mysql -u asteriskuser -p'PASSWORD' asterisk << EOF -- Update in smaller chunks to reduce lock time SET @batch_size = 100; UPDATE vicidial_list vl SET status = 'RECYCLE' WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' AND lead_id IN ( SELECT lead_id FROM ( SELECT lead_id FROM vicidial_list vl2 WHERE campaign_id = 'TESTCAMP' AND status = 'NOANSWER' LIMIT @batch_size ) tmp ); EOF # Add index for faster recycling queries mysql -u asteriskuser -p'PASSWORD' asterisk -e \ "ALTER TABLE vicidial_list ADD INDEX idx_recycle (campaign_id, status, last_local_call_time);" - ViciDial 2.14+ installed and running (2.14.1 or later recommended) - Administrator access to the ViciDial web interface at /vicidial/admin.php - SSH access to the ViciDial server with sudo privileges - MySQL/MariaDB command-line access to the asterisk database - At least one active campaign configured - Basic understanding of ViciDial list structure and lead statuses - Backup of your database before implementing changes - lead_id — Unique identifier - phone_number — Contact number - status — Current lead status - list_id — Associated campaign list - gmt_offset_now — Timezone for callback scheduling - created_date — Lead entry date - last_local_call_time — Last contact attempt timestamp - lead_id — Links to vicidial_list - call_date — Attempt timestamp - status — Outcome status - length_in_sec — Duration - user — Agent identifier - campaign_id — Campaign code - phone_number — Dialed number - call_type — INBOUND/OUTBOUND - campaign_id — Unique identifier - lead_recycle_method — Recycling strategy - reuse_trunk_level — Trunk reuse behavior - caller_id_method — Caller ID assignment - 0 — No recycling - 1 — Standard recycling (uses vicidial_closer_log status settings) - 2 — Advanced recycling (requires custom scripting) - Understand Status Flow: Different statuses recycle at different intervals; configure per vicidial_closer_log. - Database-Driven Configuration: Recycling behavior lives in vicidial_list, vicidial_log, and vicidial_campaign tables—customize via SQL. - Time-Zone Awareness: Use gmt_offset_now and last_local_call_time to avoid off-hours contact. - Prevent Lead Fatigue: Set attempt limits, monitor call frequency, and archive over-contacted leads. - Monitor Performance: Track recycle-to-sale conversion rates, queue depths, and recycling effectiveness. - Automate Maintenance: Use cron jobs and stored procedures to archive old leads, optimize tables, and maintain database health. - Test Before Deploying: Always back up your database and test recycling changes in a staging environment first. - Document Custom Logic: If implementing AGI scripts or complex procedures, maintain clear documentation for future maintenance.