Replacing your Apex Triggers with Processes and Invocable Actions

Process Builder Icon

Triggers are great, they let you react to pretty much anything that happens to your records in Salesforce in also any way that you want.

There are a few things that are not so great about triggers though, the main two being:

  • You can’t control the order in which they execute.
  • Disabling triggers requires creating a Change Set, updating tests and deploying.

Both of these can be overcome by leveraging the power of Process Builder and the @InvocableMethod annotation to create Invocable Actions instead of using Triggers.

What are Invocable Actions?

Invocable Actions are Apex methods that can be called from Flows, Processes and are exposed through the REST API.

To create an Invocable Action you need to add the @InvocableMethod annotation to your method and away you go (OK, it’s not quite that simple, there are a few restrictions which are covered here).

An Example

The easiest way to explain anything is with an example, so here is one.

This example is going to be based around a fairly simple trigger for automatically assigning Entitlements to Cases (you may notice this is a cut down and tidied up version of the trigger in this help article):

The Code bit

trigger DefaultEntitlement on Case (before insert, before update) {
    Set<Id> accountIds = new Set<Id>();
    
    for (Case c : Trigger.new){
        if (c.EntitlementId == null && c.AccountId != null) {
            accountIds.add(c.AccountId);
        }
    }
    
    if(accountIds.size() > 0) {    
        List <Entitlement> entitlements = [
            SELECT StartDate, EndDate, AccountId, AssetId
            FROM Entitlement
            WHERE AccountId in :accountIds 
                AND StartDate <= TODAY 
                AND EndDate >= TODAY
        ];
        
        if(entitlements.size() > 0) {       
            Map<Id, Entitlement> entitlementsByAccountId = new Map<Id, Entitlement>();
            
            for(Entitlement e : entitlements) {
                entitlementsByAccountId.put(e.AccountId, e);
            }
            
            for(Case c : Trigger.new) {
                if(c.EntitlementId == null && c.AccountId != null) {
                    Entitlement e = entitlementsByAccountId.get(c.AccountId);
                    
                    if(e != null) {
                        c.EntitlementId = e.Id;
                        
                        if(c.AssetId == null && e.AssetId != null) {
                            c.AssetId = e.AssetId;
                        }
                    }
                }
            }
        }
    } 
}

What does this look like as an Invocable Action?
public class DefaultEntitlementAction {
    @InvocableMethod(label='Update Default Entitlements' description='Determines the default Entitlement for the Case and returns the updated Cases.')
    public static void updateDefaultEntitlements(List<Case> casesToUpdate) {
        List<Case> cases = [
            SELECT AccountId, EntitlementId, AssetId 
            FROM Case 
            WHERE Id IN:new Map<Id, Case>(casesToUpdate).keySet()
        ];
        
        Set<Id> accountIds = new Set<Id>();
            
        for (Case c : cases){
            if (c.EntitlementId == null && c.AccountId != null) {
                accountIds.add(c.AccountId);
            }
        }

        if(accountIds.size() > 0) {    
            List <Entitlement> entitlements = [
                SELECT StartDate, EndDate, AccountId, AssetId
                FROM Entitlement
                WHERE AccountId in :accountIds 
                    AND StartDate <= TODAY 
                    AND EndDate >= TODAY
            ];
            
            if(entitlements.size() > 0) {       
                Map<Id, Entitlement> entitlementsByAccountId = new Map<Id, Entitlement>();
                
                for(Entitlement e : entitlements) {
                    entitlementsByAccountId.put(e.AccountId, e);
                }
                
                for(Case c : cases){
                    if(c.EntitlementId == null && c.AccountId != null) {
                        Entitlement e = entitlementsByAccountId.get(c.AccountId);
                        
                        if(e != null) {
                            c.EntitlementId = e.Id;
                            
                            if(c.AssetId == null && e.AssetId != null) {
                                c.AssetId = e.AssetId;
                            }
                        }
                    }
                }
                
                update cases;
            }
        }
    }
}

As you can see, there are a couple of small differences, but for the most part everything is the same.
One of the changes is that we need to requery the Cases at the start of our method, there are a couple of reasons for this.

The first reason is that unlike Triggers, Invocable Actions don’t have all of the object’s fields available to them by default so we need to query to make sure that we have everything we need.

The second reason is that because Processes act like after Triggers, in that the records passing through them (i.e. the records in Trigger.new for triggers) are read-only. We need to requery to get non-read-only versions of our records so that we can change the field values.

The other main change is that we have to call update on our Cases to actually update them. As we’ve queried for the records we need to tell Salesforce to actually commit the changes to the database, unlike in a before trigger where that step is implicit for the records in Trigger.new.

The Declarative Bit

Now we have our Invocable Action we need to call it. In this case (badum tsh) we’re going to use the Process Builder to do this.

Create a new Process (call it whatever you like) and select Case as your object, and “when a record is created or edited” as your criteria for starting the Process.

Case object selected in Process Builder

Click the “Add Criteria” node, give your criteria a name and select “No criteriaโ€”just execute the actions!” as your Criteria for Executing Actions.

No Criteria node in Process Builder

Then click “Add Action” under Immediate Actions and select “Apex” as the Action Type, give your action a name and then click in the “Apex Class” dropdown, hopefully you should see your class listed there, so select it!

One final step before we’re done, click “Add Row” in the Set Apex Variables section, under the Field dropdown you should have one option called “casesToUpdate”, select this, then click the value field and choose “Select the Case record that started your process”.

Your final process should look something like this:

Update Default Entitlement node in Process Builder

Advantages

All of this messing about with Process Builder may seem like a pain compared to just writing a trigger, but there are a few important advantages to this approach.

The biggest one is that your administrator is now in full control of if your logic runs, there may be a point in time where you no longer want to assign Entitlements automatically, now your administrator has the power to disable this.

In addition to this you can split the different bits of functionality in your triggers into multiple Invocable Actions to give your administrators the power to disable individual bits of functionality and change the order in which they execute.

You could (and probably should) go even further with this. At the moment our process has no criteria and the Apex handles which records should be processed. We could hand even more control to our admins with the added benefit of vastly simplify the Apex code by moving the criteria into the process instead.

As an added bonus, you get a couple of things for free with Invocable Actions. They can be reused by your administrators in other Processes and Flows, but more excitingly from a development perspective – they are automatically exposed by the Salesforce REST API!

Limitations

It’s worth noting that there are a couple of limitations with taking this approach for all of your triggers.

The biggest limitation is that this approach does not work at all for delete and undelete triggers, the Process Builder can only run on record creation and updates, and I don’t see that changing any time soon.

The other limitation is that Processes execute quite late on in the Order of Execution. There are some scenarios (for example, building a compound key for a duplicate rule) that will still require a trigger as that is the only way to get them to execute at the right time.

These limitations shouldn’t discourage you using this approach where it does work though as it comes with a lot of advantages.

Summary

I’ve shown you how by using Invocable Actions and Process Builder instead of Triggers you can give power back to your administrators as well as getting some nice reusability for free.

There are a couple of limitations to the approach, but so long as your use-case falls outside of those you can get a lot of benefit for very little (or no) extra effort.

Next time you start to think about writing a Trigger, consider if it can be written as an Invocable Action instead. Future you will thank you for it.

Work with Desynit

Looking for exceptional, professional Salesforce support?

Our independent tech team has been servicing enterprise clients for over 15 years from our HQ in Bristol, UK. Let’s see how we can work together and get the most out of your Salesforce implementation.