Handle Recursion in Opportunity Stage Updates

Updating field values based on business logic is one of the most common and powerful use cases for Apex triggers in Salesforce. But when these updates happen during the after update context, there’s a risk of recursion—a situation where a trigger causes itself to re-fire repeatedly. This can lead to infinite loops, governor limit exceptions, or system crashes.

This blog covers a clean solution to a real-world challenge: how to automatically update the Opportunity Description when the StageName changes to Closed Won or Closed Lost, while also preventing recursive trigger execution. This ensures data consistency, preserves performance, and keeps your automation logic safe and scalable.

The trigger runs after an Opportunity is updated and uses a simple but effective static Boolean flag to prevent the handler logic from running more than once during a single execution context.

🧠 Why This Trigger Is Important


In most sales processes, the Opportunity Stage drives key decisions and reporting. Updating the Description automatically gives better visibility into the current state of the deal, helping sales reps, managers, and leadership teams stay aligned. Instead of relying on manual data entry, this automation ensures that the Description field reflects whether the deal was Closed Won or Closed Lost, providing context at a glance.

The bigger issue arises when trying to update records from inside an after update trigger. If you don’t prevent recursion, that update can call the same trigger again, which may lead to a loop. That’s why this trigger uses a static variable to determine whether the logic has already been executed in the current transaction. If not, it proceeds. If yes, it skips—simple and safe.

🔍 What This Blog Covers


  • How to write an Apex trigger that updates the Description based on Opportunity Stage

  • Why recursion is dangerous in after update triggers and how to avoid it

  • How to use a static Boolean variable to control execution

  • Benefits of using a trigger handler class for clean code structure

  • Real-world examples where this logic improves user experience and reporting

This trigger uses a straightforward handler pattern and follows Salesforce best practices for scalability and maintainability.

🎯 Real-World Use Cases for This Trigger


  • Sales teams that want to automatically log key milestones in the Description field

  • Account managers using Description fields for deal tracking in list views or dashboards

  • Sales leadership wanting consistent messaging across Opportunities

  • Organizations preparing data for analytics or AI-based deal insights

  • Admins and developers looking to enforce consistent behavior across large Opportunity volumes

This automation improves visibility, reduces errors, and ensures every Closed Won or Closed Lost Opportunity carries the right context—without relying on reps to type notes manually.

👨‍💻 Developer & Admin Tips


This trigger runs in the after update context and performs the following steps:

  • Checks if the trigger is being executed for the first time in the current transaction

  • Compares old and new values of StageName

  • If the Stage is either Closed Won or Closed Lost, it sets a corresponding message in the Description field

  • Uses a static Boolean flag from a helper class to block repeated execution

  • Performs all updates in bulk, ensuring governor limits are respected

This approach avoids recursion and maintains data quality without hitting platform limits.

You can extend this pattern by:

  • Adding more logic based on other StageName values like “Negotiation” or “Proposal”

  • Logging old values to a custom object for audit trails

  • Sending notifications to managers or team members when a stage changes

  • Integrating this with flows or email alerts for Closed Won deals

Always make sure to test:

  • Updates from Closed Won to Closed Lost (and vice versa)

  • Updates from non-final stages to Closed stages

  • Bulk updates with 200 records to ensure the logic is bulk-safe

  • Recursive behavior by intentionally triggering updates within the same context (just to confirm prevention is working)

This pattern is highly reusable across objects like Case, Lead, or even custom objects where conditional updates are required.

🎥 Watch It in Action – YouTube Playlist


If you’re more of a visual learner, head over to the Salesforce Makes Sense YouTube playlist, where this exact trigger is broken down step-by-step. The video includes:

  • Trigger and handler setup

  • Explanation of recursion and how to avoid it

  • Testing scenarios in the developer console

  • Tips for expanding the logic to other use cases

The playlist is perfect for both beginners learning Apex and advanced devs cleaning up legacy logic.

Solution:

public class preventRecursion { public
           static Boolean firstCall=false;
}
trigger OpportunityTrigger on Opportunity(after
                 update){ if(Trigger.isUpdate){ if(Trigger.isAfter){
                 if(!preventRecursion.firstCall){
                 preventRecursion.firstCall=true;
             OpportunityTriggerHandler.updateStage(Trigger.New,Trigger.oldMap);
                 }
        }
}
             public class OpportunityTriggerHandler{ public static void
             updateStageRecursion(List<Opportunity>
             oppList,Map<Id,Oppotunity>oldMap){
             List<Opportunity> oppToBeUpdated= new List<Opportunity>();
             for(Opportunity opp: oppList){ if(opp.StageName==’Closed
             Won’||opp.StageName==’Closed
                          Lost’){
                                          Opportunity o= new Opportunity(id=opp.Id);
                                                                        if(opp.StageName==’Closed Won’){
                                                                                o.Description=’Opportunity is Closed Won’;
                                                                        }else if(opp.StageName==’Closed Lost’){
                                                                                 o.Description=’Opportunity is Closed Lost’;
                                                             }
                                          oppToBeUpdated.add(o);
                                         }
                                   }
                           if(!oppToBeUpdated.isEmpty()){
                           update oppToBeUpdated;
                      }
        }
}

Want to Apply As Content Writer?

Leave a Comment

Your email address will not be published. Required fields are marked *

Shopping Cart

Let's get you started!

Interested in writing Salesforce Content?

Fill in this form and we will get in touch with you :)