Two methods, depending on whether you want a recurring schedule or a one-shot run after a delay:
| Goal | Use |
|---|---|
| Recurring schedule (every day at 2 AM, every hour, etc.) | System.schedule + a Schedulable wrapper class |
| Run a batch once after N minutes | System.scheduleBatch |
Recurring: wrap in Schedulable and use System.schedule
// 1. The batch itself
public class NightlyCleanupBatch implements Database.Batchable<sObject> {
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator(
'SELECT Id FROM Case WHERE Status = \'New\' ' +
'AND CreatedDate < LAST_N_DAYS:30'
);
}
public void execute(Database.BatchableContext ctx, List<Case> scope) {
for (Case c : scope) c.Status = 'Auto-Closed';
update scope;
}
public void finish(Database.BatchableContext ctx) {}
}
// 2. The schedulable wrapper
public class ScheduleNightlyCleanup implements Schedulable {
public void execute(SchedulableContext ctx) {
Database.executeBatch(new NightlyCleanupBatch(), 200);
}
}
// 3. Schedule it — runs at 2 AM every day
System.schedule(
'Nightly Case Cleanup',
'0 0 2 * * ?',
new ScheduleNightlyCleanup()
);
The '0 0 2 * * ?' is a cron expression — see the related questions on CRON_EXP.
One-shot: System.scheduleBatch
If you just want a batch to run once, N minutes from now:
// Run MyBatch in 30 minutes, with chunk size 200
Id jobId = System.scheduleBatch(new MyBatch(), 'Retry MyBatch', 30, 200);
Signature: scheduleBatch(batchable, jobName, intervalInMinutes, scopeSize). Salesforce creates a Schedulable wrapper for you and schedules it to fire once.
Useful for retries — if Database.executeBatch failed because the queue was full, schedule a retry for a few minutes later.
Scheduling pattern matrix
| You want… | Use |
|---|---|
| Every day at 2 AM | System.schedule + Schedulable + '0 0 2 * * ?' |
| Every hour | System.schedule + Schedulable + '0 0 * * * ?' |
| Once, 30 min from now | System.scheduleBatch |
| Multiple recurring batches | Multiple System.schedule calls, distinct job names |
The Schedulable wrapper is mandatory for System.schedule
System.schedule requires a Schedulable, not a Database.Batchable. The wrapper is one line — but you must include it. You can also fold the wrapper into the batch class itself:
public class NightlyCleanupBatch implements
Database.Batchable<sObject>,
Schedulable
{
public void execute(SchedulableContext ctx) {
Database.executeBatch(this, 200);
}
// ... start, execute(BatchableContext, scope), finish ...
}
One class doing double duty: it’s both schedulable and batchable. Then:
System.schedule('Nightly', '0 0 2 * * ?', new NightlyCleanupBatch());
Note the two execute methods — Apex distinguishes them by parameter type.
Avoid: scheduling Apex during deploys
Salesforce blocks code changes to a class that’s actively scheduled. If NightlyCleanupBatch is scheduled, you can’t deploy a new version. Workaround: abort the scheduled job, deploy, re-schedule.
Common interview follow-ups
- Max scheduled jobs in an org? — 100 active Schedulable jobs.
- Can the scheduled batch’s execute time exceed the next scheduled run? — Yes. They overlap. Watch the Flex Queue.
- Does
scheduleBatchcount against the 100-active limit? — Yes, while it’s pending. Once it runs, the slot is freed.
Verified against: Apex Developer Guide — Using Batch Apex (Scheduling). Last reviewed 2026-05-17.