Telecaller & Leads
Schedulers & Crons
Auto-Close Scheduler

Auto-Close Scheduler

The Auto-Close Scheduler is a background job that automatically closes stale leads that have had no activity for a configurable period. This prevents the queue from becoming cluttered with abandoned leads and helps maintain accurate metrics.

Overview

The ExternalLeadsAutoCloseScheduler is a Quartz.NET scheduled job that runs at regular intervals to identify and close leads meeting inactivity criteria. The system is designed to balance automation with preventing premature closure of valid leads.

Key Features

  • Regular Execution: Runs every 5 minutes during business hours (9 AM - 8 PM)
  • Smart Filtering: Excludes leads with active statuses (SV Fixed, Call Progress)
  • Audited Closure: All auto-close actions are logged with timestamps and reasons
  • Configurable Threshold: Age threshold can be adjusted (typically 30 days)
  • Business Hours Only: Respects business operating hours to align with team schedules

Job Configuration

Cron Expression Details

Cron: 0 0/5 9-20 * * ?

Field Breakdown:
┌─────────────┬─────────────────────────────────────┐
│ Field       │ Value & Meaning                      │
├─────────────┼─────────────────────────────────────┤
│ Seconds     │ 0 (at 00 seconds)                   │
│ Minutes     │ 0/5 (every 5 minutes)               │
│ Hours       │ 9-20 (9 AM through 8 PM)            │
│ Day of Month│ * (every day)                       │
│ Month       │ * (every month)                     │
│ Day of Week │ ? (no specific day)                 │
└─────────────┴─────────────────────────────────────┘

Executions per day: 144 times (12 hours × 12 executions/hour)
First run: 9:00 AM, Last run: 8:00 PM

Auto-Close Decision Logic

Lead Age Calculation

The system calculates lead age using the most recent activity date:

-- Determine age for auto-close consideration
DECLARE @LeadAgeThresholdDays INT = 30;
DECLARE @CutoffDate DATETIME = DATEADD(DAY, -@LeadAgeThresholdDays, GETDATE());
 
SELECT
    LeadId,
    Mobile,
    Name,
    Status,
    CreatedDate,
    ModifiedDate,
    -- Use the more recent of created or modified date
    CASE
        WHEN ModifiedDate > CreatedDate THEN ModifiedDate
        ELSE CreatedDate
    END as LastActivityDate,
    DATEDIFF(DAY,
        CASE
            WHEN ModifiedDate > CreatedDate THEN ModifiedDate
            ELSE CreatedDate
        END,
        GETDATE()) as DaysOld
FROM ExternalLeads
WHERE IsActive = 1
    AND CASE
        WHEN ModifiedDate > CreatedDate THEN ModifiedDate
        ELSE CreatedDate
    END < @CutoffDate
    AND Status NOT IN ('SV_FIXED', 'CP')

Protected Lead Statuses

Certain lead statuses are NEVER auto-closed, regardless of age:

StatusReasonMax AgeAction
SV FixedSuccessful conversion - permanentN/ANever auto-close
Call ProgressActive engagement ongoingN/ANever auto-close
Pending AI CallQueued for automated calling7 daysAuto-close after 7 days + failed attempts
On HoldManager-requested holdVariableCheck hold expiry date
Pending Follow-upFollow-up scheduledUntil after review dateAuto-close only if follow-up also overdue

Scheduler Timeline & Execution Flow

Execution Process

Auto-Close Audit Record

Every auto-close action is logged for compliance and history:

Audit Record Example

AuditId: 12547
LeadId: 98765
LeadName: Rajesh Kumar
LeadMobile: 98765-43210
PreviousStatus: NO_RESPONSE
NewStatus: AUTO_CLOSED
CloseReason: No activity for 30+ days. Status: NO_RESPONSE. Last contact: 2026-02-07
DaysActive: 42
LastActivityDate: 2026-02-07 14:30:00
AutoClosedDate: 2026-03-21 09:05:00
SchedulerVersion: v2.1.0
LeadsProcessedBatch: 23

Configuration & Thresholds

The scheduler uses configurable parameters that can be adjusted without code changes:

public class AutoCloseSchedulerConfig
{
    // Days of inactivity before auto-close
    public int LeadAgeThresholdDays { get; set; } = 30;
 
    // Minimum age for Pending_AI_Call (failed auto-calls)
    public int AICallFailureThresholdDays { get; set; } = 7;
 
    // Business hours (24-hour format)
    public TimeOnly JobStartHour { get; set; } = new TimeOnly(9, 0);   // 9 AM
    public TimeOnly JobEndHour { get; set; } = new TimeOnly(20, 0);    // 8 PM
 
    // Batch processing to avoid overwhelming database
    public int MaxLeadsPerExecution { get; set; } = 100;
 
    // Prevent concurrent executions
    public bool PreventConcurrentExecutions { get; set; } = true;
 
    // Notification settings
    public bool NotifyTeleCallers { get; set; } = true;
    public bool NotifyManagers { get; set; } = true;
}

Error Handling & Resilience

public async Task Execute(IJobExecutionContext context)
{
    try
    {
        // Check if already running (concurrency guard)
        if (_isExecuting)
        {
            context.JobDetail.JobDataMap.Put("Status", "SKIPPED_CONCURRENT");
            return;
        }
 
        _isExecuting = true;
 
        var staleLeads = FindStaleLeads();
 
        if (staleLeads.Count == 0)
        {
            context.JobDetail.JobDataMap.Put("Status", "NO_STALE_LEADS");
            return;
        }
 
        using (var transaction = _db.BeginTransaction())
        {
            try
            {
                foreach (var lead in staleLeads)
                {
                    lead.Status = "AUTO_CLOSED";
                    lead.IsActive = false;
                    lead.ModifiedDate = DateTime.Now;
 
                    _db.ExternalLeads.Update(lead);
                    LogAutoClose(lead, "Age threshold exceeded");
                }
 
                await _db.SaveChangesAsync();
                transaction.Commit();
 
                context.JobDetail.JobDataMap.Put("Status", "COMPLETED");
                context.JobDetail.JobDataMap.Put("LeadsProcessed", staleLeads.Count);
            }
            catch (Exception ex)
            {
                transaction.Rollback();
                _logger.LogError($"Auto-close transaction failed: {ex.Message}");
                context.JobDetail.JobDataMap.Put("Status", "ERROR");
                throw;
            }
        }
    }
    finally
    {
        _isExecuting = false;
    }
}

Monitoring & Metrics

Track the scheduler's performance via these metrics:

MetricAlert ThresholdAction
Execution Duration> 2 minutesInvestigate query performance
Leads Processed Per Run< 5 (unusual)Check filtering logic
Failed Transactions> 0Immediate escalation
Audit Log Size> 10K/weekArchive old logs
Unintended Closures> 0.5%Review filtering rules

Related Documentation

  • Follow-up Workflow: See how open follow-ups prevent auto-close
  • AI Lead Posting: Understand Pending_AI_Call status and auto-close thresholds
  • Manager Review: View auto-closed metrics in performance dashboards
  • Database Tables: Complete schema for ExternalLeads and AutoCloseAuditLog