8

There is a lot of information out there on preventing trigger recursion. I have a related question I would like to pose but it has some key differences. I was not able to find an existing answer that covered this case but if I overlooked one feel free to just point that out.

Does anyone know a surefire way to detect the second execution of a trigger caused by a workflow field update with the following criteria:

  • Trigger logic still works when there are no field updates - meaning don't only run the second time.
  • We can detect what the workflow actually changed - By default only Trigger.new is updated by the workflow but we are interested in the difference between Trigger.new before workflow and Trigger.new after workflow because we already made some calculations and may need to adjust them or roll them back.
  • A subsequent update to the same record in the same execution context (via APEX) is considered a new trigger run and is not mistaken for a workflow field update even if there are no workflows on the object.
  • (Flexible) Third party APEX code does not need to call global utility methods to reset the cache of this trigger pattern.

The closest I have come so far is to cache a static Trigger.mid list at the end of a trigger run with a key created from the Trigger.old items. Then when a trigger is running, the trigger checks to see if it has an entry in the Trigger.mid cache for the Trigger.old key and if it does, it pulls that and uses it in place of Trigger.old. This gives us the actual changes that were made by the workflows. However, I have not figured out how to correctly clear the cache in the scenario where there are no workflows so a subsequent update to the exact same record list in APEX causes a problem because its seen as a second run caused by a workflow field update. Any thoughts on other approaches or how to tweak this approach to meet the requirements?

I have not added any code to this question because I think it's really more a theoretical question but if need be I am happy to add some.

  • You might instead consider carefully implemented criteria filters. That includes making sure relevant fields changed to a given value. – Adrian Larson Feb 7 '17 at 20:43
  • @AdrianLarson are you referring to the workflow criteria filters? – dsharrison Feb 7 '17 at 20:46
  • All of your trigger logic, be it an Apex Trigger, Workflow Rule, Process Builder flow, etc. – Adrian Larson Feb 7 '17 at 20:46
  • 2
    We own the trigger logic that is packaged, but with the nature of Salesforce we are always running alongside other code that is sometimes less thoughtfully written. Let's say we want to execute certain logic when Stage moves from A to C and a customer makes that update in the UI. If there is then a workflow that moves stage to B I would like to see the C to B change to know that we want to roll back the C updates and make some different ones just as we would if a User moved Stage from C to B in the UI. This might not be possible and we would instead need to track each update we already made. – dsharrison Feb 7 '17 at 20:53
  • @D.S. I recently ran into a similar scenario and documented it in Preventing trigger recursion and handling a Workflow field update and Trigger recursion giving me a bad day. I'll try and find the time to see if it applies to your situation. – Daniel Ballinger Feb 8 '17 at 1:48
2

Here is my attempt at solving this problem - adapted from my code here.

public class MyObjectServices {
    public static Set<Id> recordsProcessed = new Set<Id>();
    public static Map<Id, MyObject__c> cachedObjects();     

    public static void myTriggerMethod(Map<Id, MyObject__c> oldMap, Map<Id, MyObject__c> newMap){
        Boolean isFirstRun = true;

        //The number of records processed in all "batches" to this point
        Integer sizeBefore = recordsProcessed.size();

        //Add the ids all records being processed this "batch". If they've already
        //been processed, the set will prevent duplicates from being added
        recordsProcessed.addAll(newMap.keySet());

        //Determines if the records included in the current "batch" of 200 
        //have been processed before
        if (recordsProcessed.size() == sizeBefore){
            isFirstRun = false;
            //Removes them from the set so they can be re-processed in the same transaction
            recordsProcessed.removeAll(newMap.keySet());

        } else {
            cachedObjects = newMap;
        }
    }
}

After evaluating this logic, if you are in a first run, isFirstRun will be true and you will use the oldMap and newMap as before. If you are evaluating this code after a workflow, isFirstRun will be false, and the cachedObjects will be the versions of the objects before workflow.

Now, after the workflow run, the ids will be removed from the map, allowing subsequent updates on the same records to interact as usual. The only shortcoming I see is that this will not work if the trigger itself calls an update on the same records (which is enforced by Apex but workarounds exist).

  • 1
    You can use Set#addAll/Set#removeAll for efficiency purposes, FYI. – sfdcfox Feb 7 '17 at 21:46
  • Good call, @sfdcfox. Edited above. – SFDC Neuf Feb 7 '17 at 21:49
  • 2
    Also, I suspect there may be a bug lurking here. If you use this method, you may miss records in the event of a partial success. While the database is rolled back to the beginning of the DML transaction, static variables are not, which can result in missing records. I've used a similar framework, and it works well as long as you don't have any possibility of errors. – sfdcfox Feb 7 '17 at 21:52
  • I think you could still use this framework as long as you implement transaction control very carefully. But you're right, partial success, without accounting for it, would break this code. – SFDC Neuf Feb 7 '17 at 21:59
  • @SFDCNeuf Thanks for the response, I'll try this out and report back. – dsharrison Feb 7 '17 at 22:05
0

As of API 39.0 I couldn't find a reliable way to use only Apex in determining whether before update and after update triggers were running as a result of a field update.

My solution was to by convention require a field IsProcessed__c to be created on the object in question, and to also create a workflow rule to mark this checkbox. This way your trigger has a 100% reliable way to know when it's running as a result of a field update, simply by examining IsProcessed__c.

The full framework I built around this is called sf-trigger-workflow on GitHub if you'd like to see the full implementation details.

  • Not sure the repo works when Database AllOrNone = false use case and 1+ records in the batch succeed and 1+ fail. Static variables are not reset, see doc, second bullet – cropredy Dec 16 '17 at 20:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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