How to use Scheduled Apex in Salesforce?

Scheduled Apex is one of Salesforce’s most powerful features for automating business processes at specific times or intervals.

What is Scheduled Apex?

Scheduled Apex allows you to run Apex code at specific times using a time-based schedule. It’s built on the concept of CRON expressions, giving you precise control over when your automated processes execute. Unlike workflow rules or Process Builder, Scheduled Apex can handle complex business logic and bulk operations efficiently.

The Schedulable Interface

The foundation of Scheduled Apex is the Schedulable interface. Any class that implements this interface can be scheduled to run at specified times.

Basic Interface Structure

public class MyScheduledClass implements Schedulable {
    public void execute(SchedulableContext context) {
        // Your scheduled logic goes here
    }
}

Understanding SchedulableContext

The SchedulableContext parameter provides metadata about the scheduled job:

public class DetailedScheduledExample implements Schedulable {
    public void execute(SchedulableContext context) {
        // Get job information
        String jobId = context.getTriggerID();
        
        // Log job execution details
        System.debug('Scheduled job executed with ID: ' + jobId);
        System.debug('Execution time: ' + System.now());
        
        // Your business logic here
        performScheduledTask();
    }
    
    private void performScheduledTask() {
        // Example: Update records, send emails, etc.
        List<Account> accountsToUpdate = [SELECT Id, LastModifiedDate 
                                         FROM Account 
                                         WHERE LastModifiedDate < LAST_N_DAYS:30];
        
        for (Account acc : accountsToUpdate) {
            acc.Description = 'Updated by scheduled job on ' + System.now();
        }
        
        if (!accountsToUpdate.isEmpty()) {
            update accountsToUpdate;
        }
    }
}

Real-World Implementation Examples

Example 1: Daily Data Cleanup Job

public class DailyDataCleanupJob implements Schedulable {
    public void execute(SchedulableContext context) {
        // Clean up old log records
        cleanupOldLogs();
        
        // Archive completed cases
        archiveCompletedCases();
        
        // Send summary email
        sendCleanupSummary();
    }
    
    private void cleanupOldLogs() {
        List<Log__c> oldLogs = [SELECT Id FROM Log__c 
                               WHERE CreatedDate < LAST_N_DAYS:30];
        
        if (!oldLogs.isEmpty()) {
            Database.DeleteResult[] results = Database.delete(oldLogs, false);
            System.debug('Deleted ' + oldLogs.size() + ' old log records');
        }
    }
    
    private void archiveCompletedCases() {
        List<Case> completedCases = [SELECT Id, Status, ClosedDate 
                                    FROM Case 
                                    WHERE Status = 'Closed' 
                                    AND ClosedDate < LAST_N_DAYS:90];
        
        for (Case c : completedCases) {
            c.IsArchived__c = true;
        }
        
        if (!completedCases.isEmpty()) {
            update completedCases;
        }
    }
    
    private void sendCleanupSummary() {
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        email.setToAddresses(new String[]{'[email protected]'});
        email.setSubject('Daily Cleanup Job Summary');
        email.setPlainTextBody('Daily cleanup job completed successfully at ' + System.now());
        
        Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
    }
}

Example 2: Weekly Report Generation

public class WeeklyReportGenerator implements Schedulable {
    public void execute(SchedulableContext context) {
        generateSalesReport();
        generatePerformanceMetrics();
    }
    
    private void generateSalesReport() {
        // Get sales data for the past week
        Date startDate = Date.today().addDays(-7);
        Date endDate = Date.today();
        List<Opportunity> weeklyOpportunities = [
            SELECT Id, Name, Amount, StageName, CloseDate, Owner.Name
            FROM Opportunity 
            WHERE CloseDate >= :startDate 
            AND CloseDate <= :endDate
        ];
        // Create report content
        String reportContent = buildReportContent(weeklyOpportunities);
        // Save as document or send email
        saveReportAsDocument(reportContent);
    }
    
    private String buildReportContent(List<Opportunity> opportunities) {
        List<String> reportLines = new List<String>();
        reportLines.add('Weekly Sales Report');
        reportLines.add('Report Generated: ' + System.now());
        reportLines.add(''); // Empty line
   
        Decimal totalAmount = 0;
       for (Opportunity opp : opportunities) {
          String oppLine = opp.Name + ' - $' + (opp.Amount != null ? opp.Amount : 0) + ' - ' + opp.StageName;
          reportLines.add(oppLine);
          totalAmount += opp.Amount != null ? opp.Amount : 0;
       }
      reportLines.add(''); // Empty line
      reportLines.add('Total Sales: $' + totalAmount);
      return String.join(reportLines, '\n');
    }
    
    private void saveReportAsDocument(String content) {
        ContentVersion cv = new ContentVersion();
        cv.Title = 'Weekly_Sales_Report_' + Date.today().format();
        cv.PathOnClient = cv.Title + '.txt';
        cv.VersionData = Blob.valueOf(content);
        cv.ContentLocation = 'S';
        cv.IsMajorVersion = true;
        insert cv;
    }
}

Example 3: Batch Processing with Schedulable

public class ScheduledBatchProcessor implements Schedulable {
    public void execute(SchedulableContext context) {
        // Launch a batch job for heavy processing
        LeadScoringBatch batchJob = new LeadScoringBatch();
        Database.executeBatch(batchJob, 200);
    }
}

// Supporting Batch Class
public class LeadScoringBatch implements Database.Batchable<SObject> {
    public Database.QueryLocator start(Database.BatchableContext context) {
        return Database.getQueryLocator([
            SELECT Id, Email, Phone, Company, Industry, Rating
            FROM Lead 
            WHERE Rating = null
        ]);
    }
    
    public void execute(Database.BatchableContext context, List<Lead> leads) {
        for (Lead lead : leads) {
            lead.Rating = calculateLeadScore(lead);
        }
        update leads;
    }
    
    public void finish(Database.BatchableContext context) {
        System.debug('Lead scoring batch completed');
    }
    
    private String calculateLeadScore(Lead lead) {
        Integer score = 0;
        
        if (lead.Email != null) score += 20;
        if (lead.Phone != null) score += 15;
        if (lead.Company != null) score += 10;
        
        if (score >= 40) return 'Hot';
        if (score >= 25) return 'Warm';
        return 'Cold';
    }
}

Scheduling Methods

Method 1: Using System.schedule()

// Schedule the job programmatically
MyScheduledClass scheduledJob = new MyScheduledClass();
String cronExpression = '0 0 8 * * ?'; // Every day at 8 AM
String jobName = 'Daily Data Processing Job';

String jobId = System.schedule(jobName, cronExpression, scheduledJob);
System.debug('Scheduled job with ID: ' + jobId);

Method 2: Advanced Scheduling with Error Handling

public class ScheduleManager {
    public static String scheduleJob(String jobName, String cronExpression, Schedulable job) {
        try {
            // Check if job already exists
            List<CronTrigger> existingJobs = [SELECT Id, CronJobDetail.Name 
                                             FROM CronTrigger 
                                             WHERE CronJobDetail.Name = :jobName];
            
            if (!existingJobs.isEmpty()) {
                System.abortJob(existingJobs[0].Id);
                System.debug('Aborted existing job: ' + jobName);
            }
            
            // Schedule the new job
            String jobId = System.schedule(jobName, cronExpression, job);
            System.debug('Successfully scheduled job: ' + jobName + ' with ID: ' + jobId);
            
            return jobId;
        } catch (Exception e) {
            System.debug('Error scheduling job: ' + e.getMessage());
            return null;
        }
    }
    
    public static void abortScheduledJob(String jobName) {
        List<CronTrigger> jobs = [SELECT Id FROM CronTrigger 
                                 WHERE CronJobDetail.Name = :jobName];
        
        for (CronTrigger job : jobs) {
            System.abortJob(job.Id);
        }
    }
}

Understanding cron expressions is crucial for effective scheduling. Here are the most commonly used patterns:

Basic Cron Expression Format

Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year

Common Scheduling Patterns

ExpressionDescriptionUse Case
0 0 8 * * ?Every day at 8:00 AMDaily reports, morning data sync
0 30 9 * * MON-FRIMonday to Friday at 9:30 AMBusiness day processing
0 0 0 1 * ?1st and 15th of the month at 6:00 AMMonthly billing, archiving
0 0 12 1 1 ?January 1st at noonAnnual processes
0 0/15 * * * ?Every 15 minutesFrequent data synchronization
0 0 22 * * SUNEvery Sunday at 10:00 PMWeekly maintenance
0 0 6 1,15 * ?1st and 15th of month at 6:00 AMBi-monthly processes
0 45 23 L * ?Last day of the month at 11:45 PMMonth-end processing
0 0 9 ? * MONEvery Monday at 9:00 AMWeekly team reports
0 0 */4 * * ?Every 4 hoursRegular data backup

Complex Cron Examples

public class CronExamples {
    public static void scheduleVariousJobs() {
        // Every weekday at 8:30 AM
        System.schedule('Weekday Morning Job', '0 30 8 ? * MON-FRI', new DailyDataCleanupJob());
        
        // Every quarter hour during business hours
        System.schedule('Quarter Hour Sync', '0 0,15,30,45 9-17 ? * MON-FRI', new DataSyncJob());
        
        // Last Friday of every month at 5 PM
        System.schedule('Month End Report', '0 0 17 ? * FRIL', new MonthlyReportJob());
        
        // Every 6 hours
        System.schedule('System Health Check', '0 0 0/6 * * ?', new HealthCheckJob());
    }
}

Pros and Cons of Scheduled Apex

Advantages

Flexibility and Power Scheduled Apex provides complete programmatic control over your automation logic. Unlike declarative tools, you can implement complex business rules, handle exceptions gracefully, and integrate with external systems.

Precise Timing Control Cron expressions offer granular control over execution timing, supporting everything from minute-level scheduling to complex monthly patterns.

Bulk Processing Capabilities Perfect for handling large data volumes through batch processing, data cleanup operations, and bulk record updates without hitting governor limits.

Integration Friendly Easily integrate with external systems through callouts, send emails, generate reports, and perform complex data transformations.

Error Handling Implement sophisticated error handling, logging, and recovery mechanisms that aren’t possible with declarative automation.

Disadvantages

Governor Limits Still subject to Apex governor limits including DML statements (150), SOQL queries (100), and CPU time (10 seconds synchronous, 60 seconds asynchronous).

Complexity Requires Apex development skills and thorough testing. More complex to maintain than declarative alternatives.

Limited Concurrency Maximum of 100 scheduled Apex jobs per organization, and only 5 jobs can run simultaneously.

No Real-Time Processing Minimum scheduling interval is one hour. Not suitable for immediate or sub-hour automation needs.

Testing Challenges Requires careful test class design and cannot easily test actual time-based execution in unit tests.

Best Practices and Testing

Comprehensive Test Class

@isTest
public class ScheduledApexTest {
    
    @testSetup
    static void setupTestData() {
        // Create test accounts
        List<Account> testAccounts = new List<Account>();
        for (Integer i = 0; i < 10; i++) {
            testAccounts.add(new Account(Name = 'Test Account ' + i));
        }
        insert testAccounts;
        
        // Create old log records for cleanup testing
        List<Log__c> oldLogs = new List<Log__c>();
        for (Integer i = 0; i < 5; i++) {
            Log__c log = new Log__c(
                Name = 'Old Log ' + i,
                CreatedDate = System.now().addDays(-35)
            );
            oldLogs.add(log);
        }
        insert oldLogs;
    }
    
    @isTest
    static void testDailyCleanupJob() {
        Test.startTest();
        
        DailyDataCleanupJob job = new DailyDataCleanupJob();
        String cronExpression = '0 0 8 * * ?';
        String jobId = System.schedule('Test Daily Cleanup', cronExpression, job);
        
        // Verify job was scheduled
        CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime
                         FROM CronTrigger WHERE Id = :jobId];
        
        System.assertEquals(cronExpression, ct.CronExpression);
        System.assertEquals(0, ct.TimesTriggered);
        
        Test.stopTest();
        
        // Verify cleanup occurred (logs should be deleted)
        List<Log__c> remainingLogs = [SELECT Id FROM Log__c];
        System.assertEquals(0, remainingLogs.size(), 'Old logs should have been deleted');
    }
    
    @isTest
    static void testWeeklyReportGenerator() {
        // Create test opportunities
        List<Opportunity> testOpps = new List<Opportunity>();
        for (Integer i = 0; i < 3; i++) {
            testOpps.add(new Opportunity(
                Name = 'Test Opp ' + i,
                StageName = 'Prospecting',
                CloseDate = Date.today().addDays(-3),
                Amount = 1000 * (i + 1)
            ));
        }
        insert testOpps;
        
        Test.startTest();
        
        WeeklyReportGenerator reportJob = new WeeklyReportGenerator();
        String jobId = System.schedule('Test Weekly Report', '0 0 9 ? * MON', reportJob);
        
        Test.stopTest();
        
        // Verify report document was created
        List<Document> reports = [SELECT Id, Name FROM Document 
                                 WHERE Name LIKE 'Weekly_Sales_Report_%'];
        System.assertEquals(1, reports.size(), 'Report document should be created');
    }
    
    @isTest
    static void testScheduleManager() {
        Test.startTest();
        
        DailyDataCleanupJob job = new DailyDataCleanupJob();
        String jobId = ScheduleManager.scheduleJob('Test Manager Job', '0 0 8 * * ?', job);
        
        System.assertNotEquals(null, jobId, 'Job should be scheduled successfully');
        
        // Test aborting the job
        ScheduleManager.abortScheduledJob('Test Manager Job');
        
        Test.stopTest();
        
        // Verify job was aborted
        List<CronTrigger> remainingJobs = [SELECT Id FROM CronTrigger 
                                          WHERE CronJobDetail.Name = 'Test Manager Job'];
        System.assertEquals(0, remainingJobs.size(), 'Job should be aborted');
    }
    
    @isTest
    static void testErrorHandling() {
        Test.startTest();
        
        // Test with invalid cron expression
        try {
            DailyDataCleanupJob job = new DailyDataCleanupJob();
            String jobId = System.schedule('Invalid Job', 'invalid cron', job);
            System.assert(false, 'Should have thrown exception');
        } catch (Exception e) {
            System.assert(true, 'Exception expected for invalid cron expression');
        }
        
        Test.stopTest();
    }
}

Best Practices for Production

public class ProductionScheduledJob implements Schedulable {
    private static final String ERROR_EMAIL = '[email protected]';
    
    public void execute(SchedulableContext context) {
        try {
            System.debug('Starting scheduled job execution at: ' + System.now());
            
            // Implement your business logic here
            processData();
            
            System.debug('Scheduled job completed successfully at: ' + System.now());
            
        } catch (Exception e) {
            handleError(context, e);
        }
    }
    
    private void processData() {
        // Use database methods with partial success handling
        List<Account> accountsToUpdate = getAccountsForUpdate();
        
        if (!accountsToUpdate.isEmpty()) {
            Database.SaveResult[] results = Database.update(accountsToUpdate, false);
            
            // Log any failures
            for (Integer i = 0; i < results.size(); i++) {
                if (!results[i].isSuccess()) {
                    System.debug('Failed to update account: ' + accountsToUpdate[i].Id + 
                               ' Error: ' + results[i].getErrors()[0].getMessage());
                }
            }
        }
    }
    
    private List<Account> getAccountsForUpdate() {
        // Implement efficient SOQL with proper filtering
        return [SELECT Id, Name, LastModifiedDate 
                FROM Account 
                WHERE LastModifiedDate < LAST_N_DAYS:30 
                LIMIT 1000];
    }
    
    private void handleError(SchedulableContext context, Exception e) {
        System.debug('Error in scheduled job: ' + e.getMessage());
        System.debug('Stack trace: ' + e.getStackTraceString());
        
        // Send error notification
        sendErrorNotification(context, e);
        
        // Log error for monitoring
        logError(context, e);
    }
    
    private void sendErrorNotification(SchedulableContext context, Exception e) {
        try {
            Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
            email.setToAddresses(new String[]{ERROR_EMAIL});
            email.setSubject('Scheduled Job Error - ' + context.getTriggerID());
            email.setPlainTextBody('Error occurred in scheduled job:\n\n' + 
                                  'Job ID: ' + context.getTriggerID() + '\n' +
                                  'Error: ' + e.getMessage() + '\n' +
                                  'Stack Trace: ' + e.getStackTraceString());
            
            Messaging.sendEmail(new Messaging.SingleEmailMessage[]{email});
        } catch (Exception emailError) {
            System.debug('Failed to send error email: ' + emailError.getMessage());
        }
    }
    
    private void logError(SchedulableContext context, Exception e) {
        // Create custom error log record
        Error_Log__c errorLog = new Error_Log__c(
            Job_ID__c = context.getTriggerID(),
            Error_Message__c = e.getMessage(),
            Stack_Trace__c = e.getStackTraceString(),
            Timestamp__c = System.now()
        );
        
        try {
            insert errorLog;
        } catch (Exception logError) {
            System.debug('Failed to create error log: ' + logError.getMessage());
        }
    }
}

Monitoring and Management

Monitoring Scheduled Jobs

public class ScheduledJobMonitor {
    
    public static List<CronTrigger> getAllScheduledJobs() {
        return [SELECT Id, CronJobDetail.Name, CronJobDetail.JobType, 
                       CronExpression, TimesTriggered, NextFireTime, 
                       PreviousFireTime, State, StartTime, EndTime
                FROM CronTrigger
                ORDER BY NextFireTime ASC];
    }
    
    public static void displayJobStatus() {
        List<CronTrigger> jobs = getAllScheduledJobs();
        
        System.debug('=== Scheduled Jobs Status ===');
        for (CronTrigger job : jobs) {
            System.debug('Job: ' + job.CronJobDetail.Name);
            System.debug('Next Run: ' + job.NextFireTime);
            System.debug('Times Triggered: ' + job.TimesTriggered);
            System.debug('State: ' + job.State);
            System.debug('---');
        }
    }
    
    public static void abortAllScheduledJobs() {
        List<CronTrigger> jobs = [SELECT Id FROM CronTrigger];
        
        for (CronTrigger job : jobs) {
            System.abortJob(job.Id);
        }
        
        System.debug('Aborted ' + jobs.size() + ' scheduled jobs');
    }
}

Conclusion

Scheduled Apex is a powerful tool for automating complex business processes in Salesforce. While it requires development expertise and careful consideration of governor limits, it provides unmatched flexibility for handling sophisticated automation scenarios.

Must Read

Uday Bheemarpu
Uday Bheemarpu
Articles: 9

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading