Schedulable Apex

Schedulable Apex Practice Problems

15 free Salesforce Apex challenges · Full problem statements, best-practice notes, and step-by-step hints · Solve live on ApexArena

This guide walks through 15 Schedulable Apex jobs, cron expressions, and recurring automation patterns. Each entry below includes the full problem statement, the governor-limit and best-practice constraints it's testing, and a numbered approach to solving it — then you can open the same problem in ApexArena's browser-based Apex editor and get instant pass/fail feedback against real test cases.

On this page
  1. 1. Schedulable: Daily Lead Cleanup Job
  2. 2. Schedulable: Monthly Batch Job Launcher
  3. 3. Schedulable: Quarterly Pipeline Report Email
  4. 4. Schedulable: Self-Rescheduling Hourly Monitoring Job
  5. 5. Schedulable Apex (Easy): Daily Batch Launcher
  6. 6. Schedulable Apex (Easy-Medium): Monthly Contract Expiry Summary Email
  7. 7. Schedulable Apex (Medium): Weekly Won Opportunity Report via AggregateResult
  8. 8. Schedulable Apex (Medium-Hard): Conditional Batch Size Based on Lead Volume
  9. 9. Schedulable Apex (Hard): Adaptive Self-Rescheduling Based on Case Load
  10. 10. Schedulable Apex: Nightly Stale Waiting-Case Closer
  11. 11. Schedulable Apex: Weekly Account Audit Batch Launcher
  12. 12. Schedulable Apex: Daily Overdue Opportunity Follow-Up Batch
  13. 13. Schedulable Apex: Monthly New Lead Count Summary Email to Admin
  14. 14. Schedulable Apex: Self-Rescheduling Uncontacted Lead Reassigner
  15. 15. Schedulable: Weekly Inactive Lead Email Report
Easy SchedulableAsync Apex

1. Schedulable: Daily Lead Cleanup Job

Problem #206 · Salesforce Apex Coding Challenge

Problem Statement

Write a Schedulable Apex class DailyLeadCleanupScheduler that deletes Leads which were converted more than 90 days ago.

Requirements

  • Implement Schedulable interface.
  • execute(SchedulableContext sc): calculate a cutoff date 90 days in the past.
  • Query Leads where IsConverted = true AND ConvertedDate < cutoff.
  • Delete only when the list is non-empty.

Best Practices

  • Guard DML with if (!staleLeads.isEmpty()).
  • Use a bind variable (:cutoff) — never concatenate dates into SOQL strings.
  • SOQL outside any loop.

How to Schedule

String cron = '0 0 1 * * ?'; // 1 AM daily
System.schedule('Daily Lead Cleanup', cron, new DailyLeadCleanupScheduler());
Approach
  • 1Cutoff: Date cutoff = Date.today().addDays(-90);
  • 2SOQL: [SELECT Id FROM Lead WHERE IsConverted = true AND ConvertedDate < :cutoff]
  • 3Guard: if (!staleLeads.isEmpty()) { delete staleLeads; }
  • 4Schedule from Anonymous Apex: System.schedule('Daily Lead Cleanup', '0 0 1 * * ?', new DailyLeadCleanupScheduler());
Medium SchedulableBatch ApexAsync Apex

2. Schedulable: Monthly Batch Job Launcher

Problem #207 · Salesforce Apex Coding Challenge

Problem Statement

Write MonthlyAccountBatchScheduler that implements Schedulable and launches AccountRevenueTierBatch with a batch size of 200 when fired.

Requirements

  • Implement Schedulable.
  • Declare a @TestVisible private Integer batchSize = 200 instance variable.
  • execute(): instantiate AccountRevenueTierBatch and call Database.executeBatch(batch, batchSize).

Best Practices

  • Use @TestVisible so tests can override the batch size without exposing it publicly.
  • Separate scheduler and batch classes — never mix Schedulable and Batchable in one class.
  • Always specify batch size in executeBatch to avoid the default 200 silently changing behaviour.

How to Schedule

// Run at midnight on the 1st of every month
System.schedule('Monthly Account Batch', '0 0 0 1 * ?', new MonthlyAccountBatchScheduler());
Approach
  • 1Declare: @TestVisible private Integer batchSize = 200;
  • 2In execute(): AccountRevenueTierBatch batch = new AccountRevenueTierBatch();
  • 3Then: Database.executeBatch(batch, batchSize);
  • 4@TestVisible lets test classes set batchSize = 1 without exposing the field as public.
Medium SchedulableAsync Apex

3. Schedulable: Quarterly Pipeline Report Email

Problem #221 · Salesforce Apex Coding Challenge

Problem Statement

Write QuarterlyPipelineScheduler that implements Schedulable and sends an email summarising open Opportunities closing this quarter, grouped by stage.

Requirements

  • Implement Schedulable.
  • Declare @TestVisible private String recipientEmail.
  • execute(): run an aggregate SOQL with StageName, COUNT(Id), SUM(Amount) filtered by IsClosed = false AND CloseDate = THIS_QUARTER.
  • Build a plain-text body by looping the AggregateResult list.
  • Send via Messaging.SingleEmailMessage.
  • Wrap everything in try-catch(Exception e) with System.debug on error.

Best Practices

  • THIS_QUARTER is a SOQL date literal — no manual date arithmetic needed.
  • Use @TestVisible so tests can override the email address without making it public.
  • Always wrap Messaging.sendEmail in try-catch — email limits can throw.
Approach
  • 1SOQL date literal: CloseDate = THIS_QUARTER — no bind variable needed.
  • 2Read aggregate: ar.get('StageName'), ar.get('oppCount'), ar.get('totalAmount')
  • 3Email: Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); mail.setToAddresses(...); mail.setSubject(...); mail.setPlainTextBody(...);
  • 4Send: Messaging.sendEmail(new List{ mail });
Hard SchedulableAsync Apex

4. Schedulable: Self-Rescheduling Hourly Monitoring Job

Problem #222 · Salesforce Apex Coding Challenge

Problem Statement

Write HourlyMonitorScheduler that:

  1. Marks overdue open Opportunities (CloseDate < today) as at-risk by setting At_Risk__c = true.
  2. Self-reschedules for the next hour using a helper method buildNextHourCron().

Requirements

  • Implement Schedulable.
  • Declare @TestVisible static final String JOB_NAME.
  • Business logic in a try block; self-reschedule in finally guarded by !Test.isRunningTest().
  • buildNextHourCron(): returns a cron expression for the next hour (@TestVisible private static String).
  • SOQL and DML outside loops; isEmpty() guard before DML.

Best Practices

  • Use finally for self-reschedule — it runs even when the business logic throws.
  • !Test.isRunningTest() prevents infinite scheduling in unit tests.
  • Unique job name per run (append timestamp) prevents duplicate job name errors.
Approach
  • 1SOQL: [SELECT Id, At_Risk__c FROM Opportunity WHERE IsClosed = false AND CloseDate < :Date.today() AND At_Risk__c = false]
  • 2Finally block: try { ... } catch (Exception e) { ... } finally { if (!Test.isRunningTest()) System.schedule(...); }
  • 3Cron: '0 0 ' + Datetime.now().addHours(1).hour() + ' * * ?' — fires at the top of the next hour.
  • 4Unique job name: JOB_NAME + ' ' + Datetime.now().format('HH:mm') — prevents name collision.
Easy SchedulableAsync Apex

5. Schedulable Apex (Easy): Daily Batch Launcher

Problem #245 · Salesforce Apex Coding Challenge

Problem Statement

Write DailyLeadCleanupScheduler that implements Schedulable and launches a LeadScoreResetBatch each time it fires. The batch size must be configurable via a @TestVisible field so tests can override it.

Requirements

  1. Implement the Schedulable interface.
  2. Declare @TestVisible private Integer batchSize = 200.
  3. execute(SchedulableContext sc) — instantiate LeadScoreResetBatch and call Database.executeBatch(batch, batchSize).

Best Practices

  • Always use a named local variable for the batch instance — readable and debuggable.
  • @TestVisible on batchSize lets tests inject a smaller size without exposing the field publicly.
  • Schedule: System.schedule('DailyLeadCleanup', '0 0 2 * * ?', new DailyLeadCleanupScheduler());
Approach
  • 1Interface: implements Schedulable — one method: public void execute(SchedulableContext sc)
  • 2@TestVisible: @TestVisible private Integer batchSize = 200;
  • 3Launch: LeadScoreResetBatch batch = new LeadScoreResetBatch(); Database.executeBatch(batch, batchSize);
  • 4Schedule cron: '0 0 2 * * ?' fires at 2 AM every day.
Easy SchedulableAsync Apex

6. Schedulable Apex (Easy-Medium): Monthly Contract Expiry Summary Email

Problem #246 · Salesforce Apex Coding Challenge

Problem Statement

Write EndOfMonthContractScheduler that runs on a schedule, counts all Activated Contracts expiring this month, and sends a summary email to a configurable recipient.

Requirements

  1. Implement Schedulable.
  2. Declare @TestVisible private String recipientEmail = 'admin@salesforce.com'.
  3. execute():
    • COUNT query: SELECT COUNT() FROM Contract WHERE EndDate = THIS_MONTH AND Status = 'Activated'.
    • Build a Messaging.SingleEmailMessage with setToAddresses, setSubject, setPlainTextBody.
    • Guard Messaging.sendEmail() with !Test.isRunningTest().
    • Always System.debug the summary.
Approach
  • 1Date literal: EndDate = THIS_MONTH — covers the entire current calendar month.
  • 2COUNT(): Integer count = [SELECT COUNT() FROM Contract WHERE EndDate = THIS_MONTH AND Status = 'Activated'];
  • 3Email: Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
  • 4Recipients: mail.setToAddresses(new List{ recipientEmail });
  • 5Guard: if (!Test.isRunningTest()) { Messaging.sendEmail(new List{ mail }); }
Medium SchedulableAsync Apex

7. Schedulable Apex (Medium): Weekly Won Opportunity Report via AggregateResult

Problem #247 · Salesforce Apex Coding Challenge

Problem Statement

Write WeeklyAccountSummaryScheduler that queries the total number and dollar value of Closed Won Opportunities for the current week using an AggregateResult query, then sends a formatted summary email.

Requirements

  1. Implement Schedulable.
  2. Declare @TestVisible private String recipientEmail.
  3. execute():
    • Aggregate query: SELECT COUNT(Id) cnt, SUM(Amount) totalAmount FROM Opportunity WHERE IsWon = true AND CloseDate = THIS_WEEK.
    • Extract: (Integer) results[0].get('cnt') and (Decimal) results[0].get('totalAmount').
    • Null-guard totalAmount (set to 0 if null).
    • Build and send email (guard with !Test.isRunningTest()).

Key Concept: AggregateResult

Aggregate SOQL (with COUNT, SUM, AVG etc.) returns AggregateResult[]. Use result.get('alias') and cast to the expected type to read each column.

Approach
  • 1Aggregate: AggregateResult[] r = [SELECT COUNT(Id) cnt, SUM(Amount) totalAmount FROM Opportunity WHERE IsWon = true AND CloseDate = THIS_WEEK];
  • 2Extract: Integer wonCount = (Integer) r[0].get('cnt'); Decimal total = (Decimal) r[0].get('totalAmount');
  • 3Null guard: if (totalAmount == null) totalAmount = 0;
  • 4Format: totalAmount.setScale(2) — 2 decimal places for currency display.
  • 5Guard email: if (!Test.isRunningTest()) { Messaging.sendEmail(...); }
Hard SchedulableAsync Apex

8. Schedulable Apex (Medium-Hard): Conditional Batch Size Based on Lead Volume

Problem #248 · Salesforce Apex Coding Challenge

Problem Statement

Write ConditionalBatchScheduler that checks the current volume of unconverted Leads and launches LeadScoreResetBatch with a smaller batch size (100) under high load, and a larger batch size (200) under normal load.

Requirements

  1. Implement Schedulable.
  2. Declare @TestVisible static final Integer THRESHOLD = 1000.
  3. execute():
    • COUNT query: SELECT COUNT() FROM Lead WHERE IsConverted = false.
    • If leadCount > THRESHOLD: batchSize = 100; else batchSize = 200.
    • System.debug the count and chosen batch size in both branches.
    • Database.executeBatch(new LeadScoreResetBatch(), batchSize).

Why Adaptive Batch Sizes?

Smaller chunks reduce governor limit risk under heavy load. Larger chunks mean fewer async jobs under normal volume.

Approach
  • 1COUNT: Integer leadCount = [SELECT COUNT() FROM Lead WHERE IsConverted = false];
  • 2Constant: @TestVisible static final Integer THRESHOLD = 1000;
  • 3Conditional: if (leadCount > THRESHOLD) { batchSize = 100; } else { batchSize = 200; }
  • 4Log both branches so you can tell which path was taken in audit logs.
  • 5Launch: Database.executeBatch(new LeadScoreResetBatch(), batchSize);

Halfway there — solve them live

Every problem above runs in a real in-browser Apex editor with instant pass/fail feedback and best-practice linting. No Salesforce org needed.

Create Free Account →
Hard SchedulableAsync Apex

9. Schedulable Apex (Hard): Adaptive Self-Rescheduling Based on Case Load

Problem #249 · Salesforce Apex Coding Challenge

Problem Statement

Write AdaptiveReschedulingScheduler that checks open Cases created in the last hour. Under high load (open cases > threshold) it reschedules itself every 15 minutes; under normal load every 60 minutes. A @TestVisible static helper builds the dynamic CRON expression.

Requirements

  1. Implement Schedulable.
  2. Constants: JOB_NAME, HIGH_LOAD_INTERVAL_MINUTES = 15, NORMAL_INTERVAL_MINUTES = 60, @TestVisible static final Integer CASE_THRESHOLD = 100.
  3. @TestVisible static String buildCronExpression(Integer intervalMinutes):
    • DateTime nextRun = DateTime.now().addMinutes(intervalMinutes)
    • Return String.format('0 {0} {1} {2} {3} ? {4}', new List<Object>{ nextRun.minute(), nextRun.hour(), nextRun.day(), nextRun.month(), nextRun.year() })
  4. execute():
    • COUNT Cases WHERE IsClosed = false AND CreatedDate >= :oneHourAgo.
    • Choose interval based on openCaseCount > CASE_THRESHOLD.
    • Guard abort + reschedule with !Test.isRunningTest().
Approach
  • 1CRON builder: DateTime nextRun = DateTime.now().addMinutes(intervalMinutes); String.format('0 {0} {1} {2} {3} ? {4}', new List{ nextRun.minute(), nextRun.hour(), nextRun.day(), nextRun.month(), nextRun.year() })
  • 2Open cases: Integer count = [SELECT COUNT() FROM Case WHERE IsClosed = false AND CreatedDate >= :oneHourAgo];
  • 3Adaptive: intervalMinutes = count > CASE_THRESHOLD ? HIGH_LOAD_INTERVAL_MINUTES : NORMAL_INTERVAL_MINUTES;
  • 4Abort first: System.abortJob(sc.getTriggerId()); — prevents duplicate scheduled jobs.
  • 5Re-schedule: System.schedule(JOB_NAME, buildCronExpression(intervalMinutes), new AdaptiveReschedulingScheduler());
  • Easy Schedulable

    10. Schedulable Apex: Nightly Stale Waiting-Case Closer

    Problem #295 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a schedulable class NightlyCaseCloseSchedulable that launches the StaleWaitingCaseBatch every night.

    Requirements

    • Implement Schedulable interface
    • execute(SchedulableContext ctx) calls Database.executeBatch(new StaleWaitingCaseBatch(), BATCH_SCOPE)
    • Store the batch scope size in a named constant (BATCH_SCOPE = 200)
    • The class must be global

    How to Schedule

    String cron = '0 0 2 * * ?'; // 2 AM daily
    System.schedule('Nightly Case Closer', cron, new NightlyCaseCloseSchedulable());
    Approach
    • 1Implement the Schedulable interface — the only required method is execute(SchedulableContext).
    • 2Database.executeBatch(batchInstance, scope) accepts a batch size as the second argument.
    • 3A scope of 200 is the Salesforce default and a safe starting point.
    • 4Schedule with System.schedule('Job Name', cronExpression, new NightlyCaseCloseSchedulable()).
    Easy Schedulable

    11. Schedulable Apex: Weekly Account Audit Batch Launcher

    Problem #296 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a schedulable class WeeklyAccountAuditSchedulable that runs the NoActivityAccountBatch every week to flag accounts with no contacts or opportunities.

    Requirements

    • Implement Schedulable interface (global class)
    • execute(SchedulableContext ctx) launches NoActivityAccountBatch with a batch scope of 100
    • Scope stored in a named constant

    Suggested Cron

    // Every Sunday at 1 AM
    String cron = '0 0 1 ? * SUN';
    System.schedule('Weekly Account Audit', cron, new WeeklyAccountAuditSchedulable());
    Approach
    • 1The Schedulable execute method signature must match: global void execute(SchedulableContext ctx).
    • 2Use Database.executeBatch(new NoActivityAccountBatch(), BATCH_SCOPE).
    • 3Scope 100 is safe for subquery-heavy batches that may hit SOQL limits.
    • 4Weekly cron: '0 0 1 ? * SUN' runs at 1 AM every Sunday.
    Easy Schedulable

    12. Schedulable Apex: Daily Overdue Opportunity Follow-Up Batch

    Problem #297 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a schedulable class DailyOppFollowUpSchedulable that fires the OverdueOppFollowUpBatch every morning to create follow-up tasks for overdue open opportunities.

    Requirements

    • global class implementing Schedulable
    • execute(SchedulableContext ctx) launches OverdueOppFollowUpBatch with scope 200
    • Scope stored in a named constant
    Approach
    • 1Daily morning cron: '0 0 7 * * ?' runs at 7 AM every day.
    • 2Pass the constant as the second argument: Database.executeBatch(new OverdueOppFollowUpBatch(), BATCH_SCOPE).
    Medium Schedulable

    13. Schedulable Apex: Monthly New Lead Count Summary Email to Admin

    Problem #298 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a schedulable class MonthlyLeadSummarySchedulable that runs on the first of each month, counts new unconverted Lead records created during the current month, and sends a summary email to an admin address.

    Requirements

    • global class implementing Schedulable
    • execute(SchedulableContext ctx):
      • Compute firstOfMonth using Date.today().toStartOfMonth()
      • Count unconverted leads created on or after firstOfMonth
      • Build a Messaging.SingleEmailMessage and send with Messaging.sendEmail()
    • Store admin email and subject in named constants
    Approach
    • 1Date.today().toStartOfMonth() returns the 1st day of the current month as a Date.
    • 2Use [SELECT COUNT() FROM Lead WHERE IsConverted = false AND CreatedDate >= :firstOfMonth].
    • 3Set email recipients with email.setToAddresses(new List{ ADMIN_EMAIL }).
    • 4Messaging.sendEmail() accepts a List.
    Hard Schedulable

    14. Schedulable Apex: Self-Rescheduling Uncontacted Lead Reassigner

    Problem #299 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a schedulable class LeadReassignSchedulable that:

    1. Launches UncontactedLeadReassignBatch
    2. Aborts itself using ctx.getTriggerId()
    3. Re-schedules itself for the next day at 3 AM

    This self-rescheduling pattern is used when your org has limited scheduled job slots and you need fine-grained control over timing.

    Requirements

    • global class implementing Schedulable
    • Constants: BATCH_SCOPE = 200, JOB_NAME = 'Lead_Reassign_Daily', DAILY_CRON = '0 0 3 * * ?'
    • execute() must: run batch → abort current job → re-schedule
    • Use System.abortJob(ctx.getTriggerId()) to abort the current instance
    • Use System.schedule(JOB_NAME, DAILY_CRON, new LeadReassignSchedulable()) to re-schedule
    Approach
    • 1Call executeBatch first — aborting the job then rescheduling is safe from a Salesforce platform perspective.
    • 2ctx.getTriggerId() returns the CronTrigger Id of the currently executing scheduled job.
    • 3System.abortJob() removes the current schedule entry — without it you accumulate duplicate jobs.
    • 4System.schedule() creates a new CronTrigger — the class re-schedules itself fresh each time.
    Easy Schedulable

    15. Schedulable: Weekly Inactive Lead Email Report

    Problem #364 · Salesforce Apex Coding Challenge

    Problem Statement

    Write WeeklyInactiveLeadReportScheduler that fires every Monday at 8 AM and emails a summary of inactive Leads to the running user.

    Requirements

    • global class implementing Schedulable.
    • Declare CRON_EXPRESSION = '0 0 8 ? * MON'.
    • execute(): query inactive Leads; System.debug the count; send email.
    • Guard with String.isBlank(recipientEmail); wrap send in try/catch.
    • scheduleWeeklyJob(): calls System.schedule and System.debugs the job ID — visible in the Output tab.
    Approach
    • 1Cron: '0 0 8 ? * MON' = fire at 8:00 AM every Monday.
    • 2Query: SELECT Id, FirstName, LastName FROM Lead WHERE Status NOT IN ('Closed - Converted','Closed - Not Converted') AND LastModifiedDate < :cutoffDate WITH SECURITY_ENFORCED
    • 3System.debug('Sending report for ' + inactiveLeads.size() + ' inactive leads') — output shows in Console tab.
    • 4Messaging.SingleEmailMessage: setToAddresses, setSubject, setPlainTextBody — then Messaging.sendEmail(list) wrapped in try/catch.

    Practice All 15 Problems Free

    Write and run real Apex code right in your browser — instant pass/fail feedback, best-practice linting, and governor limit monitoring. No Salesforce org needed.

    Create Free Account → Explore All Problems
    About ApexArena

    ApexArena is a free, browser-based Salesforce Apex coding practice platform covering every major topic tested on the Salesforce Platform Developer I (PD1) and Platform Developer II (PD2) certification exams. All problems run directly in your browser with instant pass/fail feedback, best-practice linting (SOQL in loops, DML in loops, empty catch blocks), and governor limit monitoring — no Salesforce Developer Edition org required.

    Related tutorials: Apex Triggers · SOQL · Batch Apex · Interview Q&A · Governor Limits