We have a process where when records are updated with Sync_Prospect_to_Pardot__c to true, it should create a pi__ObjectChangeLog__c record for every record updated. However, when I use the data loader to update 250 records with Bulk API enabled, only 200 pi__ObjectChangeLog__c records are created. If I have that setting unchecked, all 250 create. I logged a ticket with salesforce support and they said it's not an issue with the data loader and they won't provide feedback on the code as it is out of scope. Pardot support hasn't been helpful as we're using a custom process to create the object change logs. There's clearly something going on here and the code isn't anything different than what I've written for the rest of the org. We don't see issues using bulk api to fire off any of those processes.

The pi__ObjectChangeLog__c object is marked as "Allow Bulk API" access.

Is the issue the code? If not, what are some other things I need to check? I get the same results whether the starting sobject is a Contact or Lead.

Here's the components. Note we use Kevin O'Hara's SFDC Trigger Framework as recommended by Trailhead.


trigger LeadTrigger on Lead (before insert, before update, before delete, after insert, after update, after delete, after undelete) {
    Map<String, Trigger_Settings__c> triggerSettings = Trigger_Settings__c.getAll();
    if(triggerSettings.get('LeadTrigger') <> null && triggerSettings.get('LeadTrigger').Execute_Trigger__c)
        new LeadTriggerDispatcher().run();


public without sharing class LeadTriggerDispatcher extends TriggerHandler {
//Trigger context variables are still available but need to be cast: trigger.new, trigger.newMap, trigger.oldMap
    List<Lead> leads = (List<Lead>) trigger.new;
    Map<Id, Lead> leadsOldMap = (Map<Id, Lead>) trigger.oldMap;
    Map<Id, Lead> leadsNewMap = (Map<Id, Lead>) trigger.newMap;
    Map<String, Trigger_Settings__c> triggerSettings = Trigger_Settings__c.getAll();

    protected override void afterUpdate() {
        //declare variables
        Set<sObject> leadsForSync = new Set<sObject>();

        //loop through to find qualifying records
        for(Lead lead : leads) {
            //leads are eligible for sync if their sync to prospect field changes, but no email changes were made. By default an OCL gets created if the email is updated
            if(leadsOldMap.get(lead.Id).Sync_Prospect_to_Pardot__c == false && leadsNewMap.get(lead.Id).Sync_Prospect_to_Pardot__c == true)

        //execute methods
        if(!leadsForSync.isEmpty() && !TriggerHandler.isBypassed('PardotSyncManager') && triggerSettings.get('PardotSyncManager') <> null && triggerSettings.get('PardotSyncManager').Execute_Trigger__c) {


public without sharing class PardotSyncManager {
    //createObjectChangeLog takes a set of lead or contact records and creates an OCL record for those records. Called from the contact or lead dispatcher
    public static void createObjectChangeLog(Set<sObject> records) {
        system.debug('PardotSyncManager Start');

        system.debug('records size: ' + records.size());

        //oclsForInsert stores a list of OCLs that need to be inserted
        List<pi__ObjectChangeLog__c> oclsForInsert = new List<pi__ObjectChangeLog__c>();

        //Iterate through all records in the scope
        for(sObject record : records) {
            system.debug('record Ids: ' + record.Id);
            //Check if the record is a contact. If so, then parse the sObject to a contact and then create an OCL for that contact.
            if(String.valueOf(record.Id).substring(0,3) == '003') {
                Contact contact = (Contact)record;
                oclsForInsert.add(new pi__ObjectChangeLog__c(pi__ObjectEmail__c = contact.Email, pi__ObjectFid__c = contact.Id, pi__ObjectType__c = 1, pi__ObjectState__c = 1));
            //Check if the record is a lead. If so, then parse the record into a lead and then create an OCL for that lead.
            else if(String.valueOf(record.Id).substring(0,3) == '00Q') {
                system.debug('lead qualified');
                Lead lead = (Lead)record;
                oclsForInsert.add(new pi__ObjectChangeLog__c(pi__ObjectEmail__c = lead.Email, pi__ObjectFid__c = lead.Id, pi__ObjectType__c = 2, pi__ObjectState__c = 1));

        //try to insert the OCL records
        try {
            system.debug('ocls for insert: ' + oclsForInsert.size());
            insert oclsForInsert;
        //If there's an error, post the error to the debug log
        catch(Exception e) {
            system.debug('EXCEPTION in PardotSyncManager.createObjectChangeLog at row ' + e.getLineNumber() + ': ' + e.getMessage());

        system.debug('PardotSyncManager Complete');
  • A simple boolean bypass is dangerous way to suppress re-execution of triggers because any operation impacting more than 200 records will have the trigger invoked multiple times, receiving 200 records at a time.
    – Phil W
    Nov 19 '20 at 21:01
  • Looking at the related question that was linked - the static Map<TriggerOperation, Set<Id>>, Is the TriggerOperation the DML operation in the format of BEFORE_INSERT, BEFORE_UPDATE, etc..? And then the Id is the record in context? Ideally, I want to prevent the class from rerunning again in subtransactions. Would using a Map<String, Set<id>> where the String is my class name be just as effective as the suggestion? Or just as dangerous? Nov 20 '20 at 13:48
  • You could have a Map<String, Map<TriggerOperation, Set<Id>> where the string is your "trigger name" and the rest is as per the other Q&A answer. This is effective in suppressing re-execution of a trigger against a specific record, by ID for a given operation, except before insert, within a given transaction/request/session.
    – Phil W
    Nov 20 '20 at 14:53
  • Is it a bad practice to suppress across any trigger operations? That's really what I'm trying to get after as there's very few instances that the actions in a insert vs update output are different for us. Nov 20 '20 at 20:02
  • Personally I would not suppress any events at all, even multiple updates, due to the fact that process builders and flows can cause re-entrancy of the entire save process (see this documentation. If your code actually only does something when specific changes happen, and during the correct phases, then you can make the trigger DML efficient at the cost of a bit more Apex processing.
    – Phil W
    Nov 20 '20 at 21:21

TriggerHandler.isBypassed is referred inside the trigger code to check whether it is executed already or not.

But Bulk API process all records under one transaction, which means the static variables are never reset.

Please refer to these - Bulk API and Static Variable State

enter image description here enter image description here


You need to clear the bypass in your dispatcher once the PardotSyncManager code is finished executing.

Effectively making your if block look like this instead:

//execute methods
if (
  !leadsForSync.isEmpty() &&
  !TriggerHandler.isBypassed('PardotSyncManager') && // this is causing it to only run on the first iteration
  triggerSettings.get('PardotSyncManager') <> null &&
) {
  // turn off the trigger handler

  // do the work

  // turn the trigger handler back on
  TriggerHandler.clearBypass('PardotSyncManager'); // because this bypass removal was not here
  • Wouldn't that defeat the purpose of suppressing recursion in a non-bulk API context? Nov 20 '20 at 13:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.