You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// ABOUTME: Batch class that sends escalation emails for cases past their next-email time.// ABOUTME: Updates Next_Escalation_Email__c to the next interval after sending.publicclassCaseEscalationEmailBatchimplementsDatabase.Batchable<SObject> {
privatestaticfinalSet<String> RESOLVED_STATUSES=newSet<String>{
'Closed', 'Resolved'
};
public Database.QueryLocatorstart(Database.BatchableContextctx) {
DateTimenow=DateTime.now();
returnDatabase.getQueryLocator([
SELECTId, CaseNumber, Subject, Priority, Status, OwnerId,
Contact.Email, Contact.Name,
Next_Escalation_Email__c, Escalation_Email_Count__cFROMCaseWHERENext_Escalation_Email__c<=:nowANDNext_Escalation_Email__c!=nullANDStatusNOT IN:RESOLVED_STATUSES
]);
}
publicvoidexecute(Database.BatchableContextctx, List<Case> scope) {
List<Messaging.SingleEmailMessage> emails=newList<Messaging.SingleEmailMessage>();
List<Case> toUpdate=newList<Case>();
// Optional: use an email template instead of inline body// EmailTemplate tmpl = [SELECT Id FROM EmailTemplate WHERE DeveloperName = 'Case_Escalation'];for (Casec:scope) {
// Build emailMessaging.SingleEmailMessagemsg=newMessaging.SingleEmailMessage();
// Send to case owner (and/or contact, distribution list, etc.)List<String> recipients=getRecipients(c);
if (recipients.isEmpty()) continue;
msg.setToAddresses(recipients);
msg.setSubject('Escalation: Case '+c.CaseNumber+' — '+c.Subject);
msg.setPlainTextBody(buildEmailBody(c));
msg.setSaveAsActivity(true);
emails.add(msg);
// Update case: set next email time, increment counterIntegerintervalMinutes=CaseEscalationEmailScheduler.getIntervalMinutes(c.Priority);
Decimalcount=c.Escalation_Email_Count__c==null?0:c.Escalation_Email_Count__c;
toUpdate.add(newCase(
Id=c.Id,
Next_Escalation_Email__c=DateTime.now().addMinutes(intervalMinutes),
Last_Escalation_Email__c=DateTime.now(),
Escalation_Email_Count__c=count+1
));
}
if (!emails.isEmpty()) {
Messaging.sendEmail(emails);
}
if (!toUpdate.isEmpty()) {
updatetoUpdate;
}
}
publicvoidfinish(Database.BatchableContextctx) {
// Optional: log completion, send admin summary, etc.
}
privateList<String> getRecipients(Casec) {
List<String> recipients=newList<String>();
// Add contact email if existsif (c.Contact?.Email!=null) {
recipients.add(c.Contact.Email);
}
// TODO: Add case team members, escalation distribution lists,// owner email, etc. based on your org's requirementsreturnrecipients;
}
privateStringbuildEmailBody(Casec) {
return'This is an automated escalation reminder.\n\n'+'Case Number: '+c.CaseNumber+'\n'+'Subject: '+c.Subject+'\n'+'Priority: '+c.Priority+'\n'+'Status: '+c.Status+'\n\n'+'This case requires attention. Escalation emails will continue '+'until the case is resolved.\n\n'+'Escalation email #'+ (c.Escalation_Email_Count__c==null?1:c.Escalation_Email_Count__c+1);
}
}
Deployment Steps
Create custom fields on Case: Next_Escalation_Email__c, Last_Escalation_Email__c, Escalation_Email_Count__c
Deploy trigger + handler — wire CaseEscalationTriggerHandler to after insert and after update
Deploy scheduled + batch classes
Schedule the jobs — run CaseEscalationEmailScheduler.scheduleAll() in Anonymous Apex
Set up email template (optional, cleaner than inline body)
Governor Limit Considerations
Concern
Mitigation
5000 email limit per day
Batch size of 50. Monitor Escalation_Email_Count__c and cap if needed.
Scheduled job limit (100)
Only 4 jobs. Leaves 96 slots free.
SOQL in batch start
Single indexed query on Next_Escalation_Email__c. Fast.
Concurrent batch limit (5)
Single batch per run. Unlikely to overlap at 15-min intervals.
Alternatives Considered
Approach
Why Not
Apex cron at 15-min intervals
Cron expressions only go to hourly. Need 4 jobs. (This is what we do.)
Platform Events + Trigger
Overkill. No real-time need — polling every 15 min is fine.
Flow + Scheduled Path
Flow scheduled paths have 1-hour minimum and limited bulkification.
Entitlement Processes + Milestones
Good for SLA tracking but not for repeated email notifications at custom intervals. Could complement this.
Daisy-chain (job reschedules itself)
Fragile. One failure breaks the chain.
Testing Notes
Test trigger: insert case → assert Next_Escalation_Email__c is ~5 min from now
Test trigger: update case to Closed → assert Next_Escalation_Email__c is null
Test batch: insert case with Next_Escalation_Email__c in the past → run batch → assert email sent + next interval set
Test interval map: assert Sev-1=15, Sev-2=30, Sev-3=720
Test edge: case with no contact email → no email sent, no error