Salesforce Apex triggers provide a powerful way to automate business processes, enforce data validation rules, and maintain data consistency. By leveraging Apex triggers, businesses can streamline their operations, reduce manual effort, and ensure their Salesforce data is accurate and up-to-date.
Apex triggers are used to perform custom actions before or after events to Salesforce records, such as insertions, updates, or deletions. These triggers can handle complex logic, integrate with other systems, and provide real-time responses to changes in data. In this blog, we will explore how real-life business problems can be solved using Salesforce Apex triggers, showcasing the versatility and power of this method.
Table of Contents
Solving Real-Life Scenarios with Salesforce Apex Triggers Example
Ensuring Data Accuracy with Apex Triggers
One of the primary uses of Apex triggers is to enforce data accuracy and validation. For example, companies that rely heavily on phone communication with customers must ensure that all new account records have valid phone numbers before they are inserted into the system. This can be accomplished with a simple Apex trigger that checks the phone number format and prevents the insertion of invalid records. In case you are preparing for an interview check our guide on Salesforce Apex Trigger Scenario Interview Questions.
Automating Business Processes
Apex triggers can also automate various business processes, reducing manual effort and increasing efficiency. For instance, subscription-based businesses can automatically create renewal opportunities when existing opportunities are marked as “Closed Won.” This ensures that renewal processes are initiated without delay, helping maintain continuous service for customers.
Enhancing Customer Communication
Improving customer communication is another area where Apex triggers shine. Businesses can automatically send email notifications to sales managers when high-value deals are created or update contact information across related records to maintain consistency. These automated actions ensure timely and accurate communication with customers, enhancing the overall customer experience.
Maintaining Data Integrity
Preventing the deletion of critical records, such as active subscriptions or key contacts, is vital for maintaining data integrity. Apex triggers can enforce rules that prevent users from deleting records that are essential for business operations, ensuring that crucial data is not lost or compromised.
Synchronizing Related Records
In complex data models, it is often necessary to synchronize information between related records. Apex triggers can automatically update related records when changes are made to parent records, ensuring that all relevant information is kept consistent and up-to-date. This synchronization is crucial for maintaining a single source of truth across the organization.
1. Validating Phone Numbers
Ensure that all new Account records have a valid phone number before they are inserted into the system.
trigger ValidatePhoneNumber on Account (before insert) {
for (Account acc : Trigger.new) {
if (String.isEmpty(acc.Phone) || !Pattern.matches('^\\d{10}$', acc.Phone)) {
acc.addError('Please enter a valid 10-digit phone number.');
}
}
}
2. Auto-Close Cases After 30 Days
Automatically close cases that have been open for more than 30 days.
trigger AutoCloseOldCases on Case (after update) {
List<Case> casesToClose = new List<Case>();
for (Case c : Trigger.new) {
if (c.Status != 'Closed' && Date.today().daysBetween(c.CreatedDate) > 30) {
c.Status = 'Closed';
casesToClose.add(c);
}
}
if (!casesToClose.isEmpty()) {
update casesToClose;
}
}
3. Sync Contact Address with Account
Sync contact’s mailing address with their parent account’s billing address.
trigger SyncContactAddress on Contact (before insert, before update) {
for (Contact con : Trigger.new) {
if (con.AccountId != null) {
Account acc = [SELECT BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account WHERE Id = :con.AccountId];
con.MailingStreet = acc.BillingStreet;
con.MailingCity = acc.BillingCity;
con.MailingState = acc.BillingState;
con.MailingPostalCode = acc.BillingPostalCode;
con.MailingCountry = acc.BillingCountry;
}
}
}
4. Prevent Deletion of Active Subscriptions
Prevent deletion of subscriptions that are currently active.
trigger PreventActiveSubscriptionDeletion on Subscription__c (before delete) {
for (Subscription__c sub : Trigger.old) {
if (sub.Status__c == 'Active') {
sub.addError('You cannot delete an active subscription.');
}
}
}
5. Update Opportunity Close Date
Automatically update the close date of an opportunity to today when the stage is set to “Closed Won.”
trigger UpdateOpportunityCloseDate on Opportunity (before update) {
for (Opportunity opp : Trigger.new) {
if (opp.StageName == 'Closed Won') {
opp.CloseDate = Date.today();
}
}
}
6. Create Follow-Up Tasks for New Leads
Create a follow-up task for every new lead created in the system.
trigger CreateFollowUpTask on Lead (after insert) {
List<Task> tasks = new List<Task>();
for (Lead lead : Trigger.new) {
Task t = new Task(
Subject = 'Follow up with new lead',
WhatId = lead.Id,
OwnerId = lead.OwnerId,
ActivityDate = Date.today().addDays(3),
Priority = 'High'
);
tasks.add(t);
}
if (!tasks.isEmpty()) {
insert tasks;
}
}
7. Calculate Discount on Opportunity
Automatically calculate a 10% discount on the amount for opportunities with a specific record type.
trigger CalculateDiscount on Opportunity (before insert, before update) {
for (Opportunity opp : Trigger.new) {
if (opp.RecordTypeId == '0121N00000XXXXX') { // Replace with the specific record type ID
opp.Discount__c = opp.Amount * 0.1;
opp.Total_Amount__c = opp.Amount - opp.Discount__c;
}
}
}
8. Prevent Duplicate Contacts
Prevent creating duplicate contacts with the same email address.
trigger PreventDuplicateContacts on Contact (before insert, before update) {
Set<String> emails = new Set<String>();
for (Contact con : Trigger.new) {
emails.add(con.Email);
}
List<Contact> existingContacts = [SELECT Email FROM Contact WHERE Email IN :emails];
for (Contact con : existingContacts) {
Trigger.newMap.get(con.Id).addError('A contact with this email already exists.');
}
}
9. Auto-Assign Cases Based on Product
Automatically assign cases to specific teams based on the product.
trigger AutoAssignCasesByProduct on Case (before insert) {
Map<String, Id> productTeamMap = new Map<String, Id>{
'Product A' => '0051N00000XXXXX', // User ID for Product A team
'Product B' => '0051N00000YYYYY' // User ID for Product B team
};
for (Case c : Trigger.new) {
if (productTeamMap.containsKey(c.Product__c)) {
c.OwnerId = productTeamMap.get(c.Product__c);
}
}
}
10. Update The Contact’s Last Interaction Date
Update the contact’s last interaction date when a task is completed.
trigger UpdateContactLastInteraction on Task (after update) {
List<Contact> contactsToUpdate = new List<Contact>();
for (Task t : Trigger.new) {
if (t.Status == 'Completed' && t.WhatId != null && t.WhatId.getSObjectType() == Contact.SObjectType) {
Contact con = [SELECT Id, Last_Interaction_Date__c FROM Contact WHERE Id = :t.WhatId];
con.Last_Interaction_Date__c = Date.today();
contactsToUpdate.add(con);
}
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
11. Limit Opportunity Amount Based on User Role
Limit the maximum opportunity amount based on the user’s role.
trigger LimitOpportunityAmount on Opportunity (before insert, before update) {
Map<Id, User> userMap = new Map<Id, User>([SELECT Id, UserRole.Name FROM User WHERE Id IN :Trigger.newMap.keySet()]);
for (Opportunity opp : Trigger.new) {
User u = userMap.get(opp.OwnerId);
if (u.UserRole.Name == 'Sales Rep' && opp.Amount > 50000) {
opp.addError('Sales Reps cannot create opportunities with an amount greater than $50,000.');
}
}
}
12. Update Account Type Based on Annual Revenue
Automatically update the account type based on the annual revenue.
trigger UpdateAccountType on Account (before insert, before update) {
for (Account acc : Trigger.new) {
if (acc.AnnualRevenue != null) {
if (acc.AnnualRevenue > 1000000) {
acc.Type = 'Enterprise';
} else if (acc.AnnualRevenue > 500000) {
acc.Type = 'Mid-Market';
} else {
acc.Type = 'Small Business';
}
}
}
}
13. Prevent Deletion of Records with Attachments
Prevent deletion of records that have attachments.
trigger PreventDeletionWithAttachments on SObject (before delete) {
for (SObject s : Trigger.old) {
Integer attachmentCount = [SELECT COUNT() FROM Attachment WHERE ParentId = :s.Id];
if (attachmentCount > 0) {
s.addError('You cannot delete a record with attachments.');
}
}
}
14. Create a Case for Expiring Contracts
Create a case for contracts that are expiring within 30 days.
trigger CreateCaseForExpiringContracts on Contract (after update) {
List<Case> cases = new List<Case>();
for (Contract c : Trigger.new) {
if (c.EndDate != null && c.EndDate <= Date.today().addDays(30)) {
Case newCase = new Case(
Subject = 'Contract Expiring Soon',
AccountId = c.AccountId,
Description = 'The contract ' + c.ContractNumber + ' is expiring on ' + c.EndDate,
Status = 'New'
);
cases.add(newCase);
}
}
if (!cases.isEmpty()) {
insert cases;
}
}
15. Auto-Assign Account Managers Based on Industry and Revenue
Assign account managers to accounts based on the industry and annual revenue using custom fields.
trigger AutoAssignAccountManagers on Account (before insert, before update) {
Map<String, Id> industryManagerMap = new Map<String, Id>{
'Technology' => '0051N00000XXXXX', // Account Manager for Technology
'Healthcare' => '0051N00000YYYYY' // Account Manager for Healthcare
};
for (Account acc : Trigger.new) {
if (acc.Industry != null && acc.AnnualRevenue != null) {
if (industryManagerMap.containsKey(acc.Industry)) {
if (acc.AnnualRevenue > 1000000) {
acc.Account_Manager__c = industryManagerMap.get(acc.Industry); // Custom field for Account Manager
}
}
}
}
}
16. Update Opportunity Stage and Send Notification Based on Custom Field
Update the stage of an opportunity and send a notification to the account team when a custom field indicates a significant milestone.
trigger UpdateStageAndNotify on Opportunity (before update) {
List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
for (Opportunity opp : Trigger.new) {
if (opp.Milestone_Achieved__c && !Trigger.oldMap.get(opp.Id).Milestone_Achieved__c) { // Custom checkbox field
opp.StageName = 'Value Proposition';
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[] {opp.Owner.Email});
email.setSubject('Milestone Achieved for Opportunity ' + opp.Name);
email.setPlainTextBody('The opportunity ' + opp.Name + ' has reached a significant milestone.');
emails.add(email);
}
}
if (!emails.isEmpty()) {
Messaging.sendEmail(emails);
}
}
17. Calculate and Update the Custom Field on the Invoice
Automatically calculate the total tax for an invoice based on custom fields and update the total amount.
trigger CalculateTotalTax on Invoice__c (before insert, before update) {
for (Invoice__c inv : Trigger.new) {
Decimal totalTax = 0;
for (Invoice_Line_Item__c lineItem : [SELECT Tax__c FROM Invoice_Line_Item__c WHERE Invoice__c = :inv.Id]) {
totalTax += lineItem.Tax__c;
}
inv.Total_Tax__c = totalTax; // Custom field for total tax
inv.Total_Amount__c = inv.Subtotal__c + totalTax; // Custom fields for subtotal and total amount
}
}
18. Synchronize Custom Fields Between Related Records
Synchronize a custom field value from an account to all related opportunities whenever the field is updated.
trigger SyncCustomFieldToOpportunities on Account (after update) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : Trigger.new) {
if (acc.Custom_Field__c != Trigger.oldMap.get(acc.Id).Custom_Field__c) { // Custom field on Account
accountIds.add(acc.Id);
}
}
List<Opportunity> oppsToUpdate = [SELECT Id, Custom_Field__c, AccountId FROM Opportunity WHERE AccountId IN :accountIds];
for (Opportunity opp : oppsToUpdate) {
opp.Custom_Field__c = Trigger.newMap.get(opp.AccountId).Custom_Field__c; // Custom field on Opportunity
}
if (!oppsToUpdate.isEmpty()) {
update oppsToUpdate;
}
}
19. Prevent Stage Progression Based on Custom Field Value
Prevent opportunities from moving to the next stage if a custom field is not completed.
trigger PreventStageProgression on Opportunity (before update) {
for (Opportunity opp : Trigger.new) {
if (opp.StageName == 'Negotiation/Review' && Trigger.oldMap.get(opp.Id).StageName != 'Negotiation/Review') {
if (String.isEmpty(opp.Custom_Approval_Status__c)) { // Custom field for approval status
opp.addError('You cannot move to the Negotiation/Review stage without an approval status.');
}
}
}
}
20. Auto-Calculate Commission on Opportunity Close
Automatically calculate and update the commission amount for a sales rep when an opportunity is closed won.
trigger CalculateCommission on Opportunity (before update) {
for (Opportunity opp : Trigger.new) {
if (opp.StageName == 'Closed Won' && Trigger.oldMap.get(opp.Id).StageName != 'Closed Won') {
Decimal commissionRate = opp.Commission_Rate__c != null ? opp.Commission_Rate__c : 0.05; // Default commission rate
opp.Commission_Amount__c = opp.Amount * commissionRate; // Custom fields for commission rate and amount
}
}
}
21. Send Reminder for Custom Date Field
Send a reminder email 7 days before a custom date field on a contract.
trigger SendReminderForCustomDate on Contract (after insert, after update) {
List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
for (Contract c : Trigger.new) {
if (c.Custom_Renewal_Date__c != null && Date.today().addDays(7) == c.Custom_Renewal_Date__c) { // Custom date field
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[] {c.Owner.Email});
email.setSubject('Contract Renewal Reminder');
email.setPlainTextBody('The contract ' + c.ContractNumber + ' is up for renewal on ' + c.Custom_Renewal_Date__c + '.');
emails.add(email);
}
}
if (!emails.isEmpty()) {
Messaging.sendEmail(emails);
}
}
22. Update Related Contacts Based on Custom Field Change
Update all related contacts when a custom field on the account is changed.
trigger UpdateRelatedContacts on Account (after update) {
Set<Id> accountIds = new Set<Id>();
for (Account acc : Trigger.new) {
if (acc.Custom_Status__c != Trigger.oldMap.get(acc.Id).Custom_Status__c) { // Custom field on Account
accountIds.add(acc.Id);
}
}
List<Contact> contactsToUpdate = [SELECT Id, Custom_Status__c, AccountId FROM Contact WHERE AccountId IN :accountIds];
for (Contact con : contactsToUpdate) {
con.Custom_Status__c = Trigger.newMap.get(con.AccountId).Custom_Status__c; // Custom field on Contact
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
23. Validate Custom Fields Before Insertion
Ensure that certain custom fields are populated before inserting a new record.
trigger ValidateCustomFields on Custom_Object__c (before insert) {
for (Custom_Object__c obj : Trigger.new) {
if (String.isEmpty(obj.Custom_Field1__c) || String.isEmpty(obj.Custom_Field2__c)) { // Custom fields
obj.addError('Custom Field 1 and Custom Field 2 must be populated before inserting the record.');
}
}
}
24. Auto-Update Custom Score Field
Automatically update a custom score field based on related records.
trigger UpdateCustomScore on Parent_Object__c (after insert, after update, after delete) {
Set<Id> parentIds = new Set<Id>();
if (Trigger.isInsert || Trigger.isUpdate) {
for (Parent_Object__c parent : Trigger.new) {
parentIds.add(parent.Id);
}
} else if (Trigger.isDelete) {
for (Parent_Object__c parent : Trigger.old) {
parentIds.add(parent.Id);
}
}
List<Parent_Object__c> parentsToUpdate = [SELECT Id, Custom_Score__c, (SELECT Score__c FROM Related_Objects__r) FROM Parent_Object__c WHERE Id IN :parentIds];
for (Parent_Object__c parent : parentsToUpdate) {
Decimal totalScore = 0;
for (Related_Object__c related : parent.Related_Objects__r) {
totalScore += related.Score__c;
}
parent.Custom_Score__c = totalScore;
}
if (!parentsToUpdate.isEmpty()) {
update parentsToUpdate;
}
}
25. Prevent Opportunity Deletion if Related Custom Records Exist
Prevent the deletion of an opportunity if there are related custom records.
trigger PreventOpportunityDeletion on Opportunity (before delete) {
Set<Id> oppIds = new Set<Id>();
for (Opportunity opp : Trigger.old) {
oppIds.add(opp.Id);
}
List<Custom_Record__c> relatedRecords = [SELECT Id FROM Custom_Record__c WHERE Opportunity__c IN :oppIds];
if (!relatedRecords.isEmpty()) {
for (Opportunity opp : Trigger.old) {
opp.addError('You cannot delete an opportunity that has related custom records.');
}
}
}
26. Create Custom Notifications Based on Field Changes
Create custom notifications when certain fields are changed.
trigger CreateCustomNotifications on Custom_Object__c (after update) {
List<Custom_Notification__c> notifications = new List<Custom_Notification__c>();
for (Custom_Object__c obj : Trigger.new) {
if (obj.Status__c != Trigger.oldMap.get(obj.Id).Status__c) { // Custom field
Custom_Notification__c notification = new Custom_Notification__c(
Name = 'Status Changed',
Related_Record__c = obj.Id,
Message__c = 'The status has changed to ' + obj.Status__c
);
notifications.add(notification);
}
}
if (!notifications.isEmpty()) {
insert notifications;
}
}
27. Auto-Populate Custom Lookup Fields
Automatically populate custom lookup fields based on other field values.
trigger AutoPopulateCustomLookup on Custom_Object__c (before insert, before update) {
Map<String, Id> lookupMap = new Map<String, Id>{
'Value1' => '0011N00000XXXXX', // Lookup value for Value1
'Value2' => '0011N00000YYYYY' // Lookup value for Value2
};
for (Custom_Object__c obj : Trigger.new) {
if (lookupMap.containsKey(obj.Custom_Field__c)) { // Custom field
obj.Custom_Lookup_Field__c = lookupMap.get(obj.Custom_Field__c); // Custom lookup field
}
}
}
28. Prevent Update if Custom Conditions are Met
Prevent updates to records if certain custom conditions are met.
trigger PreventUpdateIfConditionsMet on Custom_Object__c (before update) {
for (Custom_Object__c obj : Trigger.new) {
if (obj.Status__c == 'Locked' && obj.Status__c != Trigger.oldMap.get(obj.Id).Status__c) { // Custom field
obj.addError('You cannot update the record when the status is Locked.');
}
}
}
29. Auto-Update Parent Records Based on Child Records
Update parent records when certain custom fields on child records change.
trigger UpdateParentRecords on Child_Object__c (after update) {
Set<Id> parentIds = new Set<Id>();
for (Child_Object__c child : Trigger.new) {
if (child.Custom_Field__c != Trigger.oldMap.get(child.Id).Custom_Field__c) { // Custom field
parentIds.add(child.Parent_Record__c);
}
}
List<Parent_Object__c> parentsToUpdate = [SELECT Id, Aggregated_Custom_Field__c FROM Parent_Object__c WHERE Id IN :parentIds];
for (Parent_Object__c parent : parentsToUpdate) {
Decimal total = 0;
for (Child_Object__c child : [SELECT Custom_Field__c FROM Child_Object__c WHERE Parent_Record__c = :parent.Id]) {
total += child.Custom_Field__c;
}
parent.Aggregated_Custom_Field__c = total; // Custom field
}
if (!parentsToUpdate.isEmpty()) {
update parentsToUpdate;
}
}
30. Log Changes to Critical Fields
Log changes to critical fields for auditing purposes.
trigger LogCriticalFieldChanges on Custom_Object__c (after update) {
List<Change_Log__c> changeLogs = new List<Change_Log__c>();
for (Custom_Object__c obj : Trigger.new) {
if (obj.Critical_Field__c != Trigger.oldMap.get(obj.Id).Critical_Field__c) { // Custom field
Change_Log__c log = new Change_Log__c(
Related_Record__c = obj.Id,
Field_Changed__c = 'Critical_Field__c',
Old_Value__c = String.valueOf(Trigger.oldMap.get(obj.Id).Critical_Field__c),
New_Value__c = String.valueOf(obj.Critical_Field__c),
Changed_By__c = UserInfo.getUserId()
);
changeLogs.add(log);
}
}
if (!changeLogs.isEmpty()) {
insert changeLogs;
}
}
Conclusion
Salesforce Apex triggers are a powerful tool in the database for automating business processes, ensuring data accuracy, and maintaining data integrity. By leveraging the capabilities of Apex triggers, businesses can streamline their operations, reduce manual effort, and enhance the overall quality of their Salesforce data. Whether it’s validating phone numbers, automating renewal opportunities, sending notifications, or preventing critical record deletions, Apex triggers provide a versatile and robust solution to many real-life business challenges.
Implementing Apex triggers requires a good understanding of Salesforce’s data model and business processes. However, the benefits they bring in terms of automation, efficiency, and data integrity make them an invaluable procedure in a database for any organization looking to optimize its Salesforce environment.