AI Lead Posting Scheduler
The AI Lead Posting Scheduler is a high-frequency background job that continuously monitors for leads awaiting AI auto-calling and posts them to the AI calling service. This automation enables 24/7 lead engagement without requiring human telecaller intervention.
Overview
The PostPendingLeadsToAIScheduler runs every 30 seconds to identify leads with status Pending_AI_Call and submit them to the AI auto-caller service via the AIAutoCallerFactory. The scheduler batches requests to avoid overwhelming the AI service while maintaining rapid lead processing.
Key Features
- High-Frequency Execution: Runs every 30 seconds around the clock
- Batch Processing: Configurable batch size (e.g., max 10 leads per cycle)
- Service Abstraction: Uses factory pattern for AI provider flexibility
- Graceful Degradation: Failed leads fall back to manual queue on repeated failures
- Concurrency Safety: Lock mechanism prevents overlapping executions
Job Configuration
Cron Expression & Timing
Execution: Every 30 seconds
Format: No cron needed (custom scheduler)
Frequency: 2,880 executions per day (24 × 60 × 2)
Uptime: 24/7, 365 days/year
Typical execution time: < 5 seconds
Max concurrent executions: 1 (locked)
Max leads processed per day: ~28,800 (2,880 × 10 batch)Posting Cycle Flow
Sequence Diagram: Scheduler → Factory → AI Caller
Lead Status Transitions
Leads move through distinct states during AI calling:
Batch Size & Concurrency Management
Error Handling & Retry Strategy
public async Task Execute(IJobExecutionContext context)
{
// Concurrency guard: prevent overlapping executions
lock (_executionLock)
{
if (_isExecuting)
{
context.JobDetail.JobDataMap.Put("Status", "SKIPPED_CONCURRENT");
return;
}
_isExecuting = true;
}
try
{
// Get pending leads
var leads = await _leadsService.GetPendingAILeads(_maxBatchSize);
foreach (var lead in leads)
{
try
{
// Prepare call request
var request = PrepareCallRequest(lead);
// Get appropriate AI caller from factory
var caller = _aiFactory.CreateCaller(lead.AIProviderType);
// Post to AI service
var response = await caller.CallAsync(request);
if (response.IsSuccess)
{
// Success: update lead to Processing
lead.Status = "Processing";
lead.AICallStatus = "INITIATED";
lead.AICallId = response.CallId;
lead.PostedToAIDate = DateTime.Now;
await _leadsService.UpdateLead(lead);
// Log successful post
await LogAICall(lead, response, "SUCCESS");
}
else
{
// Failed: check retry count
lead.AIPostRetryCount++;
if (lead.AIPostRetryCount >= _maxRetryAttempts)
{
// Max retries exceeded: fallback to manual
lead.Status = "AI_FAILED";
lead.AIFailureReason = response.ErrorMessage;
lead.LastAIAttempt = DateTime.Now;
// Queue for manual TC assignment
await _leadsService.QueueForManualAssignment(lead);
await LogAICall(lead, response, "FAILED_FINAL");
}
else
{
// Retry on next cycle
lead.LastAIAttempt = DateTime.Now;
await _leadsService.UpdateLead(lead);
await LogAICall(lead, response, "RETRY");
}
}
}
catch (HttpRequestException ex)
{
// Network error: don't increment retry, will retry naturally
_logger.LogWarning($"Network error posting lead {lead.LeadId}: {ex.Message}");
await LogAICall(lead, null, "NETWORK_ERROR");
}
catch (Exception ex)
{
// Unexpected error
_logger.LogError($"Unexpected error processing lead {lead.LeadId}: {ex}");
lead.AIPostRetryCount++;
if (lead.AIPostRetryCount >= _maxRetryAttempts)
{
lead.Status = "AI_FAILED";
await _leadsService.QueueForManualAssignment(lead);
}
await _leadsService.UpdateLead(lead);
}
}
context.JobDetail.JobDataMap.Put("Status", "COMPLETED");
context.JobDetail.JobDataMap.Put("LeadsProcessed", leads.Count);
}
finally
{
lock (_executionLock)
{
_isExecuting = false;
}
}
}AIAutoCallerFactory Pattern
The factory abstracts different AI providers:
Database Tables for AI Calling
Monitoring & Metrics
Track the scheduler's performance with these key metrics:
| Metric | Threshold | Alert Action |
|---|---|---|
| Execution Duration | > 20 seconds | Check database load |
| Posting Success Rate | < 95% | Investigate API errors |
| Batch Size Variation | > 2x normal | Check lead queue buildup |
| AI Service Latency | > 3 seconds | Contact AI provider |
| Failed Lead Accumulation | > 100/day | Review fallback process |
| Concurrent Executions | > 0 | Critical - scheduler locking issue |
Configuration Options
public class AIPostingSchedulerConfig
{
// Posting batch size per execution
public int MaxBatchSize { get; set; } = 10;
// Retry strategy
public int MaxRetryAttempts { get; set; } = 3;
public int RetryDelayMs { get; set; } = 0; // Retry on next cycle
// AI service provider
public string DefaultAIProvider { get; set; } = "VoiceAPI";
public string AIServiceBaseUrl { get; set; } = "https://api.voiceai.com";
public int AIServiceTimeoutMs { get; set; } = 10000;
// Execution safety
public int MaxExecutionTimeMs { get; set; } = 25000; // 30s - 5s buffer
public bool PreventConcurrentExecutions { get; set; } = true;
// Notifications
public bool NotifyOnAllFailures { get; set; } = false;
public bool NotifyOnMaxRetriesExceeded { get; set; } = true;
}Fallback to Manual Queue
When AI posting fails after max retries, leads are queued for manual assignment:
private async Task QueueForManualAssignment(ExternalLead lead)
{
// Find available telecaller to assign
var availableTC = await _staffService
.GetAvailableTelecallers(lead.CallSourceId)
.FirstOrDefaultAsync();
if (availableTC != null)
{
lead.Status = "ASSIGNED_MANUAL";
lead.StaffId = availableTC.StaffId;
lead.AssignedDate = DateTime.Now;
// Create follow-up for same day if morning, next day if afternoon
var reviewTime = DateTime.Now.Hour < 14
? DateTime.Today.AddDays(1).AddHours(9)
: DateTime.Today.AddHours(17);
await _followupService.CreateFollowup(new FollowupCall
{
LeadId = lead.LeadId,
StaffId = availableTC.StaffId,
ReviewDateTime = reviewTime,
Notes = "Escalated from AI auto-call: manual outreach required"
});
}
else
{
// No available TCs: hold in queue
lead.Status = "MANUAL_QUEUE_PENDING";
}
await _leadsService.UpdateLead(lead);
}Integration with Telecaller Workflow
When AI calls complete, results feed back into the telecaller system:
- AI_COMPLETED + Success: Lead marked SV Fixed or Call Progress
- AI_COMPLETED + No Interest: Sent to manual TCs for follow-up
- AI_FAILED: Queued for manual assignment with priority flag
- Invalid Lead: Moved to Trashed status (bad phone/data)
This integration ensures seamless handoff between automated and manual channels.