function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion

can I create a Workflow email alert to an Opportunity Contact?

I want to send an email thanking the Primary Contact on an Opportunity when we mark the corresponding Opportunity Closed Won. But I don't see that object (Opportunity Contact Role) as an option when setting up the email alert.

Any suggestions?


Any solution?  I'm facing the same problem.
The Opportunity object doesn't come with a lookup field to the Contact table, which would allow you to create email alerts to a Contact through workflow on an Opportunity.  To get around this, I created a custom Contact lookup field on the Opportunity called "Primary Contact" and then used an apex trigger on the Opportunity table to set the field by querying the OppContactRoles table looking for the Primary.  Now I can create workflow alerts that go to this Contact based on my Opportunity criteria.

Ideally we should use a trigger off the OpportunityContactRole table to capture any changes to it, however the API doesn't currently allow us to attach triggers to this table.  If your workflow alert is being sent immediately upon some change to the Opportunity (ie. Stage = Closed Won) then the beforeUpdate trigger will populate the "Primary Contact" field at that moment and then the workflow should use that latest value when sending the email.

You could also send the email out from a trigger directly instead of updating the custom Contact field, but that method means coding anytime you want to adjust the email, add another email or cancel one, whereas setting the Contact field in a trigger allows any admin that can configure workflow to manage the email automation.

When you code the trigger, don't forget about bulk processing as the Opportunity is often an object that is updated en masse through tools or enhanced list updates.

I hope this helps.  If anyone finds a better way, let me know too!
Message Edited by NikiV on 02-26-2009 04:15 PM



Could you possibly post your sample trigger code?


This would greatly help those of us that are just learning to use triggers.


Here's my sample trigger.  You need to create a custom Lookup field called Primary_Contact__c on the Opp for this to work.  Also keep in mind it only fires when the Opportunity itself is updated, not just adding the Opp Contact Role in.


And also remember that the email alerts sent from a workflow rule are sent out addressed from the person who created the rule.  Something I thought of but haven't tested yet is to create an admin user called "Info" or "Sales" with the email address of, create the rule and then deactivate the user.  That might trick SFDC into sending out the email alert from instead of your own admin's email.  Maybe it will work, maybe not.


Anyways, here is the trigger code:



trigger UpdateContactRole on Opportunity (before update) { // limit to 1000 entries in Set, scales by number of records up to 5000 // Make list of Opp ids for query of OCR records Set<Id> OppIds = new Set<Id>(); for (Opportunity o : {OppIds.add(;} // Map of Opp id, related list of OCR ids // I used a map with a list of OCRs as you may want to have multiple Contacts listed on Opp, Primary Contact, Ship To Contact, etc // so get them all and look for other conditions like role = xxx. If you just want Primary, SFDC ensure there is only 1 per Opp so you could // make your map <Id, OpportunityContactRole> instead of the list. Map<Id, List<OpportunityContactRole>> Opp_OCR = new Map<Id, List<OpportunityContactRole>>(); // find all related OCRs to build Map for (OpportunityContactRole ocr : [select id, contactid, Opportunityid, role, isprimary, createddate from OpportunityContactRole where opportunityid in :OppIds and isprimary = true]) { // look for Oppid in master map List<OpportunityContactRole> tmp_ocr = new List<OpportunityContactRole>(); tmp_ocr = Opp_OCR.get(ocr.opportunityid); // if Oppid not already in map, add it with this new OCR record if (tmp_ocr == null) { Opp_OCR.put(ocr.opportunityid, new List<OpportunityContactRole>{ocr}); } else { // otherwise add this new OCR record to the existing list tmp_ocr.add(ocr); Opp_OCR.put(ocr.opportunityid, tmp_ocr); } } system.debug('Final OCR map: '+Opp_OCR); // for each Opp modified in the trigger, try to find relevant contacts for (Opportunity opps : { // temporary list of OCRs for this Opportunity populated from the master map List<OpportunityContactRole> this_OCR = new List<OpportunityContactRole>(); this_OCR = Opp_OCR.get(; system.debug('this Opps ('') list of OCRs: '+this_OCR); // if no OCRs related, null out contact field(s) if (this_OCR == null) opps.primary_contact__c = null; else { // cycle through all ocrs for this Opp and trap the various roles if you want for (OpportunityContactRole r : this_OCR) { //system.debug('cycling through the OCR list: '+r); // if the role is primary, track this Contact id if (r.isprimary) opps.primary_contact__c = r.contactid; } // end for loop } // end if multiple OCRs for this Opp } // end for loop of Opps }




Thanks so much for sharing your trigger!

Thanks for posting this!  I created this trigger in my sandbox and then attempted to package it and move it into production and realized I didn't have a test method written.  I've created visual force pages, but never created a trigger.  I'm new to this and don't know where to begin... Could you post your test method ? 




Here is a test class with a method that will run through the code.  I'm testing if there are no Contact Roles, if there is one but it is not marked Primary, and if there are more than one I test that it picked the primary Contact.



private class Populate_Contact_onOpp_fromOCR {

static testMethod void ContactRoleTest() {
// create test account
Account a = new Account(name='sample');
insert a;

// create test contacts
Contact c1 = new Contact(firstname='Sally', lastname='Doe');
Contact c2 = new Contact(firstname='John', lastname='Black');
Contact c3 = new Contact(firstname='Jill', lastname='Hill');
Contact[] cons = new Contact[]{c1, c2, c3};
insert cons;

// create test opp
Opportunity o = new Opportunity(name='test Opp',, stagename='Prospect');
insert o;

// test 1: no OCRs == null
o = [select primary_contact__c from opportunity where];
o.description='initial test';
update o;
system.assert(o.primary_contact__c == null);

// test 2: 1 OCR, not Primary == null
OpportunityContactRole ocr1 = new OpportunityContactRole(,, role='Consultant');
insert ocr1;
ocr1 = [select createddate, opportunityid, contactid, role, isprimary from opportunitycontactrole where id =];
system.debug('************ ocr is:'+ocr1);
update o;
o = [select primary_contact__c from opportunity where];
system.assert(o.primary_contact__c == null);

// test 3: 1 OCR, Primary == c1
update ocr1;
update o;
o = [select primary_contact__c from opportunity where];
system.assert(o.primary_contact__c ==;

// test 4: 2 OCR, 1 not Primary, 1 Primary == c2
OpportunityContactRole ocr2 = new OpportunityContactRole(,, role='Ship To', isprimary=true);
insert ocr2;
ocr2 = [select createddate, opportunityid, contactid, role, isprimary from opportunitycontactrole where id =];
update ocr1;
update o;
o = [select primary_contact__c from opportunity where];
system.assert(o.primary_contact__c ==;



Message Edited by NikiV on 07-13-2009 12:13 PM
Thanks so much for your help !

This trigger is still helpful today. Many thanks Niki!


Thank you so much!


We are upgrading to enterprise next month. I can't wait to put into place this code. Thanks for the workaround.


Can i get a sample of the trigger you used for this?




Thanks so much for the trigger & coverage, Niki!  That's great. 


I reworked your trigger so it does the same thing - only it inserts the Primary Partner Account from the opportunity into a custom Account Lookup field on the opportunity called: Primary_Partner_Account_Custom__c 


Here it is:


trigger GrabPrimaryPartner on Opportunity (before insert, before update) {

Set<Id> OppIds = new Set<Id>();
for (Opportunity o : {OppIds.add(;}

Map<Id, List<OpportunityPartner>> Opp_partner = new Map<Id, List<OpportunityPartner>>();

for (OpportunityPartner opppart : [select id, AccountToId, Opportunityid, isPrimary from
OpportunityPartner where opportunityid in :OppIds and isPrimary = true]) {

List<OpportunityPartner> tmp_opppart = new List<OpportunityPartner>();
tmp_opppart = Opp_partner.get(opppart.opportunityid);
if (tmp_opppart == null) {
Opp_partner.put(opppart.opportunityid, new List<OpportunityPartner>{opppart});
} else {

Opp_partner.put(opppart.opportunityid, tmp_opppart);
system.debug('Final Opp_partner map: '+Opp_partner);

for (Opportunity opps : {
List<OpportunityPartner> this_opppart = new List<OpportunityPartner>();
this_opppart = Opp_partner.get(;
system.debug('this Opps ('') list of oppparts: '+this_opppart);

if (this_opppart == null) opps.Primary_Partner_Account_Custom__c = null;
else {

for (OpportunityPartner r : this_opppart) {
if (r.isprimary) opps.Primary_Partner_Account_Custom__c = r.AccountToId;


Joanne ButterfieldJoanne Butterfield
Thank you for sharing your code for the primary partner, its very useful. Could you also share your test class?
Thank you!