Telecaller & Leads
Schedulers & Crons
AI Lead Posting

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:

MetricThresholdAlert Action
Execution Duration> 20 secondsCheck database load
Posting Success Rate< 95%Investigate API errors
Batch Size Variation> 2x normalCheck lead queue buildup
AI Service Latency> 3 secondsContact AI provider
Failed Lead Accumulation> 100/dayReview fallback process
Concurrent Executions> 0Critical - 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.