With Great Power Comes Great Responsibility
The ability to automate behavior is a key offering of the Salesforce platform.
Automated actions include creating, updating and deleting records in Salesforce, sending emails, and communicating with external systems. These actions can be triggered every time Salesforce record is changed (i.e. Opportunity is updated), on a one-off basis or on a regular schedule.
Salesforce offers several tools with these configuration abilities. Among them, it is recommended to use Flows for general purposes, and Apex code for complex processes (Process Builders and Workflows can be used as well, but they are being deprecated).
These tools allow the admin/developer to quickly and easily configure complex and impactful automated behavior. This necessitates that the developer use care when working with these tools.
Out of Control Automation
Perhaps the most important concept to be aware of is that automations can trigger each other and themselves. A Flow or Apex code can make a change that will cause another Flow or Apex Trigger to run, or even itself recursively.
As an example, say you have
- a Flow on Opportunity that updates a related Contact, and
- a Flow on Contact that update its Account,
updating an Opportunity will cause Flows on Opportunity, Contact and Account all to run.
(Furthermore, if Apex triggers were used instead, and there was yet another Apex Trigger on Account to update its Opportunities, the automation may run recursively until the transaction crashes (“maximum trigger depth exceeded” error), as Account updates Opportunity updates Contact updates Account etc. Flow will not have this issue because there is a built-in mechanism to prevent Flow recursion.)
While it is possible that it is the intended behavior of the developer that the Opportunity update should trigger the Contact and downstream Account update, it is often the case that the changes made to the Contact by the Opportunity Flow are not relevant to trigger the Contact Flow.
For example, if
- the Opportunity Flow updates a “Last Opportunity Date” on the Contact, and
- the Contact Flow updates the Account’s “Active Employee Count” (Contact Status = Active as criteria)
the Last Opportunity Date on the Contact being updated via the Opportunity Flow has no relevance to the Status of the Contact, and should not trigger any update on the Account.
Entry Conditions, the Best Solution
The most effective way to mitigate this issue is to place “entry conditions” at every point of automation
With flows and other “point and click” automation, Salesforce enables the user to easily set the conditions (e.g. Status has changed) only under which the automation will run. More on that here.
With code, the user needs to do a little bit of lifting themselves. A simple way to manage this: in every method responsible for automation, first filter the triggering records to those with relevant changes and proceed only with those. If no records meet the criteria, nothing happens.
A utility method to do this can look like this
/**
Accepts the list of records provided by the trigger and returns only those who have changes in the provided fields
@param triggerNew Trigger.new
@param triggerOldMap Trigger.oldMap
@param relevantFields List of api names of fields whose changes are used to determine relevancy of the record
**/
public static List<SObject> GetRecordsWithRelevantChanges(
List<SObject> triggerNew,
Map<Id, SObject> triggerOldMap,
List<String> relevantFields) {
List<SObject> relevantRecords = new List<SObject>();
for (SObject rec : triggerNew) {
Boolean isRelevant = false;
for (String field : relevantFields) {
if (rec.get(field) != triggerOldMap.get(rec.Id).get(field)) {
isRelevant = true;
break;
}
}
if (isRelevant) {
relevantRecords.add(rec);
}
}
return relevantRecords;
}
and used like so
public void updateActiveEmployeeCount(List<Contact> contacts, Map<Id, Contact> oldContactMap) {
List<Contact> relevantContacts = (List<Contact>)
Utils.GetRecordsWithRelevantChanges(contacts, oldContactMap, new List<String>{'Status__c'});
// proceed with relevantContacts
for (Contact ctc : relevantContacts) {
// ...
}
}
This simple discipline will solve a myriad of issues because it attacks the root cause of most performance issues in Salesforce, unconstrained downstream automation.
As it is likely the case that most automations (flows and code) only need to occur a fraction of the time a particular event is triggered (Opportunity update, in our example), limiting all automations to only run when relevant will greatly reduce total executions of these automations. This will accordingly reduce the resource (i.e. governor limit) usage and performance of any given Salesforce transaction (i.e. record change).
Consider also the multiplicative effect of these reductions. An optimization that prevents an automation from running also eliminates all downstream automations that would have occurred had the parent automation run.
Advantage Over Other Optimization Methods
This form of optimization is one of the safest with the least downside.
While it may be tempting to simply move any poor performing code or flows to be asynchronous (with code, future, queueable or batch, with flows, using a “Run Asynchronous” path), async has its downsides. They include:
- The action does not happen immediately at save
- If the operation errors, there is no built in mechanism to inform the end user. (Whereas a synchronous error will prevent the record from saving and inform the user of the error).
- It counts against the limit of total asynchronous executions (albeit a very generous limit).
- Higher chance of record locking issues.
- Asynchronous code triggering other asynchronous code is not allowed in certain contexts and will cause the transaction to fail if not managed properly.
Other forms of optimization like static flags to control code execution have downsides as well. It’s a more complicated argument, but the short version is that it may skip execution of code it should be running, thus reducing reliability.
New Tool Gives Insight and Direction
I created a tool that helps realize and act on these ideas.
It visually summarizes any specified transaction (for example, Opportunity update) in an indented tree format, so that downstream automations are indented beneath their parent automation and the *cumulative* limit usage (meaning, downstream automations included) is summarized at every node.
This gives visibility into the piggybacking of automation in your org and points you to the biggest troublemakers in terms of resource usage. And the solution, as stated above, is often only to *limit* when these automations are executed.
Be First to Comment