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

Apex trigger for OpportunityContactRole object

Hi experts,
I found a Salesforce object named OpportunityContactRole by using the IDE (Eclipse) and I want to add a new trigger for this object but I got the following Error message when try to save the new created trigger:
    Save error: SObject type does not allow triggers: OpportunityContactRole
I really need to add the special processing to this object before updating, do any experts have any idea how to fix this issue?
Best regards!
Boi Hue

Message Edited by boihue on 12-09-2008 11:02 AM

Message Edited by boihue on 12-09-2008 11:02 AM
The sobject is queryable through Apex. You can write a process that queries for the records and processes them.
This is not real time, and I bet you want real time updates.

I thought maybe the opportunity was updated when you add a contact role record but it's not.

Anyone else have an idea?


It's impossible to create the trigger for OpportunityContactRole object, and even the Opportunity's trigger has not been fired when Contact Roles are inserted or updated. I found that there're too much limit in Salesforce system (Governors limit, junction tables...).





I have the same problem here. I'm debugging, and when I update the contact roles for an oppty it does not make any API call...



So, the oppty trigger is not fired and the OpportunityContactRole insert is done in the "background"????


Anyone? any ideas on this?


Thanks in advance!


Was anyone able to find a solution to this problem?




Here's a (possibly known) trick to detect such a change using Visualforce and native Opportunity layouts. It's really about having your code "know" (and possibly execute) every time a user visits a native Salesforce layout (regardless of the object.) Hence, we could call it an "On-View" trigger.

Write a very simple Apex controller that has a constructor and one method with a future annotation. The future annotation will ensure there's no lack time when the page is loaded due to the "hidden" Apex processing you'll be doing:

public with sharing class OppHelper {
    public Opportunity opp;
    public OppHelper( ApexPages.StandardController stdController ) {
        opp = ( Opportunity )stdController.getRecord();        
    public void rollupOppContacts(){
        OppHelper.rollupOppContactsFuture( opp.Id );    
    @future public static void rollupOppContactsFuture( Id oppId ) {
        Contact[] contactList = [ SELECT Id, Some_field__c FROM Contact
                             WHERE Id IN ( SELECT ContactId FROM OpportunityContactRole
                                         WHERE OpportunityId = :oppId ) ];
        Opportunity opp = [ SELECT Id, Some_rollup_score__c FROM Opportunity WHERE Id = :oppId ];
        opp.Some_rollup_score__c = 0;
        for( Contact contact : contactList ) {
            opp.Some_rollup_score__c = contact.Some_field__c;
        update opp;


Then write a very simple Visualforce page that uses the "action" attribute in the apex:page markup:


<apex:page standardController="Opportunity" extensions="OppHelper" action="{!rollupOppContacts}" /> 


Add the Visualforce page to any/all Opportunity layouts with width and height settings set to zero.

Everytime the user navigates to an Opportunity layout the code will execute. Given that the only way a user can make a change to Opportunity Contact Roles is through some sort of "native" Opportunity layout, you're ensured that this will trigger for every such change. (The exception being if you're org already has some custom Apex/VF that allows/makes changes to Opportunity Contact Roles… in which case you could do the appropriate rollup/scoring etc.. within that.) Just to be clear, if a user makes a change to Opportunity Contact Roles, after clicking save, Salesforce brings the user back to the Opportunity layout by default,, so your code will execute again with the freshest Opportunity Contact Role data.

Lastly, it should be noted that using the future annotation means it won't be totally real time… but more so than a cron job and as mentioned earlier, the page will load without delay.

Happy clouds ~~~


Ingenious solution.  And with knowledge of this work-around, (which fires (translation - spins server cpu cycles) on Opportunity display whether or not OCR's were touched, viewed, or not), Salesforce should drop the restriction, and allow the creation of triggers against the OCR object itself. :)





(What if we said PLEASE?)

Nathan WilliamsNathan Williams
This is genius, well done!
Roger WickiRoger Wicki
I already set up a class "ApexDailyJobs" which I scheduled to just execute daily and tehere I can just add in any additional processing that needs to be done daily. I guess I could just add in an OCR processing, oh well...

This is a very cool trick indeed! +1 for that one. I just fear that this piece of code will be executed so often that it may eat quite some calls...
What if you have deleted an OCR?  If an OCR is removed from a certain opportunity (OppX), I don't want that opportunity name to appear in a special custom field for the contact. Is there any way to handle that situation?
Clever work around.  Too bad we have to resort to such a kludge though.  This is technical debt. 
Chirag MehtaChirag Mehta
This is really required. +1.

PS: Recently I happen to install Rollup Helper app and it generated error saying "SObject type does not allow triggers: OpportunityContactRole.
  • Does that indicate Rollup Helper app is trying to deploy trigger on OpportunityContactRole?
  • If yes, then how did Rollup Helper package was able to create trigger on OpportunityContactRole?
  • Can trigger on OpportunityContactRole be enabled by reaching Salesforce Support?
Good idea, @Tbom, except it doesn't re-run when a Contact Role is added - only when the entire Opportunity record page is reloaded.
krishna chaitanya 35krishna chaitanya 35
Hi @Tbom,

Can you please explain the situation like what is the field in contact 'Some_field__c ' and it is assigned to opportunity finally.may i know the field and exact assignment.I have this requirement in out development.please help me on this.

Jatin NarulaJatin Narula
Hello Everyone, 

It will be good to have Contact Role exposed to triggers, but there are three solutions, I can think:

1. Scheduling a script to run every day to update count field on the opportunity. (I recommend this solution).

2. creating a small VF, and adding it on the detail page of the opportunity (it will not be visible just like a VF we removed a few weeks ago), so when the opportunity detail page is opened it will count the contact roles of that opportunity and update it if changed [it will work instantly but it will require the detail page to load for once], opportunities which are not visited will be untouched.

3. Adding logic to opportunity trigger so every time an opportunity is edited, it will query on OpportunityContactRole to roll up (personally I do not recommend this).


Jatin Narula
Paula MenendezPaula Menendez
Hi all! I found this solution which seems very hellpful:
I am just curious why Jatin doesnt think adding logic on the opportunity trigger is a good solution? Thanks !
Roger WickiRoger Wicki
@Paula Menendez
The problem with jatin's solution 3. with an Opportunity trigger is that in case an opportunity is edited normally, e.g. changing the stage from Prospect to Closed Won, will cause a recalculation of the Opportunity Contact Roles even though it was not necessary as the roles were not changed. That means you do A LOT more useless recalculation than actually useful ones. Plus, a change in an Opportunity Contact Role does not necessarily cause an update on the Opportunity. Thus if you ONLY change the Opportunity Contact Role but nothing on the Opportunity, you will have wrong data.
Option 1. with a daily script is much better because you will with 100% security only update Opportunities of which the Opportunity Contact Roles have changed. The only drawback of this is, it is not real time. Usually one programs these scripts to run in the night.

Just thought about this: Your method 1. is unsecure when it's about deleting opportunity contact roles unless you include deleted records in your query.
Jatin NarulaJatin Narula
@Roger, yeah the script should not query deleted records. 
Roger WickiRoger Wicki
Hey Jatin

I actually think it should query deleted records. Otherwise how would you recognise that one Opportunity Contact Role got deleted? If you don't your count will still show the old value.
Jatin NarulaJatin Narula
Roger, we don't need to do it, use the code written below to test it:

List<Opportunity> oppsToUpdate = new List<Opportunity>();

for(Opportunity each : [SELECT Id, Count__c, (SELECT Id FROM OpportunityContactRoles) FROM Opportunity]){
    Integer roleCount = each.OpportunityContactRoles.size();
    if(roleCount != each.Count__c){
        Count__c = each.OpportunityContactRoles.size();

if(!oppsToUpdate.isEmpty()) Database.update(oppsToUpdate, false);
Roger WickiRoger Wicki
Hi Jatin

While this indeed works as you state, it is not viable for large organisations with hundreds of thousands of Opportunities. Basically you check your entire company's worth of Opportunity data every day.

I just made another check but it seems like this is the best we can achieve, since delete OCRs don't wander into the Recycle Bin and thus are not queriable with ALL ROWS.
Paula MenendezPaula Menendez
What do you guys think about this solution proposed here by Pat Penaherrera:

The solution proposed is to pass as an argument to the method below the opportunities that change stage to: Closed Won. But im wondering if I could be able to, every time an opportunityContactRole is edited, to pass the Opportunity that is related to it, as the argument of the enforceContactRoleValidation() (see code below) ?

"This is a static method that you can place in a utility class separate from your Opportunity Trigger.  You can pass in some arguments, such as the Contact Role you are checking for, and the message you want displayed to the user if the Contact Role is not found.  In this way, the method is a little more reusable in case your requirements change in the future.

In your Opportunity trigger, you would first build a map of all Opportunities that have changed to the "Closed Won" stage as a part of the current transaction.  Using that map, you can call this method below from your utility class and pass in the list of Opportunity Ids, the Opportunities themselves, as well as the Contact Role and the Error Message text.  The method will do the rest!

I hope this is helpful!  If so, please like and mark this answer as the best answer at your convenience, and as always, let us know if you need anything else! :) "
public static void enforceContactRoleValidation(List<Opportunity> opps, Set<Id> oppIds, String contactRole, String message){
        //Query for the OpportunityContactRole records related to the Opportunity Ids that were passed into the method
        List<OpportunityContactRole> ocrs = [SELECT OpportunityId, Role FROM OpportunityContactRole WHERE OpportunityId in: oppIds AND Role = :contactRole];
        //Build a Map where Opportunity Ids will be the keys, and OpportunityContactRole records will be the values
        Map<Id,OpportunityContactRole> ocrMap = new Map<Id,OpportunityContactRole>();
        for(OpportunityContactRole ocr: ocrs){
            ocrMap.put(ocr.OpportunityId, ocr);
        //Now, perform our check
        for(Opportunity o: opps){
Thankyou all for your help!!
Shubham Tiwari 11Shubham Tiwari 11
Now in the current release we can write trigger on OCR.