Using Apex: Create Task for an Opportunity

Let me be straight for you SFDC configuration vs. coding fanatics – I am in your group. I prefer point & click configuration any day as compared to the complexity that Apex can become. The example here deals about creating a task due to my lack of imagination rather than anything else – it is just another problem that can be addressed using Apex.

In a previous post, you had seen different ways of creating a task when a field is updated in Opportunity. If you have directly landed on this page, that post will tell you that there are other (possibly better) ways of handling this job.

Now, let’s get down to business.

The problem itself is simple – create a task when an opportunity is lost.

What follows is a rant of how a typical developer would go about it. We are going to create a trigger, which enables you to create your own actions that can follow a salesforce data manipulation (update, insert, delete).

 

Version (-3): The simplest trigger

Create a simple trigger that will create a task when Opportunity Stage is updated.

trigger Lost_Opportunity_Follow_up on Opportunity (after insert, after update) {
    if (trigger.new[0].StageName == 'Closed Lost') {
        Task oTask = new Task(Subject='Lost Opportunity Follow-up', ActivityDate=Date.today()+10, WhatId=trigger.new[0].Id,OwnerId=trigger.new[0].OwnerId);
        insert oTask;
    }
}

Does the trigger do its job? You bet it will.

 

So, what is wrong?

The code considers trigger.new[0] – assumes that there is only one opportunity updated at any time. The Task is created only for the first Opportunity if there are multiple opportunity updates happening in the same transaction.

By now you also understand the wierd version, huh?

 

Version (-1): Trigger that considers multiple record updates with the right conditions

Improve the previous version with provision to accommodate multiple opportunities in one go.

trigger Lost_Opportunity_Follow_up on Opportunity (after insert, after update) {
    Opportunity[] oOptyList = new List<Opportunity>();
    oOptyList = [SELECT Id, OwnerId FROM Opportunity where Id IN :trigger.new AND StageName='Closed Lost'];
    
    for (Opportunity oOpty : oOptyList) {
        Task oTask = new Task(Subject='Lost Opportunity Follow-up', ActivityDate=Date.today()+10, WhatId=trigger.new[0].Id,OwnerId=trigger.new[0].OwnerId);
        insert oTask;
    }
}

 

What is wrong?

  • oOptyList is not really needed – it would have made sense to use the query context when an entity other than Opportunity was used.
  • If there are too many records to be updated in the transaction, you will run the risk of hitting the heap size limits.
  • Creation of new task continues to happen in a loop

 

Version (0): Simple trigger that considers multiple record updates

Account for bulk updates and streamline task creation.

Also, check whether the StageName is indeed updated. You should not created duplicate tasks when updates are carried out on a lost Opportunity.

 

trigger Lost_Opportunity_Follow_up on Opportunity (after insert, after update) {
	String sOptyId;
    Task[] oTasks = new List<Task>();
	Opportunity oOptyOld;
    
    for (Opportunity oOpty : trigger.new ) {
        sOptyId = oOpty.Id;
        oOptyOld = trigger.oldmap.get(sOptyId);
        if (oOpty.StageName == 'Closed Lost' && oOptyOld.StageName != 'Closed Lost'){
            oTasks.add(new Task(Subject='Lost Opportunity Follow up', ActivityDate=Date.today()+10, WhatId=sOptyId,OwnerId=oOpty.OwnerId));
        }
    }
    if (oTasks.size() >0 ) insert oTasks;
}

The for-loop ensures that 200 records are processed at a time. More importantly, the ‘insert task’ DML is placed outside the loop.

What is wrong?

  • You cannot really predict the sequence if there are multiple triggers
  • Recurrence due to updates in workflows or by other means can create duplicate tasks

 

Version (1): Use Trigger Classes!

All the goodness of the above with a standard pattern thrown in. I am not saying that this is the **only** way to promote reuse and standardize triggers, but is certainly one of the good ways to do that.

 

trigger COGOpportunityTriggerMainEntry on Opportunity (before insert, after insert, before update, after update) {
	
    COGOpportunityTriggerDispatcher.TriggerDispatch(
        Trigger.isExecuting, Trigger.isBefore, Trigger.isAfter, 
        Trigger.isInsert, Trigger.isUpdate, Trigger.isDelete,
        Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap);
}

Now, for the class.

public class COGOpportunityTriggerDispatcher {
    private static boolean isFirstRun = true;
           
    public static void TriggerDispatch(Boolean isExecuting, Boolean isBefore, Boolean isAfter, Boolean isInsert, Boolean isUpdate, Boolean isDelete, List<Opportunity> oldList, List<Opportunity> newList, Map<Id, Opportunity> oldMap, Map<Id, Opportunity> newMap){  
        try {
            system.debug('start: COGOpportunityTriggerDispatcher. isFirstRun: ' + isFirstRun);
            Map<ID, Task> insertTasks = new Map<ID, Task>();
            
            for (Opportunity oOpty : newList ) { //one loop to rule them all?
                 String sOptyId = oOpty.Id;
                 Opportunity oOptyOld = oldMap.get(sOptyId);
                
                system.debug('isExecuting ' +isExecuting);
                if (isExecuting) { //execute if called by trigger
                    system.debug('isAfter ' + isAfter);
                    if (isAfter) {   
                        if (oOpty.StageName == 'Closed Lost' && oOptyOld.StageName != 'Closed Lost'){
                        //do: insert follow up activities
                        system.debug('inserting task for ' + sOptyId);
                        insertTasks.put(sOptyId, new Task(Subject='Lost Opportunity Follow up - Super Trigger', ActivityDate=Date.today()+10, WhatId=sOptyId,OwnerId=oOpty.OwnerId));            
                    }
                  }
               }
            }
            
            if (insertTasks.size() > 0) insert insertTasks.values();
            
            if (isFirstRun) isFirstRun(false); //not used here, but has potential use cases for future
            system.debug('end: COGOpportunityTriggerDispatcher');
    }
        catch (Exception Ex) {
            throw(Ex);
        } 
    }
    
    //set isFirstRun variable
    public static void isFirstRun(Boolean isRun) {
        // you can call this function for finer control!
        COGOpportunityTriggerDispatcher.isFirstRun = isRun;
    }   
}

 

Of course, this is more complex than the earlier versions. But you can also see that –

  • code is modular and has high reusability outside the trigger context
  • it is easier to control what logic gets executed and when
  • easily test the classes
  • you can just manipulate the class and be 100% sure that the logic gets executed in the intended sequence

Also note that complexity of triggers grow over time. Although it is easier to write multiple triggers with 2-3 lines of code, that will present an overall spaghetti mess when trying to scale the application.

 

Version (2): Use Centralized Trigger Dispatch

The next version is to centralize the trigger dispatch for multiple objects.

That will come in later versions – one step at a time, amigo!