+ Start a Discussion
Kelly KKelly K 

Trigger running twice because of workflow

Hi All,

 

I have a trigger that creates a task for a set of criteria on a custom object called Implementation. Whenever a non system admin user enters in the Target Go Live Date on the implementation record, the Trainer listed on the Implementation record will have a task created for them. Currently, whenever this trigger fires for the after update portion, it creates two duplicate tasks. I've crawled through several debug logs and toggled things on and off and found that if I turn off one specific workflow rule, the duplication does not occur. 

 

Any thoughts or ideas on why this particular workflow is causing the trigger to run twice?

 

Here's the workflow: (updates the Target Go Live Month field when the Target Go Live Date field is changed)

- Evaluate the rule when a record is created, and every time it's edited

- Object: Implementation

ISCHANGED( Target_Go_Live_Date__c)

- Field update:

- Field to update: Implementation: Target Go Live Month

CASE( TEXT( MONTH( Target_Go_Live_Date__c)), 
"1", "01 - Jan", 
"2", "02 - Feb", 
"3", "03 - Mar", 
"4", "04 - Apr", 
"5", "05 - May", 
"6", "06 - Jun", 
"7", "07 - Jul", 
"8", "08 - Aug", 
"9", "09 - Sep", 
"10", "10 - Oct", 
"11", "11 - Nov", 
"12", "12 - Dec", 
"")

 

Here's the trigger:

trigger TargetGoLiveDateTrainerTask on Implementation_del__c (after insert, after update) {
  List<Task> tasks = new List<Task>();
  Id currentUserProfileId = UserInfo.getProfileId();
  Set<Id> implementationIds = new Set<Id>();
  Date taskDate = System.today();
  RecordType recordType = RecordTypeResolver.find('Task', 'Client Services');
  
  //Exclude all System Admins from creating this task
  if (new Set<Id> {'00e30000000dSKN'}.contains(currentUserProfileId))
    return;

  for (Integer i = 0; i < Trigger.size; i++) {

    if ((Trigger.isInsert || Trigger.old[i].Target_Go_Live_Date__c == null) &&
      (Trigger.new[i].Target_Go_Live_Date__c != null &&
       Trigger.new[i].Training_Required__c != 'No'))
        
      implementationIds.add(Trigger.new[i].Id);
  }
  
  if( implementationIds != null) {

    List<Implementation_del__c> implementations = new List<Implementation_del__c>([SELECT Id, Account__c, Trainer__c, Account__r.Name, Target_Go_Live_Date__c FROM Implementation_del__c WHERE Id IN :implementationIds]);
  
    for (Implementation_del__c implementation : implementations) {

      if (implementation.Trainer__c == null) {
        implementation.addError('Implementation has no Trainer set.  Please set a Trainer for the \'Training Kick Off Call\' Task to continue.  Contact an administrator for more information.');
      }

      else if (System.today().daysBetween(implementation.Target_Go_Live_Date__c) < 30) {
        tasks.add( new Task( Subject = 'Training Kick Off Call: ' + (implementation.Account__c != null ? implementation.Account__r.Name : '<Unknown>'),
                  OwnerId = implementation.Trainer__c,
                  ActivityDate = taskDate,
                   ReminderDateTime = System.today(),
                  IsReminderSet = True,
                  WhatId = implementation.Id,
                  RecordTypeId = recordType.Id ));
      }
      
      else {
        taskDate = implementation.Target_Go_Live_Date__c - 30;
        tasks.add( new Task( Subject = 'Training Kick Off Call: ' + (implementation.Account__c != null ? implementation.Account__r.Name : '<Unknown>'),
                 OwnerId = implementation.Trainer__c,
                 ActivityDate = taskDate,
                 ReminderDateTime = DateTime.newInstance(taskDate, Time.newInstance(8, 0, 0, 0)),
                 IsReminderSet = True,
                 WhatId = implementation.Id,
                 RecordTypeId = recordType.Id ));
      }
    }
    
    if(tasks != null)
      insert tasks;
  }

 

Andrew WilkinsonAndrew Wilkinson

Its part of the platform...the article below explains it. Triggers are fired one more time if there are updates from a workflow.

 

http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_triggers_order_of_execution.htm

Kelly KKelly K

Andrew,

 

I understand that an update will cause all of the triggers to re-fire. When they re-fire the 2nd time, if they don't match the criteria in the trigger, they shouldn't process the records a second time.

 

What I don't understand, is how that particular workflow, which updates a field that isn't part of the criteria for the trigger, causing the record to get included for update a 2nd time.

 

The field that's getting updated by the workflow is Target Go Live Month, where the trigger requires a change in the Target Go Live Date.

 

Is my criteria in the trigger set up incorrectly?

dipu2dipu2

You can solve your problem by creating a Class with static variable as a controller for the trigger. Class static variables are not persistent between round trips.

 

For example create a class named TriggerController with a static variable named processedRecordIds. Store the record ids processed since your trigger is afterUpdate you will have the id. Then you will be able to skip the unwanted update. 

 

Please post the solution for others benefit when your problem gets resolved. 

 

 

 

Andrew WilkinsonAndrew Wilkinson

Try this...I think the issue is coming from your index. Trigger.old and Trigger.New are not guaranteed to be the same size so the index could be off.

 

for (Implementation_del__c impl : Trigger.New) {
    
    if ((Trigger.oldMap.get(impl.Id) == null || Trigger.oldMap.get(impl.Id).Target_Go_Live_Date__c == null) &&
      (impl.Target_Go_Live_Date__c != null &&
       impl.Training_Required__c != 'No'))
        
      implementationIds.add(impl.Id);
  }

 

Kelly KKelly K

Andrew, I gave your suggestion a shot and I'm still getting duplication on the after Update portion.

 

I went ahead and tried dipu2's suggestion and that does work. I found an article that explains it well for newbies like myself to understand: http://techblog.appirio.com/2009/10/preventing-recursive-future-method.html

http://www.embracingthecloud.com/2010/07/08/ASimpleTriggerTemplateForSalesforce.aspx

 

Though the articles mention using an @future method, I decided to ignore that since a lot of our triggers are still separated out and the majority of them do not have classes.

 

I'm certain this is not the best way to do this, but it works:

 

Created a new class: TriggerHandler

public class TriggerHandler {
	public static boolean firstRun = true;	
}

 

Modified my trigger:

trigger TargetGoLiveDateTrainerTask on Implementation_del__c (after insert, after update) {
	List<Task> tasks = new List<Task>();
	Id currentUserProfileId = UserInfo.getProfileId();
	Set<Id> implementationIds = new Set<Id>();
	Date taskDate = System.today();
	RecordType recordType = RecordTypeResolver.find('Task', 'Client Services');

if(TriggerHandler.firstRun) {

		//Exclude all System Admins from creating this task
		if (new Set<Id> {'00e30000000dSKN'}.contains(currentUserProfileId))
			return;
	
	
		for (Integer i = 0; i < Trigger.size; i++) {
	
			if ((Trigger.isInsert || Trigger.old[i].Target_Go_Live_Date__c == null) &&
				(Trigger.new[i].Target_Go_Live_Date__c != null &&
				 Trigger.new[i].Training_Required__c != 'No')) {
				implementationIds.add(Trigger.new[i].Id);
			}
		}
		
	
		if( implementationIds != null) {
	
			List<Implementation_del__c> implementations = new List<Implementation_del__c>([SELECT Id, Account__c, Trainer__c, Account__r.Name, Target_Go_Live_Date__c FROM Implementation_del__c WHERE Id IN :implementationIds]);
		
			for (Implementation_del__c implementation : implementations) {
	
				if (implementation.Trainer__c == null) {
					implementation.addError('Implementation has no Trainer set.  Please set a Trainer for the \'Training Kick Off Call\' Task to continue.  Contact an administrator for more information.');
				}
	
				else if (System.today().daysBetween(implementation.Target_Go_Live_Date__c) < 30) {
					tasks.add( new Task( Subject = 'Training Kick Off Call: ' + (implementation.Account__c != null ? implementation.Account__r.Name : '<Unknown>'),
								 	 OwnerId = implementation.Trainer__c,
								 	 ActivityDate = taskDate,
								  	 ReminderDateTime = System.today(),
								 	 IsReminderSet = True,
								 	 WhatId = implementation.Id,
								 	 RecordTypeId = recordType.Id ));
				}
				
				else {
					taskDate = implementation.Target_Go_Live_Date__c - 30;
					tasks.add( new Task( Subject = 'Training Kick Off Call: ' + (implementation.Account__c != null ? implementation.Account__r.Name : '<Unknown>'),
									 OwnerId = implementation.Trainer__c,
									 ActivityDate = taskDate,
									 ReminderDateTime = DateTime.newInstance(taskDate, Time.newInstance(8, 0, 0, 0)),
									 IsReminderSet = True,
									 WhatId = implementation.Id,
									 RecordTypeId = recordType.Id ));
				}
			}
			
			if(tasks != null)
				insert tasks;
		}
		
		TriggerHandler.firstRun = false;
	}
}

 

JoyJobingJoyJobing

Thanks all for this thread, I had a quick question.  I used the above code in my trigger/class BUT for the test code it seems like the public boolean is getting changed and then keeping that value throughout the entirety of the test.

 

To get it to perform properly, I reset it back (inside my test code) after each DML statement.  Is this realistic or does this not follow what it would do 'in real world'?  Thanks for any help!

Kelly KKelly K

JoyJobing,

 

My experience is that each test method is considered one transaction rather than a new transaction for each DML statement. This also applies to governor limits. You can either reset the boolean in your test method, or break it out into several small test methods if possible. For our needs, it's generally better to have smaller test classes, thus restricting the number of DML statements in the method and avoiding psuedo-limits.

 

If someone else knows a better practice then feel free to weigh in!