Tag Archives: recursion

Triggers – workflow – recursion control – callouts – allOrNone

An insidious set of circumstances:

Starting condition:
  • You have an afterUpdate trigger that if a condition when oldvalue changes to newValue, you want to do a callout
  • You also have a workflow that updates the same object and that workflow’s entry criteria is satisfied when the for the same update event

Now, you might not know this, but Workflow field updates will cause the trigger to re-execute AND, the value of Trigger.old will be as it was when the record was initially updated. Here’s what it says from the well-thumbed Triggers and Order of Execution Apex Doc:

Trigger.old contains a version of the objects before the specific update that fired the trigger. However, there is an exception. When a record is updated and subsequently triggers a workflow rule field update, Trigger.old in the last update trigger won’t contain the version of the object immediately prior to the workflow update, but the object before the initial update was made

Thus, your future method will be called TWICE, and, if it is doing a callout, will callout twice. Here’s a simple proof:

Apex trigger
trigger LeadTrigger on Lead (before insert, before update, after insert, after update) {

    if (Trigger.isAfter && Trigger.isUpdate)
        new LeadTriggerHandler().onAfterUpdate(Trigger.new,Trigger.oldMap);
}
Apex Trigger handler
public class LeadTriggerHandler {

    public void onAfterUpdate(Lead[] leads, map<ID,Lead> oldLeads) {
        for (Lead l: leads) { 
            Lead oldLead = oldLeads.get(l.Id);
            if (l.Company != oldLead.Company) {
                System.debug(LoggingLevel.INFO,'company has changed from ' + oldLead.Company   + 
                             'to ' + l.Company + ' .. request an @future to dowork');
                doCallout(l.Company);
            }
        }
    }
      
    @future
    static void doCallout(String company) {
        System.debug(LoggingLevel.INFO,'future method to do callout for ' + company);
        // .. callout details not important
    }
}
Workflow
  • Evaluation Criteria: Evaluate the rule when a record is created, and any time it’s edited to subsequently meet criteria
  • Rule Criteria: If Lead.Company contains ‘Changed’
  • Action: Field update Lead.Mobile to ‘650-555-1212’
Anonymous apex to demonstrate
Lead[] leads = new list<Lead> {
    new Lead(Company = 'Foo00', LastName = 'LName00'),
    new Lead(Company = 'Foo01', LastName = 'LName01')
    };

insert leads;
leads[0].Company = 'Foo00Changed';
leads[1].Company = 'Foo01Changed';
update leads; // force future to execute in handler
Debug log(s)

Workflow - Recursion - Trigger - Callout Ex 1

Ack! the future fired four(4) times! We should only have had two (2) as we updated only two records.

|USER_INFO|[EXTERNAL]|00540000000wbFS|cropredy@gmail.com|Pacific Standard Time|GMT-08:00
|EXECUTION_STARTED
//  lead inserted -- details omitted ...
//	Lead update event Lead.Company value changes
DML_BEGIN|[9]|Op:Update|Type:Lead|Rows:2
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]
USER_DEBUG|[7]|INFO|company has changed from Foo00to Foo00Changed .. request an @future to dowork
USER_DEBUG|[7]|INFO|company has changed from Foo01to Foo01Changed .. request an @future to dowork
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]
CODE_UNIT_STARTED|[EXTERNAL]|Workflow:Lead
WF_RULE_EVAL_BEGIN|Assignment
WF_SPOOL_ACTION_BEGIN|Assignment
WF_ACTION|.
WF_RULE_EVAL_END
WF_RULE_EVAL_BEGIN|Workflow
WF_CRITERIA_BEGIN|[Lead: LName00 00Q1W00001Jh9VR]|onUpdate - Set Field|01Q1W000000RGk3|ON_CREATE_OR_TRIGGERING_UPDATE|0
WF_RULE_FILTER|[Lead : Company contains Changed]
WF_RULE_EVAL_VALUE|Foo00Changed
WF_CRITERIA_END|true
WF_CRITERIA_BEGIN|[Lead: LName01 00Q1W00001Jh9VS]|onUpdate - Set Field|01Q1W000000RGk3|ON_CREATE_OR_TRIGGERING_UPDATE|0
WF_RULE_FILTER|[Lead : Company contains Changed]
WF_RULE_EVAL_VALUE|Foo01Changed
WF_CRITERIA_END|true
WF_SPOOL_ACTION_BEGIN|Workflow
WF_FIELD_UPDATE|[Lead: LName00 00Q1W00001Jh9VR]|Field:Lead: Mobile|Value:650-555-1212|Id=04Y1W000000PfJV|CurrentRule:onUpdate - Set Field (Id=01Q1W000000RGk3)
WF_FIELD_UPDATE|[Lead: LName01 00Q1W00001Jh9VS]|Field:Lead: Mobile|Value:650-555-1212|Id=04Y1W000000PfJV|CurrentRule:onUpdate - Set Field (Id=01Q1W000000RGk3)

 // Workflow updates the Leads with Field Update
WF_ACTION| Field Update: 2; 
WF_RULE_EVAL_END

// before/after triggers on Lead re-fire (expected)
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]

// uh-oh, Trigger.old has values prior to the initial update DML, 
// not the values as of the after update conclusion 
USER_DEBUG|[7]|INFO|company has changed from Foo00to Foo00Changed .. request an @future to dowork
USER_DEBUG|[7]|INFO|company has changed from Foo01to Foo01Changed .. request an @future to dowork
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9VR, 00Q1W00001Jh9VS]
WF_ACTIONS_END| Field Update: 2;
CODE_UNIT_FINISHED|Workflow:Lead
DML_END|[9]
CODE_UNIT_FINISHED|execute_anonymous_apex
EXECUTION_FINISHED

Solution 1 (sounds good)

Just add a static recursion control variable to your handler

public class LeadTriggerHandler {
    static set<ID> leadIdsAlreadySentToFuture = new set<ID>(); // recursion control
    public void onAfterUpdate(Lead[] leads, map<ID,Lead> oldLeads) {
        for (Lead l: leads) { 
            Lead oldLead = oldLeads.get(l.Id);
            if (l.Company != oldLead.Company && 
                !leadIdsAlreadySentToFuture.contains(l.Id)) { // have we already done this?
                System.debug(LoggingLevel.INFO,'company has changed from ' + oldLead.Company   + 
                             'to ' + l.Company + ' .. request an @future to dowork');
                doCallout(l.Company);
                leadIdsAlreadySentToFuture.add(l.Id);    
            }
        }
    }
      
    @future
    static void doCallout(String company) {
        System.debug(LoggingLevel.INFO,'future method to do callout for ' + company);
        // .. callout details not important
    }
}

This works as the debug log shows the future being called twice, once per Lead updated:
Workflow - Recursion - Trigger - Callout Ex 2

So, can I now work on my next JIRA ticket? Sorry ….

What if your Trigger/Handler is also invoked in a use case where partial success is allowed and one or more of the records fails to validate? AllOrNone = false can happen in many common use cases:

  • Data Loader
  • Any use of Apex Database.update(records,false); True also for the other Database.xxx methods.
  • Bulk, SOAP, or REST APIs that either default AllOrNone to false or set explicitly if available.

Here, we run into another little-known SFDC feature of trigger retries in the AllOrNone = false (i.e. partial successes allowed) use case. This is documented in the Apex guide as:

AllOrNone doc

Going back to the Triggers and Order of Execution, there’s one last tidbit as to why you can’t use static variables for recursion control in an AllOrNone = false use case:

When a DML call is made with partial success allowed, more than one attempt can be made to save the successful records if the initial attempt results in errors for some records. For example, an error can occur for a record when a user-validation rule fails. Triggers are fired during the first attempt and are fired again during subsequent attempts. Because these trigger invocations are part of the same transaction, static class variables that are accessed by the trigger aren’t reset. DML calls allow partial success when you set the allOrNone parameter of a Database DML method to false or when you call the SOAP API with default settings. For more details, see Bulk DML Exception Handling.

So, if you do a bulk update of two records, and one fails the validation rule, the static recursion control variable will be set on the first attempt, any @future calls are rolled back, and, when SFDC makes the second attempt on the non-failed record, the recursion control prevents the callout attempt from even happening so you end up with no callouts!

Let’s look at a proof:

Add a validation rule:

Website = 'www.failme.com'

Execute this code:
Lead[] leads = new list<Lead> {
    new Lead(Company = 'Foo00', LastName = 'LName00'),
    new Lead(Company = 'Foo01', LastName = 'LName01')
    };

insert leads;

leads[0].Company = 'Foo00Changed';
leads[1].Company = 'Foo01Changed';
leads[1].Website = 'www.failme.com';  // force partial success by failing this in VR
Database.SaveResult[] results = Database.update(leads,false); // allow partial success
Get this debug log

Workflow - Recursion - Trigger - Callout Ex 3

No future logs! Future never happened!

CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
// 1st time trigger is executed - both Leads passed:
DML_BEGIN|[11]|Op:Update|Type:Lead|Rows:2
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv, 00Q1W00001Jh9Vw]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv, 00Q1W00001Jh9Vw]
CODE_UNIT_STARTED|[EXTERNAL]|Validation:Lead:00Q1W00001Jh9Vv

// Validation rules execute
VALIDATION_RULE|03d1W000000Tdvy|Coerce_failure
VALIDATION_FORMULA|Website = 'www.failme.com'|Website=null
VALIDATION_PASS
CODE_UNIT_FINISHED|Validation:Lead:00Q1W00001Jh9Vv
CODE_UNIT_STARTED|[EXTERNAL]|Validation:Lead:00Q1W00001Jh9Vw
VALIDATION_RULE|03d1W000000Tdvy|Coerce_failure
VALIDATION_FORMULA|Website = 'www.failme.com'|Website=www.failme.com
// Fail the second Lead
VALIDATION_FAIL
CODE_UNIT_FINISHED|Validation:Lead:00Q1W00001Jh9Vw

// After update sees only the first, successful, Lead; future requested, static vbl set
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9Vv]
USER_DEBUG|[8]|INFO|company has changed from Foo00to Foo00Changed .. request an @future to dowork
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9Vv]
CODE_UNIT_STARTED|[EXTERNAL]|Workflow:Lead

// Workflow executes , causes field update on first lead
WF_RULE_EVAL_BEGIN|Workflow
WF_CRITERIA_BEGIN|[Lead: LName00 00Q1W00001Jh9Vv]|onUpdate - Set Field|01Q1W000000RGk3|ON_CREATE_OR_TRIGGERING_UPDATE|0
WF_RULE_FILTER|[Lead : Company contains Changed]
WF_RULE_EVAL_VALUE|Foo00Changed
WF_CRITERIA_END|true
WF_SPOOL_ACTION_BEGIN|Workflow
WF_FIELD_UPDATE|[Lead: LName00 00Q1W00001Jh9Vv]|Field:Lead: Mobile|Value:650-555-1212|Id=04Y1W000000PfJV|CurrentRule:onUpdate - Set Field (Id=01Q1W000000RGk3)
WF_ACTION| Field Update: 1;
WF_RULE_EVAL_END

// WF field update causes after trigger to re-execute (as expected)
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv]
// after trigger is NOP as recursion vbl says do nothing
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9Vv]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9Vv]
WF_ACTIONS_END| Field Update: 1;
CODE_UNIT_FINISHED|Workflow:Lead


// SFDC retries the first record because AllOrNone=false; governor limits reset
//	But static variables are not reset
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv]
CODE_UNIT_STARTED|[EXTERNAL]|Validation:Lead:00Q1W00001Jh9Vv

// WF fires again, updates the first Lead but no callout done  as recursion vbl prevents
WF_RULE_EVAL_BEGIN|Workflow
WF_CRITERIA_BEGIN|[Lead: LName00 00Q1W00001Jh9Vv]|onUpdate - Set Field|01Q1W000000RGk3|ON_CREATE_OR_TRIGGERING_UPDATE|0
WF_RULE_FILTER|[Lead : Company contains Changed]
WF_RULE_EVAL_VALUE|Foo00Changed
WF_CRITERIA_END|true
WF_SPOOL_ACTION_BEGIN|Workflow
WF_FIELD_UPDATE|[Lead: LName00 00Q1W00001Jh9Vv]|Field:Lead: Mobile|Value:650-555-1212|Id=04Y1W000000PfJV|CurrentRule:onUpdate - Set Field (Id=01Q1W000000RGk3)
WF_ACTION| Field Update: 1;
WF_RULE_EVAL_END
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event BeforeUpdate for [00Q1W00001Jh9Vv]

// no callout request made in retry of first record
CODE_UNIT_STARTED|[EXTERNAL]|01q1W000000Tdah|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9Vv]
CODE_UNIT_FINISHED|LeadTrigger on Lead trigger event AfterUpdate for [00Q1W00001Jh9Vv]
WF_ACTIONS_END| Field Update: 1;
CODE_UNIT_FINISHED|Workflow:Lead
DML_END|[11]
CODE_UNIT_FINISHED|execute_anonymous_apex
EXECUTION_FINISHED

So now what?

If we take the recursion static variable away, then the AllOrNone use case will still not pass – the future will get called twice on the successful record and never on the failed record.

... after the VR fails record[1] and before the WF executes 
USER_DEBUG|[8]|INFO|company has changed from Foo00 to Foo00Changed .. request an @future to dowork
... after the WF updates record[0] .. our original issue
USER_DEBUG|[8]|INFO|company has changed from Foo00to Foo00Changed .. request an @future to dowork
.. SFDC retries the successful records in trigger.new; skips the failed ones
.. trigger re-executes as if none of the above ever happened
USER_DEBUG|[8]|INFO|company has changed from Foo00 to Foo00Changed .. request an @future to dowork
... after the WF updates record[0] .. our original issue
USER_DEBUG|[8]|INFO|company has changed from Foo00to Foo00Changed .. request an @future to dowork

Workflow - Recursion - Trigger - Callout Ex 4

Clearly, static variables can’t be used to control redundant callouts when workflows and AllOrNone = false are combined in the same use case.

Solution 2 (better, but fragile)

    Go through your workflows that could update the Object where you are testing for make-a-callout conditions.

  • Move the field updates out and put them in the before insert/update triggers.
  • This way, the workflow will never force the trigger to re-execute with the original, start-of-transaction state of Trigger.old
  • Hence, your doCallout logic will only execute once in the transaction

This is fragile because you or some colleague could add at some later time a new workflow+field update that causes the trigger’s callout-evaluating condition to be re-assessed and you’ll be making duplicate callouts again.

Solution 3 – what could it be?

Clearly, you need to have state that:

  1. Persists across the trigger – workflow field update – trigger sequence
  2. Is rolled back when SFDC retries in the AllorNone = false (partial success) use case

We’ve seen that static variables won’t work. Platform cache would not work as it isn’t rolled back in the AllOrNone = false scenario.

The only thing that meets both criteria would be Sobject fields. A general approach …

  1. Trigger sets the field in Trigger.new (or updates some other Sobject with a known key)
  2. Workflow field update is made, trigger re-executes. Using the values in trigger.new, looks to see if the work was already done and if yes, avoids asking to do it again
  3. If trigger is running in a AllOrNone = false use case, and a record fails in the batch, the update made in step 1 is rolled back by SFDC. Thus, the trigger re-requests the work, persists the request in an sobject, and even though the workflow will re-fire, the persisted sobject can be inspected on the second trigger re-fire and the dowork request skipped

Now, what would you save in that sobject field?

  1. Initially, I was tempted to save the sessionId (encrypted as a MAC digest) as a pseudo signal that the callout request was made. As an sobject, it would be rolled back in the AllOrNone-false with error use case. But, when the Bulk API is used, there is no sessionId — it is null.
  2. Next idea was to exploit lastModifiedDate and lastModifiedById and compare the running user and current time to see if the trigger is being used in the trigger-workflow+field update-trigger use case as a sort of pseudo state variable. This seems problematic for several reasons like time skew and concurrent transactions coming from the same user
  3. Another idea was an unconditional workflow field update to set a field called Is_WF_Update_in_Progress__c. Thus, the second time through the trigger, the code would inspect the Is_WF_Update_in_Progress__c in Trigger.new, say, “ha”, I’m being called in a workflow field update-induced trigger invocation and bypass the request to do a callout. But, then the new field would have to be cleared (another DML) and, we’d be unnecessarily forcing the trigger to re-execute even if no other workflow’s rule criteria were satisfied. This slows down performance. This is complicated and doesn’t scale well. Every SObjectType needs its own clone of this if involved in workflows + triggers that compare trigger.old to trigger.new

A Workable, albeit a bit heavyweight solution

Create a Custom Object Transaction_State__c

Three relevant fields:

  • Name – This will be the transactionId
  • SObjectId__c (String) – Some Id visited in the transaction
  • Context__c – (String) – Some text that indicating the context for considering whether the SObjectId has been already visited this transaction
    1. Create a Transaction Service class
      public class TransactionService {
           // Although static, we don't care if this isn't rolled back on AllOrNone retry as it is not SobjectId-based
           static String txnId {
              get {
                  if (txnId == null) {
                      txnId = String.valueOf(System.currentTimeMillis()) + '_' + UserInfo.getName();
                  }
                  return txnId;
              }
          }
          
          public static set<ID> getVisitedIdsThisContext(String context, set<ID> proposedIds) {
              set<ID> unvisitedIds = new set<ID> (proposedIds);
              set<ID> visitedIds = new set<ID>();
              //	Get the complement of proposedIds and IDs we've already seen in some prior call this Txn
              //	Example: previous calls had us visiting  IDs 1, 3, 8
              //	Proposed Ids to see if "new" are 0, 3, 5
              //	Should return 0, 5 and also record 0, 5 in database in case trigger called a third time
              for (Transaction_State__c  txnState : [select ID, Name, SObjectId__c, Context__c from Transaction_State__c
                                                 where Name = :txnId and 
                                                 Context__c = :context and
                                                     SObjectId__c IN: proposedIds]) {
      			visitedIds.add((ID)txnState.SobjectId__c) ;                                 
              }
              unvisitedIds.removeAll(visitedIds);
              Transaction_State__c[] newlyVisitedTxnStates = new list<Transaction_State__c>();         
              for (ID unvisitedId : unvisitedIds)    
                  newlyVisitedTxnStates.add(new Transaction_State__c(Name = txnId, Context__c = context, SObjectId__c = unvisitedId));
              insert newlyVisitedTxnStates;  // save that we've been to these ids in this context       
              return unvisitedIds;    
          }
      }
      
      Modify the triggerhandler code as follows
      public class LeadTriggerHandler {
          public void onAfterUpdate(Lead[] leads, map<ID,Lead> oldLeads) {
          
              set<ID> unvisitedIds = TransactionService.getVisitedIdsThisContext('LeadDoFuture',oldLeads.keySet());
              for (Lead l: leads) { 
                  Lead oldLead = oldLeads.get(l.Id);
                  if (!unvisitedIds.contains(l.Id) && l.Company != oldLead.Company)    {
                      System.debug(LoggingLevel.INFO,'company has changed from ' + oldLead.Company   + 
                                   'to ' + l.Company + ' .. request an @future to dowork');
                      doCallout(l.Company);
          
                  }
              }
          }
            
          @future
          static void doCallout(String company) {
              System.debug(LoggingLevel.INFO,'future method to do callout for ' + company);
              // .. callout details not important
          }
      }
      

      The triggerhandler asks the Transaction Service to get all unvisited Ids for some context scope. Behind the scenes, the TransactionService saves the Ids + context scope + TransactionId in the database, thus creating a persistent store for the AllOrNone = true use case and a rolback-able store for the AllOrNone = false use case.

      Now, if you run an AllOrNone = true use case
      Lead[] leads = new list<Lead> {
          new Lead(Company = 'Foo00', LastName = 'LName00'),
          new Lead(Company = 'Foo01', LastName = 'LName01')
          };
      
      insert leads;
      leads[0].Company = 'Foo00Changed';
      leads[1].Company = 'Foo01Changed';
      update leads; // force future to execute in handler
      

      You see the future is called twice, once per record

      Workflow - Recursion - Trigger - Callout Ex 6 - allorNone true success

      If you run in an AllOrNone = false use case
      Lead[] leads = new list<Lead> {
          new Lead(Company = 'Foo00', LastName = 'LName00'),
          new Lead(Company = 'Foo01', LastName = 'LName01')
          };
      
      insert leads;
      
      leads[0].Company = 'Foo00Changed';
      leads[1].Company = 'Foo01Changed';
      leads[1].Website = 'www.failme.com';  // force partial success by failing this in VR
      Database.SaveResult[] results = Database.update(leads,false); // allow partial success
      

      You see the future is only called once for the record that does not fail validation rules
      Workflow - Recursion - Trigger - Callout Ex 5 - allorNone false success