Batch & Scheduled Apex
Batch Apex processes very large data sets (up to 50 million records via Database.QueryLocator) in chunks (scope, default 200, max 2,000).
24.1 Batch Apex
Batch Apex processes very large data sets (up to 50 million records via Database.QueryLocator) in chunks (scope, default 200, max 2,000). Each chunk is a separate transaction with fresh limits.
public class InactiveContactCleanupBatch implements Database.Batchable<SObject>, Database.Stateful {
private Integer processed = 0; // preserved across chunks (Stateful)
private List<String> errors = new List<String>();
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([
SELECT Id, Email, Status__c FROM Contact
WHERE LastActivityDate < LAST_N_DAYS:730 AND Status__c != 'Archived'
]);
}
public void execute(Database.BatchableContext bc, List<Contact> scope) {
for (Contact c : scope) c.Status__c = 'Archived';
Database.SaveResult[] results = Database.update(scope, false);
for (Integer i = 0; i < results.size(); i++) {
if (results[i].isSuccess()) processed++;
else errors.add(scope[i].Id + ': ' + results[i].getErrors()[0].getMessage());
}
}
public void finish(Database.BatchableContext bc) {
AsyncApexJob job = [SELECT Status, NumberOfErrors, JobItemsProcessed, TotalJobItems
FROM AsyncApexJob WHERE Id = :bc.getJobId()];
System.debug('Archived ' + processe
…24.2 Batch variations
public Iterable<SObject> start(...) (or a custom Iterable<T>) — subject to the 50,000 SOQL row limit
implements Database.AllowsCallouts (callout limits apply per execute)
Database.Stateful keeps instance variables between chunks (serialization cost) Publish events on failure
implements Database.RaisesPlatformEvents → BatchApexErrorEvent Schedule a batch
System.scheduleBatch(new MyBatch(), 'Nightly cleanup', 60, 200) (minutes from now)
Up to 100 jobs in *Holding*; reorder with FlexQueue.moveJobToFront(jobId)
Handling batch errors with BatchApexErrorEvent:
trigger BatchErrorTrigger on BatchApexErrorEvent (after insert) {
List<Log__c> logs = new List<Log__c>();
for (BatchApexErrorEvent evt : Trigger.new) {
logs.add(new Log__c(Message__c = evt.ExceptionType + ': ' + evt.Message,
Details__c = evt.JobScope)); // IDs of records in the failed chunk
}
insert logs;
}24.3 Scheduled Apex
0 30 8 ? * MON-FRI Weekdays at 8:30 AM First day of each month at midnight Every hour on the hour 0 0 12 L * ? Last day of the month at noon
Notes: schedules run in the org's time zone context of the scheduling user; you can't schedule more often than hourly via a single cron (use multiple jobs for every 15 minutes); a scheduled class with active jobs can't be edited or deployed unless *Allow deployments of components when corresponding Apex jobs are pending or in progress* is enabled in Deployment Settings. Keep the Schedulable thin — it should only start a batch or queueable.
public class NightlyCleanupScheduler implements Schedulable {
public void execute(SchedulableContext sc) {
Database.executeBatch(new InactiveContactCleanupBatch(), 500);
}
}
// Cron: Seconds Minutes Hours Day_of_month Month Day_of_week [Year]
String cron = '0 0 2 * * ?'; // every day at 02:00
Id cronId = System.schedule('Nightly Contact Cleanup', cron, new NightlyCleanupScheduler());
// inspect / cancel
CronTrigger ct = [SELECT Id, NextFireTime, State FROM CronTrigger WHERE Id = :cronId];
System.abortJob(cronId);24.4 Choosing the right async tool
Fire-and-forget callout from a trigger, simple params Queueable (or @future)
Complex params, job ID, chaining, retries Queueable + Finalizer Millions of records Run at a time / recurring
Scheduled Apex (starting batch/queueable) Decoupled, event-driven processing, integrations
React to record changes externally or asynchronously Change Data Capture