Okay, so the following helped: 1. I changed the trigger to an After trigger. It's got a bunch more overhead (e.g. re-querying the records), but I was desperate and just trying anything different. 2. In that After trigger, instead of doing the standard "Static Boolean"-based recursion prevention, I switched to "check the IDs you have already processed-based" recursion prevention as indicated [here][1]. New code: public with sharing class ContactTriggerHandler { ... public static void copyDailyDumpDataAfterInsertOrUpdate(Map<Id,Contact> newContacts, Map<Id, Contact> oldContacts, Boolean isInsert) { // Make sure this method only runs once per set of just-inserted/updated contacts. if (alreadyProcessed.containsAll(newContacts.keySet())) {return;} alreadyProcessed.addAll(newContacts.keySet()); List<Id> dumpCIds = new List<Id>(); List<Contact> csToUpdate = new List<Contact>(); for (Contact c : newContacts.values()) { // Cache as an "DDmp" Contact record if it was just inserted/updated by the DDmp Jitterbit load if (c.DailyDump_Contact_Id__c <> null && c.DDmp_Jitterbit_Most_Recent_Load_DateTime__c <> null && (isInsert || oldContacts == null || oldContacts.get(c.Id).DDmp_Jitterbit_Most_Recent_Load_DateTime__c == null || oldContacts.get(c.Id).DDmp_Jitterbit_Most_Recent_Load_DateTime__c < c.DDmp_Jitterbit_Most_Recent_Load_DateTime__c) ) { dumpCIds.add(c.Id); } } if (dumpCIds.size()>0) { Boolean chgdDump = false; for (Contact c : [SELECT Id ,PIDM__c,DDmp_Copy_of_PIDM__c ,Banner_UST_ID__c,DDmp_Copy_Of_Banner_ID__c ,Birthdate,DDmp_Birthdate__c ,Citizenship__c,DDmp_Applicant_Citizenship_Status__c ,Company_Holding_Spot__c,DDmp_Account_Name__c ,Email,DDmp_Email__c ,FirstName,DDmp_FirstName__c ,Gender__c,DDmp_Gender__c ,MailingStreet,DDmp_MailingStreetLine1__c,DDmp_MailingStreetLine2__c,DDmp_MailingStreetLine3__c ,MailingCity,DDmp_MailingCity__c ,MailingState,DDmp_MailingState__c ,MailingPostalCode,DDmp_MailingZip__c ,MailingCountry,DDmp_MailingCountry__c ,Phone,OtherPhone,DDmp_HomePhone__c,DDmp_MobilePhone__c,DDmp_Work_Phone__c FROM Contact WHERE Id IN :dumpCIds]) { { // Smart-edit various fields if (c.PIDM__c==null && c.DDmp_Copy_of_PIDM__c<>null) c.PIDM__c = c.DDmp_Copy_of_PIDM__c; chgdDump=TRUE; if (c.Banner_UST_ID__c==null && c.DDmp_Copy_Of_Banner_ID__c<>null) c.Banner_UST_ID__c = c.DDmp_Copy_Of_Banner_ID__c; chgdDump=TRUE; if (c.Birthdate==null && c.DDmp_Birthdate__c<>null) c.Birthdate = c.DDmp_Birthdate__c; chgdDump=TRUE; if (c.Citizenship__c==null && c.DDmp_Applicant_Citizenship_Status__c<>null) c.Citizenship__c = c.DDmp_Applicant_Citizenship_Status__c; chgdDump=TRUE; if (c.Company_Holding_Spot__c==null && c.DDmp_Account_Name__c<>null) c.Company_Holding_Spot__c = c.DDmp_Account_Name__c; chgdDump=TRUE; if (c.Email==null && c.DDmp_Email__c<>null) c.Email = c.DDmp_Email__c; chgdDump=TRUE; System.debug('First names: ' + c.FirstName + c.DDmp_FirstName__c); // DEBUG LINE ONLY if ((c.FirstName==null || Test.IsRunningTest()) && c.DDmp_FirstName__c<>null) c.FirstName = c.DDmp_FirstName__c; chgdDump=TRUE; // To make debug logs easier to read, our test to make sure this works starts with a FirstName in place, unlike real data, hence the alternative check for isRunningTest for overwriting. if (c.Gender__c==null && c.DDmp_Gender__c<>null) c.Gender__c = c.DDmp_Gender__c; chgdDump=TRUE; if (c.MailingStreet==null && c.DDmp_MailingStreetLine1__c<>null) c.MailingStreet = c.DDmp_MailingStreetLine1__c + ((c.DDmp_MailingStreetLine2__c <> null) ? ('\r\n' + c.DDmp_MailingStreetLine2__c) : '') + ((c.DDmp_MailingStreetLine3__c <> null) ? ('\r\n' + c.DDmp_MailingStreetLine3__c) : ''); chgdDump=TRUE; if (c.MailingCity==null && c.DDmp_MailingCity__c<>null) c.MailingCity = c.DDmp_MailingCity__c; chgdDump=TRUE; if (c.MailingState==null && c.DDmp_MailingState__c<>null) c.MailingState = c.DDmp_MailingState__c; chgdDump=TRUE; if (c.MailingPostalCode==null && c.DDmp_MailingZip__c<>null) c.MailingPostalCode = c.DDmp_MailingZip__c; chgdDump=TRUE; if (c.MailingCountry==null && c.DDmp_MailingCountry__c<>null) c.MailingCountry = c.DDmp_MailingCountry__c; chgdDump=TRUE; // Take phone number from best in preference order Home, Mobile, Work if (c.Phone==null) { if (c.DDmp_HomePhone__c<>null) {c.Phone=c.DDmp_HomePhone__c; chgdDump=TRUE;} else if (c.DDmp_MobilePhone__c<>null) {c.Phone=c.DDmp_MobilePhone__c; chgdDump=TRUE;} else c.Phone=c.DDmp_Work_Phone__c; chgdDump=TRUE; } // If another phone exists, take it from 2nd-best in preference order Home, Mobile, Work if (c.OtherPhone == null) { if (c.DDmp_HomePhone__c<>null) {c.OtherPhone = (c.DDmp_MobilePhone__c<>null) ? c.DDmp_MobilePhone__c : c.DDmp_Work_Phone__c ; chgdDump=TRUE;} } } // If anything was smart-edited, set the record aside for updating if (chgdDump) csToUpdate.add(c); chgdDump=FALSE; } } if(csToUpdate.size()>0) { UPDATE csToUpdate; } } ... } I'm glad it's working, but for future reference, does anyone have any suggestions how I could achieve the same "large dataset recursion prevention" effect with a Before trigger in this context? (There's no "Id" to latch onto and cache at the class level yet...) [1]: https://salesforce.stackexchange.com/questions/61613/how-to-avoid-recursive-triggers-when-trigger-set-size-is-200 "here"