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.
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():
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():
(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():
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.
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():
COUNT() of open Cases (IsClosed = false) created
since oneHourAgo (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.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 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.
execute(SchedulableContext ctx) calls
Database.executeBatch(new FlagStaleLeadsBatch(), 200).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.CronTrigger via its relationship to CronJobDetail.Name
rather than trying to track job Ids yourself across deploys.Write a Schedulable class NightlyStaleLeadLauncher whose only job is to
launch FlagStaleLeadsBatch every time it fires.
Schedulable.execute(SchedulableContext ctx) calls
Database.executeBatch(new FlagStaleLeadsBatch(), 200).'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.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.
execute() logs the current count of open Cases via System.debug.Monitor_Settings__c — if its
Stop_Scheduling__c checkbox is true, do not reschedule and
return immediately.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).CronTrigger — always build in an explicit off-switch.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.
ContractExpirationDigestScheduler (Schedulable) launches
ContractExpirationDigestBatch when it fires.EndDate falls within the next 30 days (today through today+30, inclusive).Contract_Expiration_Log__c record
capturing the Contract, its Account, the expiration date, and when it was logged.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.
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.scheduleDailyBackup() to register the job via System.schedule.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.try/catch(DmlException) so a bad chunk doesn't silently
crash the whole scheduled job.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.
WeeklyCustomerReportScheduler implementing Schedulable,
firing every Monday at 6 AM via CRON_EXPRESSION = '0 0 6 ? * MON'.Closed Won in the last 7 days, using SELECT COUNT().Messaging.SingleEmailMessage, wrapped in try/catch.SELECT COUNT() avoids pulling full record data into memory just to count rows.recipientEmail before building/sending the email avoids a
wasted Messaging.sendEmail call that would simply fail.try/catch(Exception) keeps a transient email-service
issue from surfacing as a hard job failure.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.
DailyOpportunityReminderScheduler implementing Schedulable,
firing daily at 7 AM via CRON_EXPRESSION = '0 0 7 * * ?'.IsClosed = false) and
LastActivityDate is more than NO_ACTIVITY_DAYS (14) days old, or was
never set.Task per qualifying Opportunity, assigned to its
OwnerId, linked via WhatId, with Priority = 'High'.IsClosed = false, the activity-date window) keeps
each run's working set small instead of pulling every Opportunity and filtering in Apex.insert reminders;
call outside the loop keeps this governor-safe at any Opportunity volume.try/catch(DmlException) so a validation rule on one Task
doesn't take down the whole scheduled run.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.
NightlyAccountCleanupScheduler implementing Schedulable,
firing daily at 1:30 AM via CRON_EXPRESSION = '0 30 1 * * ?'.Needs_Review__c = true and both Website
and Phone are now populated.Needs_Review__c back to false for each one.update call outside any loop keeps this governor-safe regardless of
how many Accounts qualify on a given night.try/catch(DmlException) to isolate a bad record from failing
the whole scheduled run.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.
MonthlyInvoiceScheduler implementing Schedulable, firing on
the 1st of each month at 3 AM via CRON_EXPRESSION = '0 0 3 1 * ?'.Monthly_Fee__c.Invoice__c
for the current month/year.Invoice__c per remaining Contract with
Amount__c = Monthly_Fee__c and Status__c = 'Draft'.CRON_EXPRESSION itself — day-of-month
1 plus a fixed hour reads as "first of the month" without extra comments.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.
DailyInventoryRefreshScheduler implementing Schedulable,
firing daily at 4 AM via CRON_EXPRESSION = '0 0 4 * * ?'.IsActive = true).0 or less → Out of Stock; at or below
LOW_STOCK_THRESHOLD (10) → Low Stock; otherwise
In Stock.LOW_STOCK_THRESHOLD) keeps the low-stock cutoff easy to
tune without touching the status logic itself.toUpdate
avoids no-op DML on Products whose stock level hasn't crossed a threshold.update call outside the loop, wrapped in
try/catch(DmlException).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.
MonthlyTerritoryAssignmentScheduler implementing Schedulable,
firing on the 1st of each month at 5 AM via CRON_EXPRESSION = '0 0 5 1 * ?'.Map<String, String> of BillingState
(two-letter code) to territory name (e.g. West/South/Northeast/Central).Territory__c records once to resolve territory name → Id.BillingState; reassign
Territory__c only when the resolved territory Id differs from the current value.Territory__c query (into a Map)
avoids hardcoding Salesforce record Ids, which differ across orgs/sandboxes.Territory__c avoids reassigning
(and re-triggering automation on) Accounts that already have the right territory.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.
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.ErpSyncBatch class it launches, implementing
Database.Batchable<SObject> against unreconciled ERP_Sync_Log__c rows.scheduleDailySync() to register the Schedulable job.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.BATCH_SCOPE_SIZE) for the batch's chunk size keeps it
easy to tune independently of the scheduling cadence.try/catch(DmlException) — launching from a Schedulable doesn't relax those rules.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.
ComplianceAuditScheduler implementing Schedulable, firing
weekly on Sunday at 3 AM via CRON_EXPRESSION = '0 0 3 ? * SUN'.Consent_Date__c is null, or
Data_Processing_Agreement__c is not true.Compliance_Flag__c = true and insert a
Compliance_Violation__c record capturing the specific reason(s).Compliance_Flag__c back to false (no new violation record).Violation_Reason__c)
makes the audit trail actually useful instead of a generic pass/fail flag.true avoids
duplicate violation rows on every single weekly run for the same ongoing issue.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.
MultiRegionReportScheduler implementing Schedulable, firing
every Monday at 6 AM via CRON_EXPRESSION = '0 0 6 ? * MON'.GROUP BY Account.Region__c query.Regional_Manager__c records with a non-null Manager_Email__c.Messaging.sendEmail call.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.Messaging.sendEmail(emails) once respects the single-transaction email limit far
better than sending one at a time.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.
FinancialReconciliationScheduler implementing Schedulable,
firing daily at 2 AM via CRON_EXPRESSION = '0 0 2 * * ?'.Status__c IN ('Draft','Sent')).Payment__c totals per Invoice with one
GROUP BY Invoice__c query.AMOUNT_TOLERANCE (0.01),
mark the Invoice Paid.Reconciliation_Discrepancy__c record (type Overpayment) instead of
silently marking it paid.AMOUNT_TOLERANCE avoids treating floating-point/rounding noise as a
real discrepancy.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