Creating Invocable Actions for Processes and Flows

Why should I use invocable actions?

Processes are simple to use and give you good control over the criteria that will trigger whatever you want to do, whether that’s a record change or a platform event. But Processes are limited in scope to processing a single record, and you can only manipulate data within a formula—and you can’t save values for use later in a Process. Plus, you can’t delete a record from a Process.

Flows are a lot more capable for getting and saving records from different objects, and Screen Flows provide an easy “front-end” for gathering user input.  But dealing with loops is tedious, and if you need to manipulate data, you’re stuck doing it with some very convoluted formulas.

Invocable Apex lets you do more complex work than Processes and Flows can accomplish on their own. Think about replacing triggers with a Process: that gives the admin control over the process criteria, and you can separate the different trigger functions into separate actions.

For example, let’s say you want a build a process that sends an email to a customer that confirms an appointment time. You have your email template that you can access from a Process, but you need to plug in the appointment time that’s on a datetime field in the record. To put a DateTime field into plain text with the local timezone, you have to use a formula like this. 

TEXT(MONTH(DATEVALUE([Event].StartDateTime - 7/24))) & "/" & 
TEXT(DAY(DATEVALUE([Event].StartDateTime - 7/24))) & "/" &
TEXT(YEAR(DATEVALUE([Event].StartDateTime - 7/24 ))) & ' ' &​

IF(VALUE(MID(TEXT([Event].StartDateTime - 7/24),12, 2)) > 12,
TEXT((VALUE(MID(TEXT([Event].StartDateTime - 7/24 ), 12, 2)))-12) & ':' &​
MID(TEXT([Event].StartDateTime - 7/24 ), 15, 2) & ' PM',​

IF(VALUE(MID(TEXT([Event].StartDateTime - 7/24),12, 2)) < 12,
MID(TEXT([Event].StartDateTime - 7/24 ),12, 2) & ':' &​
MID(TEXT([Event].StartDateTime - 7/24 ),15, 2) & ' AM',​

MID(TEXT([Event].StartDateTime - 7/24 ),12, 2) & ':' &​
MID(TEXT([Event].StartDateTime - 7/24 ),15, 2) & ' PM')) ​

 In contrast, Apex code can accomplish the same thing in only one line of logic:

return gmtDateTime.format();


Let's set up a simple automation so that when we create a new Opportunity record, a task is created on that record that shows the createdDate field, a dateTime field, but formatted into a string that displays in the local format and timezone. The automation starts with a Process that will be triggered when the new Opportunity record is created. The action will be to call a Flow, because we’ll be getting the date time string back from Apex and we’ll need to be able to store it. (Processes can't save a return value, but Flows can.)

A screenshot of a social media post

Description generated with very high confidence

There are no criteria for this process, and it immediately calls an autolaunched Flow called "Create Task with Local DateTime," passing along the Opportunity record for the Flow to use. (You'll actually need to create the Flow first, so that it is available to select in the Process.)

The Flow is very simple; it simply calls the invocable Apex Action that we will create in the next step, then it uses the returned dateTime string and the data from the Opportunity record to create the task.

You'll need to create an input variable of Opportunity record-type, and set it Available for Input. Next, drag an Action onto the canvas, select Get Local DateTime String as the Apex action, then set it so that you send the Opportunity.CreatedDate as the datetime parameter. NOTE: you'll have to create the Invocable Apex Action first, so that it is available to the flow.

Then, we just create a Task record tied to this Opportunity. 

A screenshot of a cell phone

Description generated with very high confidence

There’s a Formula that drops the locally formatted string that we got from Apex into the description field.


Now comes the fun part: writing the invocable Apex class!

// declare the class as public or global so that it's visible to Flows
public class LocalDateTime {
    // Declare the invocable method and provide a label that appears in the Flow.
    // The first method accepts List<DateTime> and returns List<String>.
    @InvocableMethod(label='Get Local DateTime String')
    public static List<String> localDateTimeBatch(List<DateTime> datetimes){
        // Declare the variable to hold the values we will return to the Flow.
        List<String> localDateTimes = new List<String>();
        // Loop through the list, call getLocalDatetime for each,
        // and add the result to our localDateTimes list.
        for (DateTime gmtDatetime : datetimes){
        // Return the values to the Flow.
        return localDateTimes;
    public static String getLocalDateTime(DateTime gmtDateTime){
        // This is the complete logic that transforms datetime to string
        return gmtDateTime.format();

This is the class, called LocalDateTime. You have to declare it as a public or global class, so that it will be visible to the Flow.

The first method is called localDateTimeBatch, because Invocable Apex must be bulkified. That means instead of dealing with one value at a time, you write it to handle a list — even though in the flow, we’re only sending a single value. The purpose of this first method is to handle the list, call the logic (second method) for each value, then return the results.

Annotate localDateTimeBatch as an InvocableMethod. You can only have one invocable method in the class, and you can’t have any other annotations like @future or AuraEnabled. Provide a label — that’s what gets displayed in the Flow or Process.

Declare the method, indicating that we are going to receive a List<DateTime> and return a List<String> to the flow, converting a datetime that looks like this: 2019-12-05 02:11:50Z into something more legible, like this: 12/4/2019, 6:11 PM. That date and time is formatted in my local standard, but if you're in the EU or anywhere else, it will display in your own local format.

This for-loop steps through the list of datetimes that the flow passed to us, and for each one it calls our second method, getLocalDateTime, and adds the result to our list of localDateTimes. Then, in line 19, it returns the list of localDateTimes to the flow.

The second method is that single line of logic that converts a datetime to a locally formatted string. You might realize that we could have simply added this line of logic to the first method, and you're right, but this is a template for more complicated invocable methods, so it's good to establish this design where the first method is the invocable one, and all it does is unpack our list of incoming values, call the logic in a separate method, then return the results.

In our next blog post, we'll show a more complex invocable action that follows this same design template.

Note: this post is derived from a Dreamforce '19 presentation by Pat McClellan and Donald Bohrisch. Code and automation are downloadable as an unmanaged package here.