Calling Flow From CDC Events | Salesforce Developers Blog

When a user presses save in the user interface, a lot happens in Salesforce – validation rules kick in, triggers are executed, flows are run, assignment rules are executed, and much more. This can all add up to a lot of logic being executed while that user is waiting for the application to respond again. Typically, not all of that logic needs to be ran immediately and can be processed at a later time, increasing the performance of the transaction.

In this blog post, we’ll look at a way to offload some of that logic from the save event with a combination of clicks and code. This way, you’ll have the benefit of being able to build your processes quickly using Flow and your users will have more responsive interactions with Salesforce.

Decoupling the transaction

In order to decouple the transaction, there are a few capabilities of Salesforce that you need to get to know. Together, these can help you improve the user experience and save-time.

Change event Apex triggers

In the Summer ’19 release, Salesforce introduced Change event Apex triggers to the platform. While you may already be familiar with triggers, they allow you run Apex each time a record is inserted, updated, or deleted. Triggers are something we’ve already had for a long time in Salesforce. A standard trigger runs in the same transaction context that initiated it, which works well in most cases – but lengthy processes and integrations can lead to delays in the user interface. This is where change event Apex triggers come in. With change event Apex triggers, you’re not placing a trigger on the actual object (like Account), but on the Change Data Capture event that results from a change to that object.

Change Data Capture

Change Data Capture (CDC) events are similar to Platform Events in the way that they are sent and received. The main difference is that CDC events are fired each time data changes are made to standard or custom objects. Each CDC event contains information like the object type, the fields changed, and the user who made the changes. In Setup, you can specify which objects should publish CDC events. CDC Events are published asynchronously, which decouples them from the code running from the database transaction. This sets the groundwork for creating a faster save-time.

Flow

Salesforce Flow is the tool to automate functionality in the Salesforce platform. Flows can either be Screen Flows, guiding a user through a process in the user interface; or they can be an Autolaunched Flow, performing certain tasks in the platform without any user interaction. Autolaunched Flows can be started from a record change or a Platform Event. They can also be scheduled or called as a subflow by another flow. However, CDC Events cannot trigger an Autolaunched Flow. This does require a bit of code to bring it all together, so let’s take a look at how we can combine the low code power of Flow with these events and triggers.

Passing the event data into the flow

When a Change Data Capture event triggers, it passes a lot of information into an Apex class. The sObject created is an EventBus.ChangeEventHeader object that cannot pass this object to the flow as an input variable directly. The first step is to create an Apex class that can be used for an Apex-defined data type that we can use instead as its Flow input variable. That class is very straightforward, because it simply needs to have all the data elements that are available in the ChangeEventHeader class but now in a form that Flow can handle.

For this, we’ve created the CDCTriggerInput class in Apex. In addition to the header information, we also want to store the actual values of the changed fields. The best type of structure to hold that data is inside of a Map in Apex. Now this is something that the flow will not be able to directly use, but that’s something we’ll fix later with a helper component for the flow.

The class variables have the @AuraEnabled annotation to make sure that they’re visible in the flow later on.

public class CDCTriggerInput {

    public CDCTriggerInput()
    {
        this.changedFieldsValues = new Map<String, Object>();
    }
    
    @AuraEnabled public String entityName;
    @AuraEnabled public String changeType;
    @AuraEnabled public Long commitNumber;
    @AuraEnabled public Long commitTimeStamp;
    @AuraEnabled public String commitUser;
    @AuraEnabled public String changeOrigin;
    @AuraEnabled public Integer sequenceNumber;
    @AuraEnabled public String transactionKey;
    @AuraEnabled public List<String> recordIds;
    @AuraEnabled public List<String> changedFields;
    @AuraEnabled public Map<String, Object> changedFieldsValues;
    @AuraEnabled public List<String> diffFields;
    @AuraEnabled public List<String> nulledFields;
}

Starting the flow from the Apex trigger

In order to start the flow from this trigger, there are a few things that we must do first:

  1. Copy the event header data to the CDCTriggerInput class
  2. Copy the changed field values to the CDCTriggerInput class
  3. Pass the inputs to the right flow

To do this, you can create a generic Apex class. In this case, we’ll use the InitateFlowFromCDC class. This class only has one responsibility: running the flow. As for input for the method, we pass in the API name of the flow, the Change Event Header Object, and the sObject. The code looks as follows:

public class InitiateFlowFromCDC {

    public void runFlow(String flowName, EventBus.ChangeEventHeader eventData, SObject event)
    {
        // Instantiate the ChangeEventHeader object
        CDCTriggerInput triggerInput = new CDCTriggerInput();

        // Assign the values from the ChangeEventHeader to the TriggerInput
        triggerInput.entityName = eventData.entityName;
        triggerInput.changeType = eventData.changeType;
        triggerInput.commitNumber = eventData.commitNumber;
        triggerInput.commitTimestamp = eventData.commitTimestamp;
        triggerInput.commitUser = eventData.commitUser;
        triggerInput.changeOrigin = eventData.changeOrigin;
        triggerInput.sequenceNumber = eventData.sequenceNumber;
        triggerInput.transactionKey = eventData.transactionKey;
        triggerInput.recordIds = eventData.recordIds;
        triggerInput.changedFields = eventData.changedFields;
        triggerInput.diffFields = eventData.diffFields;
        triggerInput.nulledFields = eventData.nulledFields;
           
        // Iterate over the changed fields and add them to the Changed Fields Map   
        for(String fieldName : eventData.changedfields){
            triggerInput.changedFieldsValues.put(fieldname, event.get(fieldName));
        }
        
        // Add the Trigger input to the inputs Map
        Map<String, Object> inputs = new Map<String, Object>();
        inputs.put('TriggerInput', triggerInput);
          
        // Instantiate the Flow and pass in the inputs for use 
        Flow.Interview myFlow = Flow.Interview.createInterview(flowName, inputs);
        myFlow.start();
    }
}

Writing the Change Data Capture event Apex trigger

Now that we’ve created the above classes. we can enable CDC on an object and then create a trigger to start the process. In this following example we use the Account object and create the following Apex trigger for the Account CDC object.

trigger AccountCDCTrigger on AccountChangeEvent (after insert) {

    InitiateFlowFromCDC flowCaller = new InitiateFlowFromCDC();
    
    for (AccountChangeEvent event : Trigger.New)
    {
        EventBus.ChangeEventHeader header = event.ChangeEventHeader;
        
        flowCaller.runFlow('The_Flow_That_Got_Called_From_CDC', header, event);    
    }
}

What we see in this trigger code is that we follow more or less the same structure as with any trigger. Do note that Event Triggers are always after insert. The trigger is on the event and not on the original data (an update of the original data leads to a new event that gets inserted telling you about the update).

Event triggers need to be bulkified, so you will have to assume that there is a list of events that was passed to the trigger. As you loop through these events, you take the ChangeEventHeader for each event and pass it to the InitiateFlowFromCDC class together with the API name of the flow that needs to get executed (in this example The_Flow_That_Got_Called_From_CDC) and the event itself.

Building your flow

Creating your Input parameter and using it

With the base code in place, the flow is relatively straightforward. The most important thing to note is that the flow needs to have an input variable of type Apex-Defined, using the CDCTriggerInput Apex class. The variable name should be TriggerInput as defined earlier.

Once we’ve defined the variable, the first thing you want to do is check which type of event has triggered the action. Was data inserted, updated, deleted or undeleted? We can do this by checking the changeType property of the TriggerInput variable.

The changeType can be of multiple values:

  • CREATE or GAP_CREATE indicates that a record has been created
  • UPDATE or GAP_UPDATE indicates that a record has been updated
  • DELETE indicates that a record has been deleted
  • UNDELETE indicates that a record has been undeleted

Next, you’ll also want to identify which record(s) triggered this event. As we mentioned, this could be triggered in bulk. You can use another variable in the flow to store the record Ids of any records passed in from the trigger. You want to make sure that you enable this to be stored as a collection just incase multiple records we’re passed in via a mass update. We will need to assign the recordIds from the TriggerInput into the new property and loop through them to properly assign them to the new property in the flow.

In a similar way, you can loop through the TriggerInput.changedFields list to determine which fields were changed in case of an an update – in all other use cases this value is empty.

Getting the changed field values

Getting the actual changed field values is bit more complicated. The field values are part of CDCTriggerInput structure which is stored in a map. Unfortunately, flows cannot handle maps so we must use the CDCTriggerInputHelper class that is called from the flow to get the values from the CDCTriggerInput variable.

When calling this class as an action in our flow we provide it two inputs, 
the CDCTriggerInput and the name of the field that we want the value of. 
For the output there are a number of output parameters available:

* FieldType, this will return the type of the field and can help you determine with of the other output parameters you may want to use. Possible values are: BOOLEAN, DATE, DATETIME, INTEGER, DOUBLE, LONG or STRING. FieldType is a String parameter.
* FieldValueString, if your FieldType is STRING then this parameter will hold the actual value and this parameter is of type String. This one is special, because one thing the helper class does is to make every type of field value as a string available in this output parameter. 
* FieldValueInteger, this is an output type INTEGER, in flow we’ll use a Number variable for this.
* FieldValueDouble, this is an output type DOUBLE, in flow we’ll use a Number variable for this.
* FieldValueLong, as you guess by now is linked to LONG and is again a Number variable.
* FieldValueDate, is a DATE type and linked to Date variables.
* FieldValueDatetime, is a DATETIME type and linked to Date/Time variables
* FieldValueBoolean, is the BOOLEAN type and can be stored in a Boolean variable.



global class CDCTriggerInputHelper {

    @InvocableMethod(label='Get Trigger Changed Field Value' description='Get the value for a changed field')
    global static List<CDCTriggerInputHelperResult> getTriggerChangedFieldValue(List<CDCTriggerInputHelperRequest> requests) {
        
        List<CDCTriggerInputHelperResult> returnValue = new List<CDCTriggerInputHelperResult>();
        
        for (CDCTriggerInputHelperRequest request : requests)
        {
            CDCTriggerInputHelperResult result = new CDCTriggerInputHelperResult();
               
            Schema.DescribeFieldResult dfr = Schema.describeSObjects(new String[]{request.triggerContext.entityName})[0].fields.getMap().get(request.fieldName).getDescribe(); 
            
            result.fieldType = dfr.getType().name();
            Object changedValue = request.triggerContext.changedFieldsValues.get(request.fieldName);

            switch on dfr.getType() {
                when Boolean {
                    result.fieldValueBoolean = (Boolean) changedValue;
                }
                when Date {
                    result.fieldValueDate = (Date) changedValue;
                    result.fieldValueString = String.valueOfGmt((Date) changedValue);
                }
                when DateTime {
                    result.fieldValueDateTime = (DateTime) changedValue;
                    result.fieldValueString = String.valueOfGmt((DateTime) changedValue);
                }
                when Double, Currency {
                    result.fieldValueDouble = (Double) changedValue;
                    result.fieldType = Schema.DisplayType.Double.name();
                }
                when Integer {
                    result.fieldValueInteger = (Integer) changedValue;
                }
                when Long {
                    result.fieldValueLong = (Long) changedValue;
                }
                when Else {
                    result.fieldValueString = (String) changedValue;
                    result.fieldType = Schema.DisplayType.String.name();
                }  
            }
            returnValue.add(result);
        }
        return returnValue;
    }
    
    global class CDCTriggerInputHelperRequest {
        @InvocableVariable
        global CDCTriggerInput triggerContext;
        
        @InvocableVariable
        global String fieldName;
    }
    
    global class CDCTriggerInputHelperResult {
            
        @InvocableVariable
        global String fieldType;
        
        @InvocableVariable
        global String fieldValueString;
        
        @InvocableVariable
        global Integer fieldValueInteger;
        
        @InvocableVariable
        global Double fieldValueDouble;
        
        @InvocableVariable
        global Long fieldValueLong;
        
        @InvocableVariable
        global Date fieldValueDate;
        
        @InvocableVariable
        global Datetime fieldValueDatetime;
            
        @InvocableVariable
        global Boolean fieldValueBoolean;
    }
}

Using this in Flow, you specify (as input) the CDCTriggerInputHelperRequest variable that you get from looping through the changed fields. Then have separate Flow variables available of the various types that can store the actual value. By checking the fieldType variable, you can find out which of the other variables will contain the actual value.

Once completed, the flow is actually quite simple!

You can see the first assignment step takes the recordIds and adds them to the to local variables. Next we loop through the recordids, and inside that loop we loop through all the changed fields for that record. We use the Get Trigger Field Value Apex Action to get the field value, and finally we call the Sync Account action that does the actual heavy work which was the reason for this solution in the first place.

Conclusion

In this blog post we’ve looked at how with some straightforward generic Apex classes we can make it possible to have flows run off Change Data Capture events, thereby decoupling them from the database transaction and not impacting the performance of the user in the user interface.

If you don’t have any experience with Change Data Capture yet, then have a look at the Change Data Capture Basics module on Trailhead.

And if you’re not yet familiar with Flow, then the ‘Automate Your Business Process with Lightning Flow’ trail on Trailhead is a great starting point.

About the author

Jack van Dijk is a Technical Architect working for Salesforce in the Netherlands. He has now more than four years experience in Salesforce and over 20 years in the CRM world. Jack can be reached at https://www.linkedin.com/in/jackvandijk/

Stay up to date with the latest news from the Salesforce Developers Blog

Subscribe