Scenario: When Opportunity get Closed Won send Email to Opportunity Owner.
OpportunityTrigger
trigger OpportunityTrigger on Opportunity (after update) {
OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
if(trigger.isAfter && Trigger.isUpdate) {
handler.sendEmailWhenClosed(trigger.old, Trigger.newMap);
}
} OpportunityTriggerHandler
public class OpportunityTriggerHandler {
public void sendEmailWhenClosed(List<Opportunity> lstOppor, Map<id, Opportunity> opprMap) {
for(Id opprId: opprMap.keySet()) {
if(opprMap.get(opprId).StageName == 'Closed Won') {
sendEmail(opprId);
}
}
List<Opportunity> oppr = [Select Id, OwnerId, StageName From Opportunity Where Id in:lstOppor];
}
public void sendEmail(Id OpportunityId) {
List<Opportunity> oppr = [Select Id, Name, OwnerId, StageName From Opportunity Where Id =: OpportunityId limit 1];
List<User> userDetail = [Select UserName, Email From User Where Id=: oppr[0].OwnerId limit 1];
List<Messaging.Email> emailList = new List<Messaging.Email>();
Messaging.SingleEmailMessage sendEmail = new Messaging.SingleEmailMessage();
sendEmail.setToAddresses(new String[] {userDetail[0].Email});
sendEmail.setSubject('Opportunity ' + oppr[0].Name + ' is Closed Won.');
String body = 'Dear ' + userDetail[0].UserName + ' Your Opportunity ' + oppr[0].Name + ' is Closed Won';
sendEmail.setHtmlBody(body);
emailList.add(sendEmail);
Messaging.sendEmail(emailList);
}
} How the code works
When an Opportunity is updated and its stage is Closed Won, the code looks up the Opportunity owner and sends them an email through Messaging.SingleEmailMessage.
Problems in this version
- The email is sent again on every later edit. The code only checks that the stage is Closed Won now. It never checks the old value, so each save of a Closed Won Opportunity sends another email.
- Queries and emails inside a loop.
sendEmailruns two queries and oneMessaging.sendEmailcall for every Opportunity. Apex allows only 10 sendEmail calls per transaction, so a bulk update fails. - An unused query at the end of the first method, which wastes one of the 100 query limit.
- Wrong greeting.
UserNameis the login name. UseNamefor a friendly greeting. - The trigger passes
trigger.old. It is better to passTrigger.newandTrigger.oldMapso the handler can compare old and new stages.
Improved version
trigger OpportunityTrigger on Opportunity (after update) {
OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
handler.sendEmailWhenClosed(Trigger.new, Trigger.oldMap);
} public class OpportunityTriggerHandler {
public void sendEmailWhenClosed(List<Opportunity> newOpps, Map<Id, Opportunity> oldMap) {
List<Opportunity> closedWon = new List<Opportunity>();
for (Opportunity opp : newOpps) {
Opportunity oldOpp = oldMap.get(opp.Id);
if (opp.StageName == 'Closed Won' && oldOpp.StageName != 'Closed Won') {
closedWon.add(opp);
}
}
if (closedWon.isEmpty()) {
return;
}
Set<Id> ownerIds = new Set<Id>();
for (Opportunity opp : closedWon) {
ownerIds.add(opp.OwnerId);
}
Map<Id, User> owners = new Map<Id, User>([SELECT Id, Name, Email FROM User WHERE Id IN :ownerIds]);
List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
for (Opportunity opp : closedWon) {
User owner = owners.get(opp.OwnerId);
if (owner == null || String.isBlank(owner.Email)) {
continue;
}
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new List<String>{ owner.Email });
mail.setSubject('Opportunity ' + opp.Name + ' is Closed Won');
mail.setPlainTextBody('Dear ' + owner.Name + ', your Opportunity ' + opp.Name + ' is Closed Won.');
emails.add(mail);
}
if (!emails.isEmpty()) {
Messaging.sendEmail(emails);
}
}
} This version sends the email only when the stage changes to Closed Won, loads all owners in one query, and sends all emails with one call.
How to test
- Move an Opportunity to Closed Won. The owner should receive one email.
- Edit the same Opportunity again. No new email should be sent.
- Update several Opportunities to Closed Won at once from a list view and check that every owner gets an email.
FAQ
Is there a limit on emails?
Single emails sent from Apex are limited per day for the whole org, so avoid sending emails for every small change.
Could I use a Flow instead?
Yes. A record-triggered Flow with an email alert can do the same without code.