Schedulable Apex

Schedulable Apex Practice Problems

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

This guide walks through 30 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
  16. 16. Schedulable Apex: Prevent Duplicate Scheduled Jobs
  17. 17. Schedulable Apex: Nightly Stale Lead Batch Launcher
  18. 18. Schedulable Apex: Self-Rescheduling Hourly Job With a Stop Guard
  19. 19. Scenario: Nightly Contract Expiration Digest Log
  20. 20. Schedulable Apex: Daily Account Snapshot Backup
  21. 21. Schedulable Apex: Weekly Customer Growth Report Email
  22. 22. Schedulable Apex: Daily Follow-Up Task for Stalling Opportunities
  23. 23. Schedulable Apex: Nightly Stale-Flag Account Cleanup
  24. 24. Schedulable Apex: Monthly Subscription Invoice Run
  25. 25. Schedulable Apex: Daily Product Inventory Status Refresh
  26. 26. Schedulable Apex: Monthly Territory Assignment by Billing State
  27. 27. Schedulable Apex: Daily ERP Sync Job Launcher
  28. 28. Schedulable Apex: Weekly Contact Compliance Field Audit
  29. 29. Schedulable Apex: Multi-Region Weekly Sales Report Dispatcher
  30. 30. Schedulable Apex: Automated Payment-to-Invoice Reconciliation
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);
  • 2Query Leads, filtering to only those that are converted and whose ConvertedDate is older than the 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 each AggregateResult row with ar.get('') using whatever aliases you chose for the grouped field, count, and sum.
  • 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
  • 1Query Opportunities that are still open, overdue (past their close date), and not yet flagged as at-risk.
  • 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():
    • Run a COUNT query against Contract to tally how many Activated contracts have an EndDate falling within the current calendar month.
    • Build a Messaging.SingleEmailMessage with setToAddresses, setSubject, setPlainTextBody.
    • Guard Messaging.sendEmail() with !Test.isRunningTest().
    • Always System.debug the summary.
Approach
  • 1SOQL has a relative date literal that covers the entire current calendar month, so you don't need to compute month boundaries yourself.
  • 2The COUNT() aggregate function returns a plain Integer directly, without wrapping it in a list of records.
  • 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():
    • Run an aggregate query against Opportunity that returns both a count and a sum of Amount (each with an alias), restricted to won opportunities that closed during the current 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
  • 1Alias each aggregate function in the SELECT list so you can retrieve its value from the AggregateResult by name afterward.
  • 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():
    • Run a COUNT query against Lead to tally how many leads have not yet been converted.
    • 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
  • 1The COUNT() aggregate function returns a plain Integer directly — no need to loop over a result list.
  • 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);
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():
    • Get a COUNT() of open Cases (IsClosed = false) created since oneHourAgo (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: run a COUNT() query on Case filtered to open records (IsClosed = false) created since oneHourAgo (CreatedDate >= :oneHourAgo), and store the result in Integer count.
  • 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 for Leads that are not yet closed (exclude both closed-converted and closed-not-converted statuses) and whose LastModifiedDate is older than the 30-day cutoff — remember 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.

    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 →
    Medium Schedulable

    16. Schedulable Apex: Prevent Duplicate Scheduled Jobs

    Problem #390 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a Schedulable class DailyLeadCleanupScheduler that launches FlagStaleLeadsBatch when it fires.

    Add a static helper scheduleIdempotent(String cronExpression) that can be called any number of times without ever creating duplicate scheduled jobs — it should abort any existing job with the same name before scheduling a new one.

    Requirements

    • execute(SchedulableContext ctx) calls Database.executeBatch(new FlagStaleLeadsBatch(), 200).
    • A public static final String JOB_NAME constant names the job consistently.
    • scheduleIdempotent(cronExpression): query CronTrigger joined to CronJobDetail.Name for the job name, call System.abortJob() on any match, then System.schedule(...) a fresh run and return its Id.

    Best Practices

    • Re-running setup code (e.g. a deployment post-install script, or an admin clicking a "reschedule" button twice) is a very common way scheduled jobs end up duplicated — aborting-then-rescheduling by name is the standard fix.
    • Query CronTrigger via its relationship to CronJobDetail.Name rather than trying to track job Ids yourself across deploys.
    • Keep the job name as one named constant so the scheduling code and the lookup query can never drift out of sync.
    Approach
    • 1CronTrigger has a relationship field CronJobDetail.Name you can filter on directly in SOQL.
    • 2System.abortJob(ct.Id) cancels an existing scheduled job by its CronTrigger Id.
    • 3System.schedule(jobName, cronExpression, schedulableInstance) returns the new job's Id — return that directly.
    • 4Always abort BEFORE scheduling the new one, so there's never a moment with two active jobs of the same name.
    Easy Schedulable

    17. Schedulable Apex: Nightly Stale Lead Batch Launcher

    Problem #391 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a Schedulable class NightlyStaleLeadLauncher whose only job is to launch FlagStaleLeadsBatch every time it fires.

    Requirements

    • Implement Schedulable.
    • execute(SchedulableContext ctx) calls Database.executeBatch(new FlagStaleLeadsBatch(), 200).

    Best Practices

    • Keep the Schedulable class this thin on purpose — it's just a trigger for the batch, not a place to put business logic.
    • A cron expression like '0 0 2 * * ?' (2:00 AM daily) is passed to System.schedule(...) separately, one time, outside this class — the class itself doesn't need to know its own schedule.
    Approach
    • 1A Schedulable class only needs one method: execute(SchedulableContext ctx).
    • 2Database.executeBatch(new FlagStaleLeadsBatch(), 200) is the entire body of execute() here.
    • 3Scheduling this class (System.schedule with a cron string) happens separately, wherever setup code runs — not inside the class.
    Hard Schedulable

    18. Schedulable Apex: Self-Rescheduling Hourly Job With a Stop Guard

    Problem #392 · Salesforce Apex Coding Challenge

    Problem Statement

    Write a Schedulable class HourlyMonitorJob that logs the number of open Cases, then reschedules itself to run again exactly one hour later — unless a Custom Setting says to stop.

    Requirements

    • execute() logs the current count of open Cases via System.debug.
    • Check a hierarchy Custom Setting Monitor_Settings__c — if its Stop_Scheduling__c checkbox is true, do not reschedule and return immediately.
    • Otherwise, compute a cron expression for exactly one hour from now and call System.schedule(...) with a unique job name (each run's job name must differ, since a fixed name would collide with the still-existing prior job).

    Best Practices

    • Self-rescheduling without a stop condition is a classic way to create a job that runs forever with no clean way to turn it off except manually aborting the CronTrigger — always build in an explicit off-switch.
    • Each scheduled instance needs a unique job name (e.g. suffixed with a timestamp) — reusing the same name while the previous run's job might still be registered throws a duplicate-job error.
    • Do the real work before deciding whether to reschedule, so the very last run (the one that decides to stop) still completes its job.
    Approach
    • 1Monitor_Settings__c.getInstance() reads the org-default (or hierarchy) Custom Setting record without a SOQL query.
    • 2DateTime.now().addHours(1) gives you the exact instant to run again; String.format(...) can build the 6/7-field cron expression from its second/minute/hour/day/month/year.
    • 3A fixed job name would collide with the currently-running job's own CronTrigger entry — suffix the name with something unique per run, e.g. the target DateTime's epoch millis.
    • 4Always do the actual monitoring work before checking the stop flag, so the final scheduled run still completes its real job.
    Hard Schedulable

    19. Scenario: Nightly Contract Expiration Digest Log

    Problem #448 · Salesforce Apex Coding Challenge

    Business Scenario

    The renewals team wants a nightly log of every active Contract expiring within the next 30 days, so they can proactively reach out before contracts lapse.

    Requirements

    • ContractExpirationDigestScheduler (Schedulable) launches ContractExpirationDigestBatch when it fires.
    • The batch queries Contracts that are currently in Activated status and whose EndDate falls within the next 30 days (today through today+30, inclusive).
    • For each matching Contract, insert one Contract_Expiration_Log__c record capturing the Contract, its Account, the expiration date, and when it was logged.

    Best Practices

    • Keep the Schedulable thin — its only job is to launch the batch that does the real work.
    • Bound the date window on both ends so already-expired Contracts don't get logged again every night.
    Approach
    • 1Date windowEnd = Date.today().addDays(30); gives the far end of the 30-day window.
    • 2Bound the query on both sides of the window — otherwise already-expired Contracts would be logged every single night.
    • 3'Activated' is the standard Contract status for a currently-active agreement.
    Easy Schedulable

    20. Schedulable Apex: Daily Account Snapshot Backup

    Problem #516 · Salesforce Apex Coding Challenge

    Business Scenario

    Compliance wants a nightly point-in-time snapshot of every Account, in case a bad data-load or user error needs to be rolled back manually. Apex cannot write to a real filesystem, so this is modeled as a nightly export into a custom log object, Backup_Log__c.

    Requirements

    • Write DailyAccountBackupScheduler implementing Schedulable, firing daily at 2 AM via CRON_EXPRESSION = '0 0 2 * * ?'.
    • execute() queries all Accounts and serializes each one to JSON into a Backup_Log__c.Snapshot_Data__c field, along with Source_Record_Id__c, Object_Type__c, and today's Backup_Date__c.
    • Insert all snapshot records with a single bulk DML call.
    • Provide scheduleDailyBackup() to register the job via System.schedule.

    Best Practices

    • JSON.serialize(a) captures the full record state in one field without needing a rigid schema per object type — a pragmatic way to model "backup" inside Apex's record-based model.
    • All snapshot records are built in memory and inserted with one bulk DML call, never per record.
    • Wrap the insert in try/catch(DmlException) so a bad chunk doesn't silently crash the whole scheduled job.
    Approach
    • 1Cron '0 0 2 * * ?' fires at 2:00 AM every day.
    • 2JSON.serialize(a) turns the whole Account record into a single string you can store in Snapshot_Data__c.
    • 3Build the full List in memory first, then insert it once outside any loop.
    • 4Wrap the insert in try/catch(DmlException) to keep one bad record from crashing the scheduled job.
    Easy Schedulable

    21. Schedulable Apex: Weekly Customer Growth Report Email

    Problem #517 · Salesforce Apex Coding Challenge

    Business Scenario

    Leadership wants a lightweight weekly pulse on customer growth — new Accounts created and deals closed in the last 7 days — emailed automatically every Monday morning.

    Requirements

    • Write WeeklyCustomerReportScheduler implementing Schedulable, firing every Monday at 6 AM via CRON_EXPRESSION = '0 0 6 ? * MON'.
    • Count Accounts created in the last 7 days, and Opportunities that reached Closed Won in the last 7 days, using SELECT COUNT().
    • Guard against a blank recipient email before attempting to send.
    • Send a single summary email via Messaging.SingleEmailMessage, wrapped in try/catch.

    Best Practices

    • SELECT COUNT() avoids pulling full record data into memory just to count rows.
    • Guarding on a blank recipientEmail before building/sending the email avoids a wasted Messaging.sendEmail call that would simply fail.
    • Wrapping the send in try/catch(Exception) keeps a transient email-service issue from surfacing as a hard job failure.
    Approach
    • 1Cron '0 0 6 ? * MON' fires at 6:00 AM every Monday.
    • 2A COUNT() aggregate query lets you get the number of matching Accounts (or Opportunities) directly as an Integer, without pulling back the records themselves.
    • 3Guard: if (String.isBlank(recipientEmail)) return; before building the email.
    • 4Messaging.SingleEmailMessage: setToAddresses, setSubject, setPlainTextBody, then Messaging.sendEmail wrapped in try/catch.
    Easy Schedulable

    22. Schedulable Apex: Daily Follow-Up Task for Stalling Opportunities

    Problem #518 · Salesforce Apex Coding Challenge

    Business Scenario

    Sales managers keep losing track of Opportunities that have gone quiet. Every morning, a scheduled job should create a follow-up Task for the owner of any open Opportunity with no activity in the last two weeks.

    Requirements

    • Write DailyOpportunityReminderScheduler implementing Schedulable, firing daily at 7 AM via CRON_EXPRESSION = '0 0 7 * * ?'.
    • An Opportunity qualifies when it's open (IsClosed = false) and LastActivityDate is more than NO_ACTIVITY_DAYS (14) days old, or was never set.
    • Create one Task per qualifying Opportunity, assigned to its OwnerId, linked via WhatId, with Priority = 'High'.
    • Insert all Tasks in a single bulk DML call.

    Best Practices

    • Filtering directly in SOQL (IsClosed = false, the activity-date window) keeps each run's working set small instead of pulling every Opportunity and filtering in Apex.
    • All Task records are assembled in a list first; the single insert reminders; call outside the loop keeps this governor-safe at any Opportunity volume.
    • Wrap the DML in try/catch(DmlException) so a validation rule on one Task doesn't take down the whole scheduled run.
    Approach
    • 1Cron '0 0 7 * * ?' fires at 7:00 AM every day.
    • 2Query filter: IsClosed = false AND (LastActivityDate < :cutoff OR LastActivityDate = null).
    • 3Set Task.WhatId to the Opportunity Id to link it, and Task.OwnerId to the Opportunity owner.
    • 4insert reminders; once outside the loop, wrapped in try/catch(DmlException).
    Easy Schedulable

    23. Schedulable Apex: Nightly Stale-Flag Account Cleanup

    Problem #519 · Salesforce Apex Coding Challenge

    Business Scenario

    Data quality flags Accounts missing key fields with Needs_Review__c = true. Once a rep fills in the missing Website and Phone, the stale flag is never cleared automatically. A nightly job should clear it once both fields are populated.

    Requirements

    • Write NightlyAccountCleanupScheduler implementing Schedulable, firing daily at 1:30 AM via CRON_EXPRESSION = '0 30 1 * * ?'.
    • Query only Accounts where Needs_Review__c = true and both Website and Phone are now populated.
    • Clear Needs_Review__c back to false for each one.
    • Perform the update as a single bulk DML call.

    Best Practices

    • Filtering for "both fields now populated" directly in SOQL means the job only ever touches Accounts that genuinely need the flag cleared.
    • One bulk update call outside any loop keeps this governor-safe regardless of how many Accounts qualify on a given night.
    • Wrap the DML in try/catch(DmlException) to isolate a bad record from failing the whole scheduled run.
    Approach
    • 1Cron '0 30 1 * * ?' fires at 1:30 AM every day.
    • 2Only Accounts still flagged for review, and only once both previously-missing fields have a value, should qualify.
    • 3Set Needs_Review__c = false on a new Account(Id = a.Id, ...) — no need to re-set Website/Phone.
    • 4update toUpdate; once outside the loop, wrapped in try/catch(DmlException).
    Medium Schedulable

    24. Schedulable Apex: Monthly Subscription Invoice Run

    Problem #520 · Salesforce Apex Coding Challenge

    Business Scenario

    Billing needs one Invoice__c generated on the first of every month for each Active Contract__c with a flat monthly fee. Unlike the annual Batch Apex invoice run, the monthly population is small enough that this Schedulable class performs the whole generation itself, without launching a separate batch.

    Requirements

    • Write MonthlyInvoiceScheduler implementing Schedulable, firing on the 1st of each month at 3 AM via CRON_EXPRESSION = '0 0 3 1 * ?'.
    • Query Active Contracts with a non-null Monthly_Fee__c.
    • Idempotency: skip any Contract that already has an Invoice__c for the current month/year.
    • Create one Invoice__c per remaining Contract with Amount__c = Monthly_Fee__c and Status__c = 'Draft'.
    • Insert all invoices in a single bulk DML call.

    Best Practices

    • Checking for an existing invoice per Contract/month/year before inserting makes the job safe to re-run (e.g., after a manual retry) without generating duplicates.
    • Because this Schedulable does real record-generation work (not just launching a batch), it still follows Batch-style bulkification: one query for Contracts, one for existing invoices, one bulk insert — never per-record SOQL/DML.
    • Keep the cadence obvious from the CRON_EXPRESSION itself — day-of-month 1 plus a fixed hour reads as "first of the month" without extra comments.
    Approach
    • 1Cron '0 0 3 1 * ?' fires at 3:00 AM on the 1st of every month.
    • 2Query Invoice__c WHERE Contract__c IN :contractIds AND Invoice_Month__c = :currentMonth AND Invoice_Year__c = :currentYear to find already-invoiced Contracts.
    • 3Skip a Contract entirely if its Id is in the alreadyInvoiced set — this keeps re-runs idempotent.
    • 4insert toInsert; once outside the loop, wrapped in try/catch(DmlException).
    Medium Schedulable

    25. Schedulable Apex: Daily Product Inventory Status Refresh

    Problem #521 · Salesforce Apex Coding Challenge

    Business Scenario

    Warehouse operations wants Product2.Inventory_Status__c refreshed every night to reflect current stock levels, so sales reps see an accurate "In Stock / Low Stock / Out of Stock" badge without a real-time integration.

    Requirements

    • Write DailyInventoryRefreshScheduler implementing Schedulable, firing daily at 4 AM via CRON_EXPRESSION = '0 0 4 * * ?'.
    • Process all active Products (IsActive = true).
    • Status rule: 0 or less → Out of Stock; at or below LOW_STOCK_THRESHOLD (10) → Low Stock; otherwise In Stock.
    • Only update Products whose computed status differs from the current value.

    Best Practices

    • A named constant (LOW_STOCK_THRESHOLD) keeps the low-stock cutoff easy to tune without touching the status logic itself.
    • Comparing the computed status to the current value before adding to toUpdate avoids no-op DML on Products whose stock level hasn't crossed a threshold.
    • One bulk update call outside the loop, wrapped in try/catch(DmlException).
    Approach
    • 1Cron '0 0 4 * * ?' fires at 4:00 AM every day.
    • 2Treat a null Quantity_In_Stock__c as 0 before comparing against thresholds.
    • 3qty <= 0 -> Out of Stock; qty <= LOW_STOCK_THRESHOLD -> Low Stock; else In Stock.
    • 4Compare Inventory_Status__c against the computed value before adding to toUpdate — skip no-ops.
    Hard Schedulable

    26. Schedulable Apex: Monthly Territory Assignment by Billing State

    Problem #522 · Salesforce Apex Coding Challenge

    Business Scenario

    Sales operations maintains a state-to-territory mapping and wants Accounts automatically re-aligned to the correct Territory__c lookup once a month, since reps sometimes leave it blank or stale after a BillingState change.

    Requirements

    • Write MonthlyTerritoryAssignmentScheduler implementing Schedulable, firing on the 1st of each month at 5 AM via CRON_EXPRESSION = '0 0 5 1 * ?'.
    • Maintain an in-memory Map<String, String> of BillingState (two-letter code) to territory name (e.g. West/South/Northeast/Central).
    • Query all Territory__c records once to resolve territory name → Id.
    • Query Accounts with a non-null BillingState; reassign Territory__c only when the resolved territory Id differs from the current value.

    Best Practices

    • Resolving territory names to Ids via one bulk Territory__c query (into a Map) avoids hardcoding Salesforce record Ids, which differ across orgs/sandboxes.
    • States with no mapping entry, or territories that don't exist yet, are safely skipped instead of throwing — a partial rollout of new territories doesn't break the whole job.
    • Comparing against the Account's current Territory__c avoids reassigning (and re-triggering automation on) Accounts that already have the right territory.
    Approach
    • 1Cron '0 0 5 1 * ?' fires at 5:00 AM on the 1st of every month.
    • 2Build territoryByState as a literal Map inside execute() — no custom metadata needed for this exercise.
    • 3Query Territory__c once and build Map territoryIdByName keyed by Name.
    • 4Skip an Account (continue) if its BillingState has no mapping, or the mapped territory doesn't exist yet.
    Hard Schedulable

    27. Schedulable Apex: Daily ERP Sync Job Launcher

    Problem #523 · Salesforce Apex Coding Challenge

    Business Scenario

    The reconciliation logic against staged ERP data needs to run automatically every night, not just on demand. This problem is the Schedulable-launches-a-Batch pattern: a thin Schedulable class whose only job is to kick off the batch reconciliation job on a fixed daily cadence.

    Requirements

    • Write DailyErpSyncScheduler implementing Schedulable, firing daily at 1 AM via CRON_EXPRESSION = '0 0 1 * * ?'.
    • execute(SchedulableContext sc) must do nothing except call Database.executeBatch(new ErpSyncBatch(), BATCH_SCOPE_SIZE) and log the returned batch job Id.
    • Include the ErpSyncBatch class it launches, implementing Database.Batchable<SObject> against unreconciled ERP_Sync_Log__c rows.
    • Provide scheduleDailySync() to register the Schedulable job.

    Best Practices

    • A Schedulable's execute() should stay thin when its real job is to hand off to a Batch — the Batch, not the Schedulable, is where governor-limit-sensitive bulk work belongs.
    • Using a named constant (BATCH_SCOPE_SIZE) for the batch's chunk size keeps it easy to tune independently of the scheduling cadence.
    • Even the launched batch keeps its own DML bulkified and wrapped in try/catch(DmlException) — launching from a Schedulable doesn't relax those rules.
    Approach
    • 1Cron '0 0 1 * * ?' fires at 1:00 AM every day.
    • 2Database.executeBatch(new ErpSyncBatch(), BATCH_SCOPE_SIZE) returns the AsyncApexJob Id for the launched batch.
    • 3The Schedulable execute() should contain nothing but the executeBatch call and a debug log — all real work belongs in the batch.
    • 4ErpSyncBatch still needs its own try/catch(DmlException) around its bulk update — being launched from a scheduler doesn't change that.
    Expert Schedulable

    28. Schedulable Apex: Weekly Contact Compliance Field Audit

    Problem #524 · Salesforce Apex Coding Challenge

    Business Scenario

    Legal requires every Contact with an email on file to have a recorded consent date and a signed data-processing agreement. A weekly job should detect violations, log them for audit trail purposes, and flag/unflag the Contact so reports stay accurate as records are fixed.

    Requirements

    • Write ComplianceAuditScheduler implementing Schedulable, firing weekly on Sunday at 3 AM via CRON_EXPRESSION = '0 0 3 ? * SUN'.
    • A Contact with an email violates compliance if Consent_Date__c is null, or Data_Processing_Agreement__c is not true.
    • For each newly-violating Contact, set Compliance_Flag__c = true and insert a Compliance_Violation__c record capturing the specific reason(s).
    • For a Contact that is no longer violating but is still flagged, clear Compliance_Flag__c back to false (no new violation record).
    • Perform all Contact updates and violation inserts as separate single bulk DML calls.

    Best Practices

    • Recording why a Contact is out of compliance (via Violation_Reason__c) makes the audit trail actually useful instead of a generic pass/fail flag.
    • Only writing a new violation record when the flag transitions to true avoids duplicate violation rows on every single weekly run for the same ongoing issue.
    • Un-flagging Contacts that have since been fixed keeps compliance dashboards trustworthy.
    • Two focused bulk DML calls (Contact updates, Violation inserts) — never per-record DML.
    Approach
    • 1Cron '0 0 3 ? * SUN' fires at 3:00 AM every Sunday.
    • 2Build a List reasons per Contact; join with String.join(reasons, '; ') for Violation_Reason__c.
    • 3Only insert a new Compliance_Violation__c when the Contact is transitioning INTO violation (Compliance_Flag__c was not already true).
    • 4A Contact that is no longer violating but still flagged just needs Compliance_Flag__c cleared — no violation record for that case.
    Expert Schedulable

    29. Schedulable Apex: Multi-Region Weekly Sales Report Dispatcher

    Problem #525 · Salesforce Apex Coding Challenge

    Business Scenario

    A multi-region sales org wants each Regional Manager to receive their own weekly sales summary email — scoped only to their region's Closed Won results — dispatched from a single scheduled job rather than one job per region.

    Requirements

    • Write MultiRegionReportScheduler implementing Schedulable, firing every Monday at 6 AM via CRON_EXPRESSION = '0 0 6 ? * MON'.
    • Aggregate the last 7 days of Closed Won Opportunity revenue and deal count per region with a single GROUP BY Account.Region__c query.
    • Query all Regional_Manager__c records with a non-null Manager_Email__c.
    • Build one region-scoped email per manager (region with zero deals still gets an email reporting zero), and send them all in a single Messaging.sendEmail call.

    Best Practices

    • One aggregate query grouped by region, rather than one query per region, keeps this governor-safe regardless of how many regions exist.
    • Building a Map<String, AggregateResult> keyed by region lets each manager's email be assembled with an O(1) lookup instead of a nested loop/query per manager.
    • Batching every manager's email into one list and calling Messaging.sendEmail(emails) once respects the single-transaction email limit far better than sending one at a time.
    Approach
    • 1Cron '0 0 6 ? * MON' fires at 6:00 AM every Monday.
    • 2A single aggregate query grouped by Account.Region__c, summing revenue and counting deals, gives you one summarized row per region in one shot.
    • 3Store each AggregateResult in a Map keyed by region for O(1) lookup per manager.
    • 4Collect every Messaging.SingleEmailMessage into one List and call Messaging.sendEmail(emails) exactly once.
    Master Schedulable

    30. Schedulable Apex: Automated Payment-to-Invoice Reconciliation

    Problem #526 · Salesforce Apex Coding Challenge

    Business Scenario

    Finance needs open Invoices automatically reconciled against completed Payments every night: fully-paid invoices should close themselves out, and any overpayment should be logged for manual review rather than silently ignored.

    Requirements

    • Write FinancialReconciliationScheduler implementing Schedulable, firing daily at 2 AM via CRON_EXPRESSION = '0 0 2 * * ?'.
    • Query open Invoices (Status__c IN ('Draft','Sent')).
    • Aggregate completed Payment__c totals per Invoice with one GROUP BY Invoice__c query.
    • If the paid total matches the invoice amount within AMOUNT_TOLERANCE (0.01), mark the Invoice Paid.
    • If the paid total exceeds the invoice amount beyond tolerance, log a Reconciliation_Discrepancy__c record (type Overpayment) instead of silently marking it paid.

    Best Practices

    • A small AMOUNT_TOLERANCE avoids treating floating-point/rounding noise as a real discrepancy.
    • Explicitly logging overpayments (rather than just marking them Paid) surfaces a real financial issue for a human to review instead of hiding it.
    • One aggregate query for all Payments in the invoice set, and two focused bulk DML calls (Invoice update, Discrepancy insert) — never per-record SOQL or DML.
    Approach
    • 1Cron '0 0 2 * * ?' fires at 2:00 AM every day.
    • 2Aggregate completed Payment__c amounts per Invoice in a single grouped query keyed by Invoice__c, so every invoice's paid total is resolved in one pass.
    • 3difference = invoiceAmount - paidTotal; Math.abs(difference) <= AMOUNT_TOLERANCE means fully paid.
    • 4A negative difference beyond tolerance means the paid total exceeds the invoice amount — log it as an Overpayment discrepancy.

    Practice All 30 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