A configurable, metadata-driven Salesforce solution for sending email notifications when record status fields change to specific values. Zero code changes required to add new notifications — just create configuration records.
- Fully Declarative Configuration — Add new email notifications without writing code
- Works with Any Object — Standard or custom objects
- Flexible Recipient Options — Send to Contact, User, Lead lookups, or direct email fields
- CC Support — Add multiple CC recipients per configuration
- Org-Wide Email Address Support — Send from verified org-wide email addresses
- Template Merge Fields — Full support for Salesforce email template merge fields
- Bulk-Safe — Handles large data volumes with proper caching
- Insert & Update Support — Trigger emails on record creation or status changes
| Component | Type | Description |
|---|---|---|
Email_Notification_Config__c |
Custom Object | Stores email notification configurations |
EmailNotificationUtility |
Apex Class | Core utility that processes and sends emails |
EmailNotificationUtilityTest |
Test Class | 100% coverage test class |
# Authenticate to your org
sf org login web -a YourOrgAlias
# Deploy the package
sf project deploy start -d force-app -o YourOrgAlias- Open the project in VS Code with Salesforce Extensions
- Right-click on
force-appfolder - Select SFDX: Deploy Source to Org
Create a Salesforce Email Template (Classic or Lightning) for your notification. Note the Developer Name (not the Label).
Navigate to the Email Notification Configs tab and create a new record:
| Field | Description | Example |
|---|---|---|
| Object API Name | API name of the object to monitor | Proposal__c, Case, Opportunity |
| Status Field API Name | API name of the field to watch for changes | Status__c, StageName, Status |
| Status Value | The value that triggers the email | Approved, Closed Won, Escalated |
| Email Template Developer Name | Developer name of the email template | Proposal_Approved_Notification |
| Recipient Field API Name | Field containing the recipient (lookup or email) | Contact__c, OwnerId, Email__c |
| Active | Enable/disable this configuration | ✓ (checked) |
| CC Email Addresses | Comma-separated CC addresses (optional) | manager@company.com, team@company.com |
| Org Wide Email Address | Display Name of org-wide email address (optional) | Support Team |
| Description | Notes about this configuration (optional) | Notify contact when proposal is approved |
The Recipient_Field_API_Name__c field supports multiple formats:
| Type | Example | Description |
|---|---|---|
| Contact Lookup | Contact__c |
Sends to the related Contact's email |
| User Lookup | OwnerId |
Sends to the User's email |
| Lead Lookup | Lead__c |
Sends to the related Lead's email |
| Email Field | Email__c |
Sends directly to the email address in this field |
| Related Field | Contact__r.Email |
Dot notation for related record fields |
Create or update a trigger on the object you want to monitor:
trigger ProposalTrigger on Proposal__c (after insert, after update) {
if (Trigger.isAfter) {
if (Trigger.isInsert) {
// Check for matching status on insert
EmailNotificationUtility.processStatusChangeEmailsOnInsert(
'Proposal__c',
Trigger.new
);
}
if (Trigger.isUpdate) {
// Check for status changes on update
EmailNotificationUtility.processStatusChangeEmails(
'Proposal__c',
Trigger.oldMap,
Trigger.newMap
);
}
}
}If you use a trigger handler framework:
public class ProposalTriggerHandler extends TriggerHandler {
public override void afterInsert() {
EmailNotificationUtility.processStatusChangeEmailsOnInsert(
'Proposal__c',
Trigger.new
);
}
public override void afterUpdate() {
EmailNotificationUtility.processStatusChangeEmails(
'Proposal__c',
Trigger.oldMap,
Trigger.newMap
);
}
}Configuration:
- Object API Name:
Proposal__c - Status Field API Name:
Status__c - Status Value:
Approved - Email Template Developer Name:
Proposal_Approved - Recipient Field API Name:
Contact__c - Active: ✓
Configuration:
- Object API Name:
Case - Status Field API Name:
Priority - Status Value:
High - Email Template Developer Name:
Case_Escalation_Alert - Recipient Field API Name:
OwnerId - CC Email Addresses:
support-manager@company.com - Org Wide Email Address:
Support Team - Active: ✓
Configuration:
- Object API Name:
Opportunity - Status Field API Name:
StageName - Status Value:
Closed Won - Email Template Developer Name:
Opp_Won_Notification - Recipient Field API Name:
Contact__r.Email - CC Email Addresses:
sales-ops@company.com, finance@company.com - Active: ✓
public static void processStatusChangeEmails(
String objectApiName,
Map<Id, SObject> oldMap,
Map<Id, SObject> newMap
)Use in after update trigger context. Sends emails when a status field changes TO the configured target value.
public static void processStatusChangeEmailsOnInsert(
String objectApiName,
List<SObject> newRecords
)Use in after insert trigger context. Sends emails when a new record is created with the target status value.
public static void processStatusChangeEmailsWithRenderedContent(
String objectApiName,
Map<Id, SObject> oldMap,
Map<Id, SObject> newMap
)Alternative method that renders template content manually. Use this when you need CC addresses to work reliably with all recipient types.
- Use Descriptive Template Names — Makes configurations easier to manage
- Test in Sandbox First — Validate email content and recipients before production
- Monitor Email Limits — Salesforce has daily email limits; monitor usage for high-volume objects
- Use Org-Wide Addresses — Improves deliverability and provides consistent sender identity
- Keep Configurations Active/Inactive — Use the Active checkbox instead of deleting configs
| Issue | Solution |
|---|---|
| Emails not sending | Check that the configuration is Active |
| Template merge fields empty | Ensure the template's Related Entity Type matches your object |
| CC addresses not receiving | Verify email addresses are valid; check spam folders |
| Org-Wide Email not working | Verify the Display Name matches exactly (case-sensitive) |
| No email on insert | Make sure you're calling processStatusChangeEmailsOnInsert |
MIT License — free to use and modify.
- Fork the repository
- Create a feature branch
- Submit a pull request
Questions or issues? Open an issue on GitHub.