Welcome to Salesforce Hours!
If you’re looking to sharpen your Salesforce development skills with real-world examples and practical tutorials, you’re in the right place.
In this Part 2 of our “Salesforce Apex Trigger Scenarios Example “, we’re diving into some of the most common and sometimes tricky use cases you’ll come across when working with Apex triggers. These examples are built around standard Salesforce objects and designed to mirror the actual interview question that are asked in most of the Salesforce interview .
Before going forward , I would recommend you to first have a basic knowledge about basic of Triggers and Trigger Syntax .
Scenario 1: Update Account Rating When a High-Value Opportunity is Added
Requirement: When a new Opportunity with an Amount over 100,000 is inserted, update the related Account’s Rating field to “Hot”.
Trigger:
trigger OpportunityTrigger on Opportunity (after insert) {
OpportunityTriggerHandler.updateAccountRatingForHighValueOpps(Trigger.new);
}
Handler:
public class OpportunityTriggerHandler {
public static void updateAccountRatingForHighValueOpps(List<Opportunity> newOpps) {
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : newOpps) {
if (opp.Amount != null && opp.Amount > 100000 && opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : [SELECT Id, Rating FROM Account WHERE Id IN :accountIds]) {
acc.Rating = 'Hot';
accountsToUpdate.add(acc);
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate;
}
}
}
Scenario 2: Update Account Phone When Contact is Marked as Primary
Requirement: When a Contact’s Primary__c
checkbox is checked, copy that Contact’s phone number to the Account’s main phone field.
Trigger:
trigger ContactTrigger on Contact (after insert, after update) {
ContactTriggerHandler.syncPrimaryContactPhoneToAccount(Trigger.new);
}
Handler:
public class ContactTriggerHandler {
public static void syncPrimaryContactPhoneToAccount(List<Contact> contacts) {
Map<Id, String> accountPhoneMap = new Map<Id, String>();
for (Contact con : contacts) {
if (con.Primary__c == true && con.AccountId != null && con.Phone != null) {
accountPhoneMap.put(con.AccountId, con.Phone);
}
}
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : [SELECT Id, Phone FROM Account WHERE Id IN :accountPhoneMap.keySet()]) {
acc.Phone = accountPhoneMap.get(acc.Id);
accountsToUpdate.add(acc);
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate;
}
}
}
Scenario 3: Reflect Open Case Count on Related Opportunity
Requirement: Show the number of open Cases on the related Opportunity using a custom field Open_Case_Count__c
.
Trigger:
trigger CaseTrigger on Case (after insert, after update, after delete, after undelete) {
CaseTriggerHandler.updateOpportunityCaseCount(Trigger.newMap, Trigger.oldMap, Trigger.isDelete);
}
Handler:
public class CaseTriggerHandler {
public static void updateOpportunityCaseCount(Map<Id, Case> newMap, Map<Id, Case> oldMap, Boolean isDelete) {
Set<Id> oppIds = new Set<Id>();
if (isDelete) {
for (Case c : oldMap.values()) {
if (c.OpportunityId != null) oppIds.add(c.OpportunityId);
}
} else {
for (Case c : newMap.values()) {
if (c.OpportunityId != null) oppIds.add(c.OpportunityId);
}
}
Map<Id, Integer> countMap = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT OpportunityId, COUNT(Id) count
FROM Case
WHERE OpportunityId IN :oppIds AND Status != 'Closed'
GROUP BY OpportunityId
]) {
countMap.put((Id)ar.get('OpportunityId'), (Integer)ar.get('count'));
}
List<Opportunity> oppsToUpdate = new List<Opportunity>();
for (Id oppId : oppIds) {
Integer count = countMap.containsKey(oppId) ? countMap.get(oppId) : 0;
oppsToUpdate.add(new Opportunity(Id = oppId, Open_Case_Count__c = count));
}
if (!oppsToUpdate.isEmpty()) {
update oppsToUpdate;
}
}
}
Scenario 4: Copy Contact’s Email to Lead When Created from Web Form
Requirement: When a Lead is created from a web-to-lead form, match the name with a Contact and copy the Contact’s Email to the custom field Matched_Contact_Email__c
.
Trigger:
trigger LeadTrigger on Lead (before insert) {
LeadTriggerHandler.matchAndSetContactEmail(Trigger.new);
}
Handler:
public class LeadTriggerHandler {
public static void matchAndSetContactEmail(List<Lead> leads) {
Map<String, Lead> nameToLead = new Map<String, Lead>();
for (Lead l : leads) {
if (l.FirstName != null && l.LastName != null) {
String key = l.FirstName + ' ' + l.LastName;
nameToLead.put(key, l);
}
}
List<Contact> matchingContacts = [SELECT FirstName, LastName, Email FROM Contact WHERE FirstName != null AND LastName != null];
for (Contact c : matchingContacts) {
String key = c.FirstName + ' ' + c.LastName;
if (nameToLead.containsKey(key)) {
nameToLead.get(key).Matched_Contact_Email__c = c.Email;
}
}
}
}
Scenario 5: Update Contact Description When Account Industry Changes
Requirement: Append the new Industry value to all related Contacts’ Descriptions when an Account’s Industry changes.
Trigger:
trigger AccountTrigger on Account (after update) {
AccountTriggerHandler.appendIndustryToContactDescription(Trigger.new, Trigger.oldMap);
}
Handler:
public class AccountTriggerHandler {
public static void appendIndustryToContactDescription(List<Account> accounts, Map<Id, Account> oldMap) {
Set<Id> changedAccountIds = new Set<Id>();
Map<Id, Account> accountsMap = new Map<Id, Account>();
for (Account acc : accounts) {
Account oldAcc = oldMap.get(acc.Id);
if (acc.Industry != oldAcc.Industry && acc.Industry != null) {
changedAccountIds.add(acc.Id);
accountsMap.put(acc.Id, acc);
}
}
if (changedAccountIds.isEmpty()) return;
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact c : [
SELECT Id, Description, AccountId FROM Contact WHERE AccountId IN :changedAccountIds
]) {
String updatedDesc = (c.Description != null ? c.Description + ' | ' : '') + 'Industry changed to: ' + accountsMap.get(c.AccountId).Industry;
c.Description = updatedDesc;
contactsToUpdate.add(c);
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
}
}
Scenario 6: Recursive Trigger Prevention for Custom Object Updates
Requirement: When a custom object Project__c
is updated, it also updates a related Client__c
record. But this, in turn, triggers an update back to Project__c
, causing recursion. We need to prevent that.
Trigger:
trigger ProjectTrigger on Project__c (after update) {
if (!RecursiveHelper.hasRun()) {
RecursiveHelper.markRun();
ProjectTriggerHandler.syncClientFromProject(Trigger.new);
}
}
Recursive Helper:
public class RecursiveHelper {
private static Boolean hasRun = false;
public static Boolean hasRun() {
return hasRun;
}
public static void markRun() {
hasRun = true;
}
}
Scenario 7: Roll-Up Summary on Lookup (Not Master-Detail)
Requirement: Count the number of Invoice__c
records related to a Customer__c
and store the count in Invoice_Count__c
on the Customer.
Trigger:
trigger InvoiceTrigger on Invoice__c (after insert, after delete, after undelete) {
InvoiceTriggerHandler.updateCustomerInvoiceCount(Trigger.newMap, Trigger.oldMap, Trigger.isDelete);
}
Handler:
public class InvoiceTriggerHandler {
public static void updateCustomerInvoiceCount(Map<Id, Invoice__c> newMap, Map<Id, Invoice__c> oldMap, Boolean isDelete) {
Set<Id> customerIds = new Set<Id>();
for (Invoice__c inv : isDelete ? oldMap.values() : newMap.values()) {
if (inv.Customer__c != null) {
customerIds.add(inv.Customer__c);
}
}
Map<Id, Integer> countMap = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT Customer__c, COUNT(Id) cnt FROM Invoice__c
WHERE Customer__c IN :customerIds
GROUP BY Customer__c
]) {
countMap.put((Id)ar.get('Customer__c'), (Integer)ar.get('cnt'));
}
List<Customer__c> updates = new List<Customer__c>();
for (Id custId : customerIds) {
Integer count = countMap.get(custId);
updates.add(new Customer__c(Id = custId, Invoice_Count__c = count));
}
update updates;
}
}
Scenario 8: Bypass Validation Rule for Automated Integration Users
Requirement: A validation rule prevents updating Account
fields when Industry
is blank. However, integration users should bypass this.
Solution: Use a custom checkbox field Bypass_Validations__c
and set it in a before update
trigger for integration users.
Trigger:
trigger AccountTrigger on Account (before update) {
if (UserInfo.getProfileId() == '00eIntegrationProfileId') {
for (Account acc : Trigger.new) {
acc.Bypass_Validations__c = true;
}
}
}
Scenario 9: Prevent Deletion of Active Projects
Requirement: Don’t allow deletion of Project__c
records if their status is “Active”.
Trigger:
trigger ProjectTrigger on Project__c (before delete) {
for (Project__c proj : Trigger.old) {
if (proj.Status__c == 'Active') {
proj.addError('Cannot delete active projects.');
}
}
}
Scenario 10: Auto-Assign Case to Default Queue on Creation
Requirement: All new Case
records must be assigned to a default queue unless otherwise specified.
Trigger:
trigger CaseTrigger on Case (before insert) {
Id queueId = [SELECT Id FROM Group WHERE Type = 'Queue' AND Name = 'Default Case Queue' LIMIT 1].Id;
for (Case c : Trigger.new) {
if (c.OwnerId == null) {
c.OwnerId = queueId;
}
}
}
Scenario 11: Calculate Total Revenue on Account from Opportunities
Requirement: Whenever Opportunities are inserted or updated, update the total revenue on the related Account using a custom field Total_Revenue__c
.
Trigger:
trigger OpportunityTrigger on Opportunity (after insert, after update, after delete, after undelete) {
OpportunityHandler.updateTotalRevenue(Trigger.newMap, Trigger.oldMap, Trigger.isDelete);
}
Handler:
public class OpportunityHandler {
public static void updateTotalRevenue(Map<Id, Opportunity> newMap, Map<Id, Opportunity> oldMap, Boolean isDelete) {
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : isDelete ? oldMap.values() : newMap.values()) {
if (opp.AccountId != null) {
accountIds.add(opp.AccountId);
}
}
Map<Id, Decimal> totalRevenueMap = new Map<Id, Decimal>();
for (AggregateResult ar : [
SELECT AccountId, SUM(Amount) total FROM Opportunity
WHERE AccountId IN :accountIds
GROUP BY AccountId
]) {
totalRevenueMap.put((Id)ar.get('AccountId'), (Decimal)ar.get('total'));
}
List<Account> accToUpdate = new List<Account>();
for (Id accId : accountIds) {
accToUpdate.add(new Account(Id = accId, Total_Revenue__c = totalRevenueMap.get(accId)));
}
update accToUpdate;
}
}
Scenario 12: Flag Account if It Has More Than 3 Open Cases
Requirement: When the number of open Cases (Status != ‘Closed’) for an Account exceeds 3, mark the Account’s custom checkbox High_Case_Risk__c
as true to alert management.
Trigger:
trigger CaseTrigger on Case (after insert, after update, after delete, after undelete) {
AccountTriggerHandler.flagHighCaseRisk(Trigger.newMap, Trigger.oldMap, Trigger.isDelete);
}
Handler Class:
public class AccountTriggerHandler {
public static void flagHighCaseRisk(Map<Id, Case> newMap, Map<Id, Case> oldMap, Boolean isDelete) {
Set<Id> accountIds = new Set<Id>();
// Collect Account IDs from new or deleted cases
if (isDelete) {
for (Case c : oldMap.values()) if (c.AccountId != null) accountIds.add(c.AccountId);
} else {
for (Case c : newMap.values()) if (c.AccountId != null) accountIds.add(c.AccountId);
}
// Aggregate open case count per Account
Map<Id, Integer> openCaseCount = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT AccountId, COUNT(Id) cnt
FROM Case
WHERE AccountId IN :accountIds AND Status != 'Closed'
GROUP BY AccountId
]) {
openCaseCount.put((Id)ar.get('AccountId'), (Integer)ar.get('cnt'));
}
// Prepare Account updates
List<Account> acctsToUpdate = new List<Account>();
for (Id accId : accountIds) {
Boolean isHighRisk = openCaseCount.get(accId) != null && openCaseCount.get(accId) > 3;
acctsToUpdate.add(new Account(Id = accId, High_Case_Risk__c = isHighRisk));
}
if (!acctsToUpdate.isEmpty()) update acctsToUpdate;
}
}
Scenario 13: Update Contact Last Activity Date Based on Completed Tasks
Requirement: Whenever a Task is completed (Status = ‘Completed’), update the related Contact’s custom field Last_Activity_Date__c
to the Task’s ActivityDate
for accurate activity tracking.
Trigger:
trigger TaskTrigger on Task (after update) {
ContactTriggerHandler.updateLastActivityDate(Trigger.new, Trigger.oldMap);
}
Handler Class:
public class ContactTriggerHandler {
public static void updateLastActivityDate(List<Task> newTasks, Map<Id, Task> oldMap) {
Set<Id> contactIds = new Set<Id>();
// Identify tasks newly completed
for (Task t : newTasks) {
Task oldT = oldMap.get(t.Id);
if (t.Status == 'Completed' && oldT.Status != 'Completed' && t.WhoId != null && t.WhoId.getSObjectType() == Contact.SObjectType) {
contactIds.add((Id)t.WhoId);
}
}
// Update Contacts
List<Contact> contactsToUpdate = new List<Contact>();
for (Task t : [SELECT Id, WhoId, ActivityDate FROM Task WHERE WhoId IN :contactIds AND Status = 'Completed' ORDER BY ActivityDate DESC LIMIT 1]) {
contactsToUpdate.add(new Contact(Id = (Id)t.WhoId, Last_Activity_Date__c = t.ActivityDate));
}
if (!contactsToUpdate.isEmpty()) update contactsToUpdate;
}
}
Scenario 14: Auto-Populate Primary Contact on Opportunity Based on Account
Requirement: On Opportunity creation, if the parent Account has a Contact marked Primary__c = true
, automatically set that Contact as the Opportunity’s Primary_Contact__c
lookup.
Trigger:
trigger OpportunityTrigger on Opportunity (before insert) {
OpportunityTriggerHandler.setPrimaryContact(Trigger.new);
}
Handler Class:
public class OpportunityTriggerHandler {
public static void setPrimaryContact(List<Opportunity> newOpps) {
Set<Id> accIds = new Set<Id>();
for (Opportunity opp : newOpps) if (opp.AccountId != null) accIds.add(opp.AccountId);
// Map Account to its primary Contact
Map<Id, Id> primaryMap = new Map<Id, Id>();
for (Contact c : [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accIds AND Primary__c = true LIMIT 1]) {
primaryMap.put(c.AccountId, c.Id);
}
// Assign lookup on Opportunities
for (Opportunity opp : newOpps) {
if (opp.AccountId != null && primaryMap.containsKey(opp.AccountId)) {
opp.Primary_Contact__c = primaryMap.get(opp.AccountId);
}
}
}
}
Scenario 15: Prevent Contact Deletion If Related Opportunities Exist
Requirement: Do not allow deletion of a Contact if it is referenced on any OpportunityContactRole records, ensuring historical deal data remains intact.
Trigger:
trigger ContactTrigger on Contact (before delete) {
ContactTriggerHandler.preventDeletionIfHasOCR(Trigger.old);
}
Handler Class:
public class ContactTriggerHandler {
public static void preventDeletionIfHasOCR(List<Contact> oldContacts) {
Set<Id> conIds = new Set<Id>();
for (Contact c : oldContacts) conIds.add(c.Id);
// Count Roles per Contact
for (AggregateResult ar : [SELECT ContactId, COUNT(Id) cnt FROM OpportunityContactRole WHERE ContactId IN :conIds GROUP BY ContactId]) {
if ((Integer)ar.get('cnt') > 0) {
// Add error to block deletion
for (Contact c : oldContacts) {
if (c.Id == (Id)ar.get('ContactId')) {
c.addError('Cannot delete a Contact assigned to active Opportunities.');
}
}
}
}
}
}
Scenario 16: Limit Number of Open Opportunities Per Account to 5
Requirement: Prevent creating more than 5 open (StageName != ‘Closed Won’ and != ‘Closed Lost’) Opportunities under a single Account, enforcing sales capacity limits.
Trigger:
trigger OpportunityTrigger on Opportunity (before insert) {
OpportunityTriggerHandler.limitOpenOppsPerAccount(Trigger.new);
}
Handler Class:
public class OpportunityTriggerHandler {
public static void limitOpenOppsPerAccount(List<Opportunity> newOpps) {
Map<Id, Integer> openCountMap = new Map<Id, Integer>();
Set<Id> accIds = new Set<Id>();
for (Opportunity opp : newOpps) if (opp.AccountId != null) accIds.add(opp.AccountId);
// Query existing open opportunities
for (AggregateResult ar : [
SELECT AccountId, COUNT(Id) cnt FROM Opportunity
WHERE AccountId IN :accIds AND StageName NOT IN ('Closed Won','Closed Lost') GROUP BY AccountId
]) openCountMap.put((Id)ar.get('AccountId'), (Integer)ar.get('cnt'));
// Add errors if limit exceeded
for (Opportunity opp : newOpps) {
Integer existing = openCountMap.containsKey(opp.AccountId) ? openCountMap.get(opp.AccountId) : 0;
if (existing >= 5) opp.addError('Cannot have more than 5 open Opportunities per Account.');
else openCountMap.put(opp.AccountId, existing + 1);
}
}
}
Scenario 17: Assign Task to Opportunity Owner on Big Deal (> 250K)
Requirement: For new Opportunities with Amount > 250,000, create a follow-up Task assigned to the Opportunity Owner to review contract details.
Trigger:
trigger OpportunityTrigger on Opportunity (after insert) {
TaskTriggerHandler.createReviewTasks(Trigger.new);
}
Handler Class:
public class TaskTriggerHandler {
public static void createReviewTasks(List<Opportunity> newOpps) {
List<Task> tasks = new List<Task>();
for (Opportunity opp : newOpps) {
if (opp.Amount != null && opp.Amount > 250000 && opp.OwnerId != null) {
tasks.add(new Task(
WhatId = opp.Id,
OwnerId = opp.OwnerId,
Subject = 'Review Large Opportunity',
Status = 'Not Started',
Priority = 'High'
));
}
}
if (!tasks.isEmpty()) insert tasks;
}
}
Scenario 18: Block Lead Conversion if Matched Account Has No Phone
Requirement: Prevent converting a Lead to Contact+Account if a matching Account exists but its Phone field is blank, ensuring contactability.
Trigger:
trigger LeadTrigger on Lead (before update) {
if (Trigger.isBefore && Trigger.isUpdate) {
for (Lead ld : Trigger.new) {
Lead oldLd = Trigger.oldMap.get(ld.Id);
if (ld.IsConverted && !oldLd.IsConverted) {
Account acct = [SELECT Id, Phone FROM Account WHERE Name = :ld.Company LIMIT 1];
if (acct != null && acct.Phone == null) {
ld.addError('Cannot convert Lead: matching Account has no Phone.');
}
}
}
}
}
Scenario 19: Notify Manager if Contact Title = “VP” or Higher
Requirement: When a new Contact is inserted with Title containing “VP”, send an email notification to the sales manager.
Trigger:
trigger ContactTrigger on Contact (after insert) {
Messaging.TriggerHandler.sendVIPNotifications(Trigger.new);
}
Messaging Helper:
public class Messaging.TriggerHandler {
public static void sendVIPNotifications(List<Contact> contacts) {
List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
for (Contact c : contacts) {
if (c.Title != null && c.Title.contains('VP')) {
emails.add(new Messaging.SingleEmailMessage(
toAddresses = new List<String>{ 'manager@company.com' },
subject = 'New VIP Contact: ' + c.Name,
plainTextBody = 'A new VP-level contact was created: ' + c.Name + '\nEmail: ' + c.Email
));
}
}
if (!emails.isEmpty()) Messaging.sendEmail(emails);
}
}
Scenario 20: Set Contract End Date Based on Subscription Product Term
Requirement: After an Opportunity is marked Closed Won and contains subscription products, create a related Contract and set its End Date to CloseDate + Product_Term__c
months.
Trigger:
trigger OpportunityTrigger on Opportunity (after update) {
ContractTriggerHandler.createContractFromSubscription(Trigger.new, Trigger.oldMap);
}
Handler Class:
public class ContractTriggerHandler {
public static void createContractFromSubscription(List<Opportunity> opps, Map<Id, Opportunity> oldMap) {
List<Contract> contractsToInsert = new List<Contract>();
for (Opportunity opp : opps) {
if (opp.StageName == 'Closed Won' && oldMap.get(opp.Id).StageName != 'Closed Won') {
// Query subscription products
for (OpportunityLineItem oli : [
SELECT PricebookEntry.Product2Id, Quantity, Subscription_Term__c
FROM OpportunityLineItem
WHERE OpportunityId = :opp.Id AND Subscription_Term__c != null
]) {
Date endDate = opp.CloseDate.addMonths((Integer)oli.Subscription_Term__c);
contractsToInsert.add(new Contract(
AccountId = opp.AccountId,
StartDate = opp.CloseDate,
EndDate = endDate,
Opportunity__c = opp.Id
));
}
}
}
if (!contractsToInsert.isEmpty()) insert contractsToInsert;
}
}
Summary
We hope this guide strengthens your Salesforce skill set and prepares you for challenging development scenarios and interviews. Keep innovating, stay curious, and follow SalesforceHours for more in-depth tutorials, code snippets, and real-world best practices.
Follow us for Part 3 or Drop a comment if want more Scenario based Question on Different topics .
Checkout our latest posts 👇
- Ace Your Interview: 35+ Salesforce Sales Cloud Interview Questions
- 20 Salesforce Apex Trigger Scenarios Example (Interview Questions) – Part 2
- Master Your Salesforce Interview: 15 Real-World LWC Scenarios
Discover more from Salesforce Hours
Subscribe to get the latest posts sent to your email.