Skip to main content

SF-0411 · Concept · Medium

Can you tell me about some best practices for scheduled apex?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

Scheduled Apex is the cron of the platform — it should orchestrate work, not do it. The mature pattern is: fire on schedule, enqueue Batch or Queueable for the heavy lifting. Below is the working checklist senior Salesforce devs apply.

1. Keep execute() thin

Your Schedulable should resolve to a few SOQL queries plus a Database.executeBatch or System.enqueueJob call. Heavy DML, callouts, and looping go in the async job that’s kicked off, not in the Schedulable itself.

public class NightlyClose implements Schedulable {
    public void execute(SchedulableContext sc) {
        Database.executeBatch(new NightlyCloseBatch(), 200);
    }
}

2. Treat callouts as forbidden in execute()

Synchronous callouts are blocked from Schedulable code. Always route to Queueable with Database.AllowsCallouts, @future(callout=true), or Batchable with Database.AllowsCallouts.

3. Watch the 100-job cap

Salesforce limits active scheduled Apex jobs to 100 per org. Before scheduling, count current jobs:

Integer waiting = [SELECT COUNT() FROM CronTrigger WHERE State = 'WAITING'];
if (waiting >= 95) {
    throw new IllegalStateException('Too many scheduled jobs, cleanup needed');
}

Reuse a single Schedulable rather than spawning a new one per record.

4. Use System.scheduleBatch when you can

For “run this batch once at time X” cases, System.scheduleBatch(new MyBatch(), 'job-name', delayMinutes) skips the Schedulable boilerplate entirely.

5. Make scheduled classes idempotent

If a job fires twice (manual run plus scheduled fire, or retry after error), the result should be the same. Stamp processed records with a “Last Run At” timestamp or external Id so the next invocation skips already-handled rows.

6. Don’t change the schedulable signature after deployment

Once a class is scheduled, its API signature is frozen in the cron job blueprint. If you change the class name or method signature, the scheduled instance fails. Pattern: abort the job, deploy, re-schedule during release windows.

7. Name jobs deterministically

When you call System.schedule('My Nightly Job', cronExpr, new MyClass()), the second argument is the job name. Use a stable, descriptive name so monitoring tools and ops can identify it.

8. Provide a manual rescheduler

Build a Setup-only Lightning page or executeAnonymous snippet to abort and reschedule all jobs. Saves admins from clicking through Setup → Scheduled Jobs after deploys.

public class JobScheduler {
    public static void schedule() {
        // Abort prior instance if present
        for (CronTrigger ct : [SELECT Id FROM CronTrigger WHERE CronJobDetail.Name = 'Nightly Close']) {
            System.abortJob(ct.Id);
        }
        System.schedule('Nightly Close', '0 0 1 * * ?', new NightlyClose());
    }
}

9. Test with Test.startTest() and System.schedule

@isTest
static void schedules() {
    Test.startTest();
    String jobId = System.schedule('Test Close', '0 0 1 * * ?', new NightlyClose());
    Test.stopTest();
    System.assert(jobId != null);
}

Test.stopTest() forces synchronous execution of the scheduled job in tests so you can assert side effects.

10. Monitor failures

Watch AsyncApexJob.Status = 'Failed' and CronTrigger.State = 'ERROR'. Surface those into a custom report or send a Slack/email alert from a daily scheduled “monitor” job. Silent failures in nightly jobs are the most common cause of “why didn’t this run last weekend?” tickets.

Common follow-ups

  • Cron format? — Seconds Minutes Hours Day-of-Month Month Day-of-Week (optional Year). Example: 0 0 2 * * ? runs every day at 2 AM.
  • Can two scheduled jobs run at the same time? — Yes, subject to async governor limits.

Verified against: Apex Developer Guide — Apex Scheduler. Last reviewed 2026-05-17 for Spring ‘26.