Stopping Apex Trigger Execution from Flows

You have an issue: your flow updates one or many records. When it updates those records, it triggers Apex that innocently racks up SOQL queries. Before you know it, you may have a “Too Many SOQL” error, or run into an instance where your trigger does or undoes some work you’ve done in your flow.

Now, in a perfect world, our triggers would have sufficient enough logical conditions such that we wouldn’t have to perform any workarounds, but sometimes, we just need a solution…

What if we could temporarily halt execution of apex triggers for the duration of a flow run?

We can! Firstly, let’s make sure we have a working “Trigger Framework” implemented in Apex.

What do I mean by that?

The way Salesforce implements triggers is by creating a special file and extension “.trigger”. These aren’t covered by tests, and behave differently to other apex.

One way to resolve the difference, is to have each trigger call a special “TriggerService” class that handles calling specific Apex classes for every record type. This way, you can maintain code coverage for your logic, and more importantly, implement a “trigger control” system.

So what does this look like?

Let’s start with a base ‘trigger helper’.

public interface TriggerHelper {
    Boolean isActive();
    void beforeInsert(List<SObject> newList);
    void beforeUpdate(Map<Id, SObject> oldMap, Map<Id, SObject> newMap);
    void beforeDelete(Map<Id, SObject> oldMap);
    void afterInsert(Map<Id, SObject> newMap);
    void afterUpdate(Map<Id, SObject> oldMap, Map<Id, SObject> newMap);
    void afterDelete(Map<Id, SObject> oldMap);
    void afterUndelete(Map<Id, SObject> newMap);
}

This is just an interface that other classes can inherit.

Now, let’s create a “TriggerControl” class

/**
* The TriggerControl class is a utility class that may be used to determine the state
* of various settings related to controlling custom triggers.
*/
public class TriggerControl {

    public static final String CONTACT = 'Contact';

    private TriggerControlHandler handler;

    @TestVisible
    private static TriggerControl instance;

    @TestVisible
    private TriggerControl(TriggerControlHandler h) {
        this.handler = h;
    }

    /**
     * Get the one and only instance of the TriggerControl.
     * @return the TriggerControl instance.
     */
    public static TriggerControl getInstance() {
        if (instance == null) {
            instance = new TriggerControl( new TriggerControlHandler() ) ;
        }
        return instance;
    }

    public Boolean isDisabled(String triggerName) {
        return this.handler.isDisabled(triggerName);
    }

    public void disableTrigger(String triggerName){
        this.handler.disableTrigger(triggerName);
    }

    public void enableTrigger(String triggerName){
        this.handler.enableTrigger(triggerName);
    }
}

This is a singleton class. It will only ever return one instance of a trigger control class. It will use this, along with a TriggerControlHandler to detect if you’ve enabled or disabled a trigger.

public virtual class TriggerControlHandler {

    private Map<String, Trigger_Control__mdt> triggerControlRecs;

    // list of triggers by string and whether the trigger is enabled
    private Map<String, Boolean> triggerControlFlags;

    public TriggerControlHandler(){
        this.triggerControlFlags = new Map<String, Boolean>();
    }

    /**
     * Determines whether or not the trigger is disabled.
     * @param triggerName The name of the trigger.
     * @return true if it is disabled; false otherwise. 
     */
    public Boolean isDisabled(String triggerName) {
        Boolean disabled = false;

        if(this.triggerControlFlags.containsKey(triggerName)){
            Boolean triggerFlagValue = this.triggerControlFlags.get(triggerName);

            // if set, set.
            disabled = triggerFlagValue;
        }

        return disabled;
    }

    // disable the trigger, put the key in for the trigger name along with "true"
    public void disableTrigger(String triggerName){
        this.triggerControlFlags.put(triggerName, true);
    }

    // enable the trigger, remove the key in the map that matches the trigger name, causing isDisabled to return false
    public void enableTrigger(String triggerName){
        if(this.triggerControlFlags.containsKey(triggerName)){
            this.triggerControlFlags.remove(triggerName);
        }
    }
}

This is where most of the meat of our application lives. It maintains a Map of type <String, Boolean>. This Map keeps track of the names of record types (e.g. Contact) and whether or not it’s active. It has a “isDisabled” method that returns true if disabled or false if not.

Since there’s one and only one instance of this singleton, anywhere we activate it or deactivate it will impact everywhere else in our code.

Finally, let’s create our “TriggerService” class. It is the final piece of the puzzle that will bring the entire framework together.

It has one method: execute. This will be handling the Execute calls from every trigger for every record type. Before it runs a “helper”, it will check TriggerControl to see if it’s disabled. If it is, it will skip running the trigger.

public inherited sharing class TriggerService {

    public static void execute(TriggerHelper helper, String triggerName) {
        if (isActive() && helper.isActive() && !TriggerControl.getInstance().isDisabled(triggerName)) {
            switch on Trigger.operationType {
                when BEFORE_INSERT {
                    helper.beforeInsert(Trigger.new);
                }
                when BEFORE_UPDATE {
                    helper.beforeUpdate(Trigger.oldMap, Trigger.newMap);
                }
                when BEFORE_DELETE {
                    helper.beforeDelete(Trigger.oldMap);
                }
                when AFTER_INSERT {
                    helper.afterInsert(Trigger.newMap);
                }
                when AFTER_UPDATE {
                    helper.afterUpdate(Trigger.oldMap, Trigger.newMap);
                }
                when AFTER_DELETE {
                    helper.afterDelete(Trigger.oldMap);
                }
                when AFTER_UNDELETE {
                    helper.afterUndelete(Trigger.newMap);
                }
            }
        }
    }
}

Now, let’s see what that looks like in a trigger:

trigger Contact on Contact (before insert, before update, after insert, after update) {
    TriggerService.execute(new ContactTriggerHelper(), TriggerControl.CONTACT);
}

Pretty simple!

And here’s our ContactTriggerHelper:

public inherited sharing class ContactTriggerHelper extends AbstractTriggerHelper {    
    public override void beforeInsert(List<SObject> newList) {
    }

    public override void beforeUpdate(Map<Id, SObject> oldMap, Map<Id, SObject> newMap) {
    }

    public override void afterUpdate(Map<Id,SObject> oldMap, Map<Id,SObject> newMap) {        
    }
}

This system ensures that any call to our ContactTriggerHelper will be wrapped by our TriggerService. Our TriggerService always calls out to our TriggerHandler before calling “execute” and therefore, we can control enabling or disabling through…

Drum roll

Another Apex class!

We want to do this through a flow, so this will be an invocable Apex class. Two of them

public class AllowTriggerHandler{
    /**
     * @description allowTrigger method can be called from PB/FLOWs to allow the object trigger for a transaction.
     * @param triggerNameToStop
    */
    @InvocableMethod(label='Allow sObject Trigger to execute' description='Allow the sObject trigger for this transaction')
    public static void allowTrigger(List<List<String>> triggerNameToAllow){
        
        // gets the single instance of the trigger control which calls to TriggerControlHandler
        for(String triggerName: triggerNameToAllow[0]){
            TriggerControl.getInstance().enableTrigger(triggerName);
        }
    }
}

This class is invocable and takes a list of trigger names to allow. It will call our singleton instance of TriggerControl and enable any trigger that we pass in.

One more:

public class StopTriggerHandler{
    /**
     * @description stopTrigger method can be called from PB/FLOWs to stop the object trigger for a transaction.
     * @param triggerNameToStop
    */
    @InvocableMethod(label='Stop sObject Trigger' description='Stop the sObject trigger transaction')
    public static void stopTrigger(List<List<String>> triggerNameToStop){
        
        // gets the single instance of the trigger control which calls to TriggerControlHandler
        for(String triggerName: triggerNameToStop[0]){
            TriggerControl.getInstance().disableTrigger(triggerName);
        }
    }
}

This class does the same thing as above, but calls to disableTrigger, instead.

OK – let’s tie this all together:

Let’s setup a new flow – it doesn’t matter on what. In it, create a new variable that allows multiple values:

Setup a variable to handle the records you want to trigger
Assign the record names to the variable (in our case, contact)
Add our Stop sObject Trigger to the flow
Our finished flow

This process ensures that you can update your records in the flow without having to worry about the downstream triggers.

Back