Scheduled Apex is one of Salesforce’s most powerful features for automating business processes at specific times or intervals.
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 foundation of Scheduled Apex is the Schedulable interface. Any class that implements this interface can be scheduled to run at specified times.
public class MyScheduledClass implements Schedulable {
public void execute(SchedulableContext context) {
// Your scheduled logic goes here
}
}
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;
}
}
}
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});
}
}
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;
}
}
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';
}
}
// 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);
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:
Seconds Minutes Hours Day_of_month Month Day_of_week Optional_year
| Expression | Description | Use Case |
|---|---|---|
0 0 8 * * ? | Every day at 8:00 AM | Daily reports, morning data sync |
0 30 9 * * MON-FRI | Monday to Friday at 9:30 AM | Business day processing |
0 0 0 1 * ? | 1st and 15th of the month at 6:00 AM | Monthly billing, archiving |
0 0 12 1 1 ? | January 1st at noon | Annual processes |
0 0/15 * * * ? | Every 15 minutes | Frequent data synchronization |
0 0 22 * * SUN | Every Sunday at 10:00 PM | Weekly maintenance |
0 0 6 1,15 * ? | 1st and 15th of month at 6:00 AM | Bi-monthly processes |
0 45 23 L * ? | Last day of the month at 11:45 PM | Month-end processing |
0 0 9 ? * MON | Every Monday at 9:00 AM | Weekly team reports |
0 0 */4 * * ? | Every 4 hours | Regular data backup |
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());
}
}
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.
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.
@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();
}
}
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());
}
}
}
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');
}
}
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.