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.
Write a Schedulable Apex class DailyLeadCleanupScheduler that deletes
Leads which were converted more than 90 days ago.
Schedulable interface.execute(SchedulableContext sc): calculate a cutoff date 90 days in the past.IsConverted = true AND ConvertedDate < cutoff.if (!staleLeads.isEmpty()).:cutoff) — never concatenate dates into SOQL strings.String cron = '0 0 1 * * ?'; // 1 AM daily
System.schedule('Daily Lead Cleanup', cron, new DailyLeadCleanupScheduler());Write MonthlyAccountBatchScheduler that implements Schedulable
and launches AccountRevenueTierBatch with a batch size of 200 when fired.
Schedulable.@TestVisible private Integer batchSize = 200 instance variable.execute(): instantiate AccountRevenueTierBatch and call
Database.executeBatch(batch, batchSize).@TestVisible so tests can override the batch size without exposing it publicly.Schedulable and Batchable in one class.executeBatch to avoid the default 200 silently changing behaviour.// Run at midnight on the 1st of every month
System.schedule('Monthly Account Batch', '0 0 0 1 * ?', new MonthlyAccountBatchScheduler());Write QuarterlyPipelineScheduler that implements Schedulable
and sends an email summarising open Opportunities closing this quarter, grouped by stage.
Schedulable.@TestVisible private String recipientEmail.execute(): run an aggregate SOQL with
StageName, COUNT(Id), SUM(Amount) filtered by
IsClosed = false AND CloseDate = THIS_QUARTER.AggregateResult list.Messaging.SingleEmailMessage.try-catch(Exception e) with
System.debug on error.THIS_QUARTER is a SOQL date literal — no manual date arithmetic needed.@TestVisible so tests can override the email address without
making it public.Messaging.sendEmail in try-catch — email limits can throw.Write HourlyMonitorScheduler that:
CloseDate < today) as at-risk
by setting At_Risk__c = true.buildNextHourCron().Schedulable.@TestVisible static final String JOB_NAME.try block; self-reschedule in
finally guarded by !Test.isRunningTest().buildNextHourCron(): returns a cron expression for the next hour
(@TestVisible private static String).isEmpty() guard before DML.finally for self-reschedule — it runs even when the business
logic throws.!Test.isRunningTest() prevents infinite scheduling in unit tests.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.
Schedulable interface.@TestVisible private Integer batchSize = 200.execute(SchedulableContext sc) — instantiate LeadScoreResetBatch
and call Database.executeBatch(batch, batchSize).@TestVisible on batchSize lets tests inject a smaller size
without exposing the field publicly.System.schedule('DailyLeadCleanup', '0 0 2 * * ?', new DailyLeadCleanupScheduler());Write EndOfMonthContractScheduler that runs on a schedule, counts
all Activated Contracts expiring this month, and
sends a summary email to a configurable recipient.
Schedulable.@TestVisible private String recipientEmail = 'admin@salesforce.com'.execute():
SELECT COUNT() FROM Contract WHERE EndDate = THIS_MONTH AND Status = 'Activated'.Messaging.SingleEmailMessage with
setToAddresses, setSubject, setPlainTextBody.Messaging.sendEmail() with !Test.isRunningTest().System.debug the summary.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.
Schedulable.@TestVisible private String recipientEmail.execute():
SELECT COUNT(Id) cnt, SUM(Amount) totalAmount FROM Opportunity WHERE IsWon = true AND CloseDate = THIS_WEEK.(Integer) results[0].get('cnt') and (Decimal) results[0].get('totalAmount').totalAmount (set to 0 if null).!Test.isRunningTest()).Aggregate SOQL (with COUNT, SUM, AVG etc.) returns AggregateResult[].
Use result.get('alias') and cast to the expected type to read each column.
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.
Schedulable.@TestVisible static final Integer THRESHOLD = 1000.execute():
SELECT COUNT() FROM Lead WHERE IsConverted = false.leadCount > THRESHOLD: batchSize = 100; else batchSize = 200.System.debug the count and chosen batch size in both branches.Database.executeBatch(new LeadScoreResetBatch(), batchSize).Smaller chunks reduce governor limit risk under heavy load. Larger chunks mean fewer async jobs under normal volume.
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 →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.
Schedulable.JOB_NAME, HIGH_LOAD_INTERVAL_MINUTES = 15,
NORMAL_INTERVAL_MINUTES = 60,
@TestVisible static final Integer CASE_THRESHOLD = 100.@TestVisible static String buildCronExpression(Integer intervalMinutes):
DateTime nextRun = DateTime.now().addMinutes(intervalMinutes)String.format('0 {0} {1} {2} {3} ? {4}', new List<Object>{ nextRun.minute(), nextRun.hour(), nextRun.day(), nextRun.month(), nextRun.year() })execute():
IsClosed = false AND CreatedDate >= :oneHourAgo.openCaseCount > CASE_THRESHOLD.!Test.isRunningTest().Write a schedulable class NightlyCaseCloseSchedulable that
launches the StaleWaitingCaseBatch every night.
Schedulable interfaceexecute(SchedulableContext ctx) calls
Database.executeBatch(new StaleWaitingCaseBatch(), BATCH_SCOPE)BATCH_SCOPE = 200)globalString cron = '0 0 2 * * ?'; // 2 AM daily
System.schedule('Nightly Case Closer', cron, new NightlyCaseCloseSchedulable());Write a schedulable class WeeklyAccountAuditSchedulable
that runs the NoActivityAccountBatch every week to flag
accounts with no contacts or opportunities.
Schedulable interface (global class)execute(SchedulableContext ctx) launches
NoActivityAccountBatch with a batch scope of 100// Every Sunday at 1 AM
String cron = '0 0 1 ? * SUN';
System.schedule('Weekly Account Audit', cron, new WeeklyAccountAuditSchedulable());Write a schedulable class DailyOppFollowUpSchedulable
that fires the OverdueOppFollowUpBatch every morning to
create follow-up tasks for overdue open opportunities.
global class implementing Schedulableexecute(SchedulableContext ctx) launches
OverdueOppFollowUpBatch with scope 200Write 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.
global class implementing Schedulableexecute(SchedulableContext ctx):
firstOfMonth using Date.today().toStartOfMonth()firstOfMonthMessaging.SingleEmailMessage and send with
Messaging.sendEmail()Write a schedulable class LeadReassignSchedulable that:
UncontactedLeadReassignBatchctx.getTriggerId()This self-rescheduling pattern is used when your org has limited scheduled job slots and you need fine-grained control over timing.
global class implementing SchedulableBATCH_SCOPE = 200,
JOB_NAME = 'Lead_Reassign_Daily',
DAILY_CRON = '0 0 3 * * ?'execute() must: run batch → abort current job → re-scheduleSystem.abortJob(ctx.getTriggerId()) to abort the current instanceSystem.schedule(JOB_NAME, DAILY_CRON, new LeadReassignSchedulable()) to re-scheduleWrite WeeklyInactiveLeadReportScheduler that fires every Monday at 8 AM
and emails a summary of inactive Leads to the running user.
global class implementing Schedulable.CRON_EXPRESSION = '0 0 8 ? * MON'.execute(): query inactive Leads; System.debug the count; send email.String.isBlank(recipientEmail); wrap send in try/catch.scheduleWeeklyJob(): calls System.schedule and System.debugs the job ID — visible in the Output tab.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 ProblemsApexArena 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