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

Detect/prevent trigger recursion between your code and vendor's managed code?

Hello.  I have a client who uses Convio Common Ground, which is a managed package.  I wrote an opportunity trigger to do some custom YTD calculations and update a YTD field in the Contact record.  The trigger seems to works fine for single records, but when the client does a mass update of as few as 10-12 opps using the Data Loader, sometimes the opp trigger fails, but the error actually originates in Common Ground's own contact trigger.

[My opp trigger is CalcYTDAmounts, their contact trigger is cv.ContactAll:]

"CalcYTDAmounts: execution of AfterUpdate caused by: System.DmlException: Update failed. First exception on row 0 with id 0038000000j3NvyAAE; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, cv.ContactAll: execution of BeforeUpdate caused by: System.Exception: cv:Too many script statements: 11001"

My opp trigger code is definitely bulkified - I've written many other bulk triggers before, but not in situations where managed packages are also triggering actions.

I opened a case with Convio and they suggested  A) try reducing the batch size to 1 and B) "write trigger recursion safeguards into your code."

I've searched the forums and documentation but haven't found enough info to help me understand how to detect/prevent trigger recursion, esp. when managed package code is involved.  Can anyone suggest some examples?




The opp trigger code:


// ----------------------------------------------------------------------------------------------- // Trigger: CalcYTDAmounts() - Rollup YTD Opps into Contact field. // // Purpose: // - Sum this year's opp amounts using various filters, then update the contact YTD opp amt field // - and also update the contact remaining expected amt field. // ----------------------------------------------------------------------------------------------- trigger CalcYTDAmounts on Opportunity (after insert, after update, after delete) { Set<Id> conIDs = new Set<Id>(); Contact[] changedCons = new List<Contact>(); Opportunity[] recordSet = new List<Opportunity>(); Map<String, Id> recTypesRev = new Map<String, Id>(); Map<Id, Decimal> conRecur = new Map<Id, Decimal>(); //Build a "reversed" map so that the record type name becomes the Key and the id is the Value. for(RecordType rec : [select id, DeveloperName from RecordType]) { recTypesRev.put(rec.DeveloperName, rec.Id); } if(Trigger.IsDelete) { recordSet = Trigger.old; } else { recordSet =; } for(Opportunity opp : recordSet) { conIDs.add(opp.cv__Contact__c); } //Create a contact map. Map<Id, Contact> conMap = new Map<Id, Contact>([select Id, YTD_Donation_Amount__c, Remaining_Recurring_Amount__c from Contact where Id in :conIDs]); //Get contacts with their active recurring gifts. Contact[] temp = [select Id, YTD_Donation_Amount__c, Remaining_Recurring_Amount__c, (select cv__Start_Date__c, Remaining_Expected_Amount__c from cv__R00N40000001X8sTEAS__r where cv__Recurring_Gift_Status__c = 'Active' order by cv__Start_Date__c DESC) from Contact where Id in :conIDs]; //Map most recent recurring gift's remaining expected amt to the contact. //If no recurring gift for the contact, then set a null value. for(Contact con : temp) { if(con.cv__R00N40000001X8sTEAS__r.size() > 0) { conRecur.put(con.Id, con.cv__R00N40000001X8sTEAS__r[0].Remaining_Expected_Amount__c); } else { conRecur.put(con.Id, null); } } //Sum the opps. AggregateResult[] conOppSum = [select cv__Contact__c, SUM(Amount) Amt from Opportunity where cv__Contact__c in :conIDs and CALENDAR_YEAR(CloseDate) = and ( (RecordTypeId = :recTypesRev.get('SingleDonation') and StageName = 'Received') or (RecordTypeId in (:recTypesRev.get('PledgeInstallment'), :recTypesRev.get('RecurringDonation')) and StageName in ('Received', 'Pending Installment') )) GROUP BY cv__Contact__c]; //Loop through the agg result to cast the contact ID and YTD amount and update the values in the contact map. //If the last opp in the current year was deleted for a contact, the contact will not appear in the agg result. //Thus we must capture the contact IDs in the agg result and compare to the conIDs set from the start. Set<Id> arConIDs = new Set<Id>(); for(AggregateResult ar : conOppSum) { Id conID = (Id)ar.get('cv__Contact__c'); arConIDs.add(conID); Decimal oppSum = (Decimal)ar.get('Amt'); conMap.get(conID).YTD_Donation_Amount__c = oppSum; conMap.get(conID).Remaining_Recurring_Amount__c = conRecur.get(conID); changedCons.add(conmap.get(conID)); } //Find the contacts that don't appear in the agg results and null out their YTD field //because they have no opps in the current year. for(Id conID : conIDs) { if(!arConIDs.contains(conID)) { conMap.get(conID).YTD_Donation_Amount__c = null; conMap.get(conID).Remaining_Recurring_Amount__c = conRecur.get(conID); changedCons.add(conmap.get(conID)); } } update changedCons; }





Try some debug statements showing what kind of event triggered the execution (insert, update, delete). You may find the trigger running again if there has been a workflow field update.

There is a way of avoiding running the trigger twice using a static boolean which seems to get set back to its initial value between batches/transactions. There is a thread about it somewhere.


Ah here:

Message Edited by ColinKenworthy2 on 03-26-2010 11:42 AM

Colin - thanks for the info, I'll check it out.


Doug ACFDoug ACF

Hi Dave -


The approaches suggested below will help prevent recursive execution of the trigger you authored, but may not help avoid recursion within the Common Ground triggers.  Have you tried disabling your triggers/workflow rules and seeing if you still see the issue just with the Common Ground triggers?  Based on our experience with Common Ground, I'm guessing that you will still have a problem with mass updates.  You'll need to go with their other advice and reduce your data loader batch size.