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