9.4 Schedulable Apex & Job Orchestration
Key Takeaways
- Implement Schedulable and use System.schedule with a cron expression to run Apex on a schedule
- Scheduled jobs commonly launch Batch Apex (or enqueue Queueable) from the execute method for heavy work
- Cron strings define second, minute, hour, day-of-month, month, day-of-week, optional year—one-time and recurring patterns both appear on exams
- Manage jobs in Setup (Scheduled Jobs / Apex Jobs); abort or reschedule carefully in production
- Test with System.schedule between Test.startTest and Test.stopTest; design orchestration as schedule → batch → queueable chains when needed
9.4 Schedulable Apex & Job Orchestration
Quick Answer: Schedulable Apex implements Schedulable and is registered with System.schedule(jobName, cronExp, schedulableInstance). On fire, execute(SchedulableContext) runs—typically to Database.executeBatch or System.enqueueJob. Use cron for recurring or one-time runs, manage jobs in Setup, and test with startTest/stopTest. Orchestration chains schedule → batch → queueable for reliable multi-step automation.
Not all async work is user-triggered. Nightly cleanups, hourly syncs, and monthly rollups need a clock. Schedulable Apex is the platform timer; combined with Batch and Queueable it becomes a full job orchestration toolkit tested on Platform Developer I.
The Schedulable Interface
public class NightlyAccountReviewSched implements Schedulable {
public void execute(SchedulableContext sc) {
// Keep this method thin: start the real work asynchronously
Database.executeBatch(new AccountIndustryBatch(), 200);
}
}
Best practice: Do not put massive SOQL/DML directly in Schedulable.execute if a Batch or Queueable is more appropriate. The scheduled execute is a kickoff—especially when data volume is large.
You can still run modest logic inline for tiny maintenance tasks, but exam and production guidance favor launching Batch/Queueable for non-trivial work so governors and monitoring stay clear.
System.schedule and Cron Expressions
String cron = '0 0 2 * * ?'; // 02:00:00 every day
String jobName = 'Nightly Account Review';
Id jobId = System.schedule(jobName, cron, new NightlyAccountReviewSched());
Cron field order (Salesforce)
| Position | Field | Common values |
|---|---|---|
| 1 | Seconds | 0–59 |
| 2 | Minutes | 0–59 |
| 3 | Hours | 0–23 |
| 4 | Day of month | 1–31 or ? |
| 5 | Month | 1–12 or JAN–DEC |
| 6 | Day of week | 1–7 or SUN–SAT or ? |
| 7 | Year (optional) | e.g. 2026 |
Special characters you should recognize:
*— every value?— no specific value (used for day-of-month or day-of-week when the other is specified),— list (1,15)-— range (MON-FRI)/— increments (0/15minutes = every 15 minutes starting at 0)
Examples:
0 0 2 * * ? → every day at 2:00:00 AM
0 30 9 ? * MON-FRI → 9:30:00 AM on weekdays
0 0 0 1 * ? → midnight on the 1st of each month
0 0/30 * * * ? → every 30 minutes
0 0 12 8 8 ? 2026 → one-shot noon on Aug 8, 2026 (with year)
Day-of-month vs day-of-week: Specify one and put ? in the other. Specifying both concretely is a common cron mistake that fails scheduling.
Job name uniqueness: System.schedule job names must be unique among scheduled jobs; reusing a name without aborting the old job causes errors.
One-Time vs Recurring Jobs
| Style | How |
|---|---|
| Recurring | Cron with wildcards/increments (0 0 2 * * ?) |
| One-time | Cron fixed to a future datetime (optionally with year), or schedule once and abort after run |
| Near-term delay | Some designs use a cron a few minutes ahead as a poor-man’s delay; Queueable/future is usually better for “run soon after this transaction” |
For “run after this save,” prefer Queueable/@future, not Schedulable. Schedulable is for calendar-driven execution.
Scheduled Jobs Launching Batch (Core Pattern)
public class ContactCleanupSched implements Schedulable {
public void execute(SchedulableContext sc) {
// Optional: use sc.getTriggerId() to correlate the scheduled job
Id batchId = Database.executeBatch(new ContactCleanupBatch(), 200);
System.debug('Started batch: ' + batchId);
}
}
Why this pattern dominates:
- Schedule provides the clock.
- Batch provides volume-safe chunking.
- finish on the batch can enqueue Queueable for notifications or callouts that do not need full batch machinery.
// Inside batch finish
public void finish(Database.BatchableContext bc) {
System.enqueueJob(new CleanupReportQueueable(bc.getJobId()));
}
Orchestration Patterns (Schedule → Batch → Queueable)
Pattern 1 — Nightly pipeline
- Schedulable fires at 2 AM.
- Batch A processes object 1.
finishstarts Batch B for object 2.- Final
finishenqueues Queueable to call out a “success” webhook or email.
Each stage is a separate async boundary with its own governors and monitoring row in Apex Jobs.
Pattern 2 — Hourly delta sync
Schedulable every hour → Batch with QueryLocator filtered by LastModifiedDate = LAST_N_HOURS:1 → Queueable callouts per failed Id list stored in Stateful or custom object.
Pattern 3 — Multi-step with Queueable only
When volume is low, Schedulable → Queueable chain (step1 → step2) without Batch. Prefer this when QueryLocator scale is unnecessary.
Pattern 4 — Guardrails
Before executeBatch, query whether a job of the same type is already Processing to avoid stacking duplicate nightly runs after long overruns. Store “last successful run” on a custom settings/metadata-backed config if business rules require exact windows.
Management in Setup
Admins and developers manage jobs in Setup:
- Scheduled Jobs — view cron-based jobs, next fire time, abort schedules
- Apex Jobs — monitor Batch, Queueable, future, and related async execution status
Programmatic management includes System.abortJob(jobId) for scheduled or async jobs when you have the appropriate Id (scheduled job Id from schedule, or AsyncApexJob Id).
Operational tips the exam may frame as best practice:
- Use clear job names including purpose and environment
- Document cron in business time zones carefully (org time zone matters)
- Abort and reschedule rather than silently stacking duplicates
- Prefer idempotent batch logic so a rerun after failure is safe
Testing Scheduled Classes
@IsTest
private class NightlyAccountReviewSchedTest {
@IsTest
static void schedulesAndRunsKickoff() {
// Arrange data the batch will see
insert new Account(Name = 'Sched Test');
String cron = '0 0 0 15 3 ? 2099'; // far-future one-time style expression
Test.startTest();
String jobId = System.schedule(
'Test Nightly Account Review',
cron,
new NightlyAccountReviewSched()
);
Test.stopTest();
// After stopTest, scheduled execute is forced in test context
// Assert batch effects or that AsyncApexJob rows exist as appropriate
System.assertNotEquals(null, jobId);
}
}
Testing facts:
System.schedulein tests still needs startTest/stopTest so the scheduledexecuteruns in the test’s async flush.- You do not wait until 2099 for the cron to fire in a unit test—the test framework executes the scheduled job at stopTest when scheduled inside the test boundary.
- Assert outcomes of the kicked-off batch/queueable, not only that schedule returned an Id.
- For callouts in downstream jobs, set mocks before stopTest.
Combining with Other Async Types (Decision Matrix)
| Triggering need | Mechanism |
|---|---|
| User save / trigger side effect | Queueable or @future |
| Calendar / recurring | Schedulable |
| Large record volume | Batch (often from Schedulable) |
| Flexible multi-step with state | Queueable chain |
| Mass volume multi-object | Batch chain via finish |
| Callout-heavy small sets | Queueable + AllowsCallouts |
| Callout-heavy large sets | Batch + AllowsCallouts + small scope |
Common Exam Traps
- Putting millions of rows of DML directly in Schedulable.execute instead of Batch.
- Wrong cron: day-of-month and day-of-week both set without
?. - Expecting Schedulable to return processed data to a Visualforce page synchronously—scheduled work is async; the page cannot wait on tonight’s job.
- Confusing System.schedule (Schedulable) with System.enqueueJob (Queueable) or Database.executeBatch (Batchable).
- Forgetting that scheduled jobs count toward org async limits and concurrent batch limits—design spacing and scope accordingly.
- Using Schedulable for work that must run immediately after DML in the same user flow—use Queueable/future instead.
End-to-End Mental Model
[Clock] System.schedule → Schedulable.execute
↓
[Volume] Database.executeBatch → start → execute* → finish
↓
[Flex] System.enqueueJob → Queueable.execute → (optional chain)
↓
[Watch] AsyncApexJob / Setup Apex Jobs / Scheduled Jobs
If you can sketch that pipeline and pick the correct interface for each box, you are ready for orchestration items on the exam.
Closing Checklist for Chapter 9
- @future: static void, primitives/Ids only, callout=true, no future-to-future, test with start/stop.
- Queueable: rich state, job Id, chaining, AllowsCallouts, post-commit workhorse.
- Batch: start/execute/finish, QueryLocator at scale, scope + per-execute governors, Stateful, finish chaining.
- Schedulable: cron clock, thin execute, launch batch/queueable, Setup management, test schedule inside start/stop.
Asynchronous Apex is how Lightning Platform apps stay responsive while still processing callouts, mixed DML, and org-scale data—choose the lightest tool that fits the volume and sequencing requirements.
How do you register a class that implements Schedulable to run every day at 2:00 AM?
A nightly process must update hundreds of thousands of Contacts, then send one summary callout. Which orchestration is most appropriate?
In a unit test, what ensures a scheduled job’s execute method runs so you can assert its effects?