Undeleting Records with Flow

Created by Yumi Ibrahimzade


You can perform insert, update and delete operations in Salesforce Flow. Most of the time, they are enough but what if you want to undelete records from the recycle bin? There isn’t undelete records operation in Flow. However, you can achieve this using this Invocable Apex Action.

See this post for the main discussion of this new Flow capability.


Production or Developer Edition



recordIds – Enter a text collection of record Ids to undelete.


This Invocable Apex Action undeletes the records from the Recycle Bin. If the record is permanently deleted, which means that it is not in the Recycle Bin anymore, you cannot undelete it.


Add an Action element from the Toolbox and search for FlowUndeleteRecords. Pass the text collection of the record Ids to the input parameter called recordIds. Even if you want to pass a single record Id, you still need to create a text collection and pass it as the input value.

undelete records action and parameters

Read this post to learn how to find the deleted record Ids and see an example of undeleting records using this Invocable Apex Action.


From Tamar Erlich – Submit an orchestration from a button

Currently there are two ways you can launch a flow orchestration: using a record trigger, or auto launch from Apex, But what if I want to be able to launch an orchestration from a button and mimic the good old “Submit for Approval” button?
I looked at alternative ways to start orchestrations, and this is what I found:
1. Launching an orchestration from a URL button was not user friendly as the user was presented with a blank auto launched flow screen and was not redirected back to the record when finished even if the the return URL was part of the button definition. Overall, while this may work, the user experience was not smooth
2. Trying to include the submit flow as an interactive step assigned to the current user, resulted in a warning when saving the flow and an error during execution saying that no assigned user was found

I ended up with the following solution that worked well and gave a smooth user experience:

1. An action button to launch the submit flow on the record page
2. A submit screen flow that will update a field on the record to indicate the record was submitted and who the approver is
3. An orchestration that will listen to the field updated by the submit flow and will run the approval flow, routing to the approver that was updated on the record by the submit flow
4. The approval flow will update the record again with the final approval status

Here is an illustration of the overall process:

Both the submit and approve flows are very simple and only have one screen followed by an update records step

Submit Flow

Launch the submit flow from an action button added to the page layout

Action Button
Approval Flow

The flow orchestration only has one stage and one step

Orchestration Flow

It starts when the record’s approval status is updated to submitted

And routes the approval to the username that was updated on the record by the submit flow

Here is a short video demonstrating the complete process

Some notes about the flows:

1. The submit flow clears any previous approval status and rejection notes
2. The approval flow only displays rejection notes if the user selects to reject, using a visibility rule
3. In both flows, I’m using a record input variable and this saves having a get records element in the flow as the whole record gets passed in from the button or from the orchestration step

Possible enhancements:

1. The approval request flow can include a message with approval details to the approval request flow by using a display text with merge fields from the record and related records information
2. The submit flow can select the approver automatically by using criteria from the record or an approval matrix stored in custom metadata
3. The submit flow can also be used to recall or not allow to submit an approval by displaying another screen based on the approval status

Launching an orchestration from a button opens up a lot of possibilities, please share if you come up with other use cases.

Flow Orchestration Use Case – approve user provisioning

I was eager to test the new flow orchestrator and found a use case to try. When a user submits a case to be provisioned as a partner user, we need to process two manager approvals and then provision the contact and the user under the partner account. I built an orchestration to complete the approvals and then provision the user

I used the tutorials provided here by Alex to build this. The overall design uses three flows that share data using input and output variables.

My starting conditions were based on case creation

The manager approval is routed to the approving manager lookup field from the case

The supervisor approval is routed to a specified username

My approval flow is very simple and includes just one screen. It outputs the approval, and some user data from the case, that will be consumed by the provisioning flow. I used some interesting formulas to parse the supplied Web Name from the case to a first name and a last name, and since only variables can be designated for output, I’m assigning my formulas as the default value of the output variable

Here is how the approval request screen looks to the approving manager

Next, I created an autolaunched flow that evaluates both approvals and outputs the special “isOrchestrationConditionMet” boolean value that tells the next step to start. This flow also updated the final approval status on the case record.

Then my provisioning flow will start and show as a work item to the case owner

Here is how the provisioning screen looks to the user. The first name, last name, email, and account name, are populated from the approval flow and case record data.

In this screen I’m using the Quick Lookup flow screen component to dynamically filter the user role list based on the selected partner account

And after the provisioning is complete, the flow shows the success scree

I used some special formulas to set the alias, email, nickname, and username for the new user.
To avoid a mixed DML error, I used the Commit Transaction action between creating the contact and creating the user.

When the orchestration completed, I was able to view all the completed stages, and steps in the flow orchestration instance history

Overall, building this orchestration was fun and easy. I can see how powerful this tool can be for handling complex processes. I would like to be able send an approval request to non Salesforce users, that can approve by replying to the email, or by going to the work item component publicly exposed on a community page.
I’m looking forward to seeing how this tool will evolve and to all the enhancements that will be made.

From Yumi Ibrahimzade: Using Flow to Mass Delete Records from List View

Yumi Ibrahimzade from the blog Salesforce Time has written a post explaining how to use flow to add a mass delete button to a list view. Pay special attention to the second solution that avoids performing DML inside a loop by using a custom action that can be found in the collection actions here on UnofficialSF

Check it out

From SFDCPanther: Rich Text Area component for screen flow

Amit Singh from the blog has written a post explaining how to create a LWC that will enable a Rich Text Area component in flow screens
Check it out

Best Practices for Creating Invocable Methods: Using Inner Classes and Invocable Variables

I’ve long been a fan of using invocable methods with flows. Today I’ll use a real-life scenario to explain the importance of using invocable variables as well. We’ll start by looking at the anti-pattern of a standalone invocable method, then we’ll follow best practices and refactor the solution into an invocable method plus invocable variables.

The use case comes from a pro bono project for Playworks. Playworks is the leading national nonprofit leveraging the power of play to transform children’s social and emotional health. They’re a great organization, and they could definitely use some support.

Playworks tracks each school partnership with an opportunity record. A school might purchase 5 trainings, represented by opportunity product records. For financial purposes, the service completion date is tracked for each training, so each training needs its own opportunity product record. Manual entry can get cumbersome, with some opportunity records having 10 or more opportunity products associated with them.

To address this need, we created the Add Products Wizard. Built using flow, the wizard starts by presenting a user with a list of products to select from.

Note: The handy table above is made possible by a free LWC called the Welkin Data Table for Flows.

The user selects the products they want, then enters the quantity and sales price of each product they selected.

So far, so good, but things get tricky if you build an invocable method without invocable variables. Let’s take a look at what not to do, then we’ll refactor our solution to follow best practices.

First up, the anti-pattern. Here’s what the flow loop element looks like behind the scenes, designed for a standalone invocable method. After the user selects their products, the loop begins. The loop runs once for each product selected. The pattern is as follows:

  1. Get the quantity and sales price for each product from the user.
  2. Gather up all needed inputs for the invocable method into a flow text collection variable.
  3. Pass the flow text collection variable into the invocable method.
  4. The invocable method then creates the desired number of opportunity product records.
  5. Clear the values of the text collection variable so the loop can start over.

Then comes the messy part. The text collection variable holds 4 values.

The four values are added to the text collection variable in the following order:

  • First: Product Id
  • Second: Opportunity Id
  • Third: Sales Price
  • Fourth: Quantity

Imagine having to come in and explain what the opportunityProductInputs variable is holding. There is no way to know, without explicitly inspecting the assignment element that populates the opportunityProductInputs variable. Frustrating, to say the least. The code gets even more wonky. Take a look at the invocable method.

@InvocableMethod(label='Create New Opportunity Products')
public static void createNewOpportunityProducts(List<List<String>> inputs){

List<OpportunityLineItem> oppProds = new List<OpportunityLineItem>();

PriceBook2 stdPriceBook = [SELECT Id
FROM Pricebook2
WHERE isStandard = True

PriceBookEntry pbe = [SELECT Id
FROM PriceBookEntry
WHERE Product2Id =: inputs.get(0).get(0)
AND Pricebook2Id =: stdPriceBook.Id

for(Integer i = 0; i < Integer.valueOf(inputs.get(0).get(3)); i++){
OpportunityLineItem oppProd = new OpportunityLineItem();
oppProd.Product2Id = inputs.get(0).get(0);
oppProd.PricebookEntryId = pbe.Id;
oppProd.OpportunityId = inputs.get(0).get(1);
oppProd.UnitPrice = Integer.valueOf(inputs.get(0).get(2));
oppProd.Quantity = 1;
insert oppProds;

Notice that the data type of the input parameter is a nested list of strings. A standalone invocable method requires that the input parameter be a list. A single string becomes a list of strings, and a list of strings becomes a nested list of strings. Not to mention, you have to use nondescript indexes to reference the inputs within the method. The whole thing gets unwieldy fast. This has major downstream implications for code readability.

Now for a best practice. Let’s refactor this code by pivoting away from a standalone invocable method, and adding some invocable variables to hold the inputs. The invocable variables are defined in an inner class called Requests.

public class Requests{
    @invocableVariable(label='Product Id' required=true)
    public String productId;
    @invocableVariable(label='Opportunity Id' required=true)
    public String opportunityId;
    @invocableVariable(label='Sales Price' required=true)
    public Double salesPrice;
    @invocableVariable(label='Quantity' required=true)
    public Integer quantity;

Next, let’s change the input parameter of the invocable method to accept the data type of the inner class.

@InvocableMethod(label='Create New Opportunity Products')
public static void createNewOpportunityProducts(List<Requests> requests)

Here’s what the refactored method looks like, using the new input parameter.

@InvocableMethod(label='Create New Opportunity Products')
public static void createNewOpportunityProducts(List<Requests> requests){
     Requests request = requests[0];
     List<OpportunityLineItem> oppProds = new List<OpportunityLineItem>();
     Opportunity oppty = [SELECT Playworks_Fiscal_Date__c 
                          FROM Opportunity                           
                          WHERE Id =: request.opportunityId]; 
     PriceBook2 stdPriceBook = [SELECT Id 
                                FROM Pricebook2                                  
                                WHERE isStandard = True 
                                LIMIT 1];       
     PriceBookEntry pbe = [SELECT Id 
                           FROM PriceBookEntry 
                           WHERE Product2Id =: request.productId 
                           AND Pricebook2Id =: stdPriceBook.Id
                           LIMIT 1];       
     for(Integer i = 0; i < request.quantity ; i++){
         OpportunityLineItem oppProd = new OpportunityLineItem();
         oppProd.Product2Id = request.productId;
         oppProd.PricebookEntryId = pbe.Id;
         oppProd.OpportunityId = request.opportunityId;
         oppProd.UnitPrice = request.salesPrice;
         oppProd.Quantity = 1;
     insert oppProds;

Notice how much more readable the Apex is. Gone are the nondescript indexes, replaced by logical variable names. The Apex action element is also much more readable inside the flow. It is no longer necessary to gather up the inputs with an assignment element. The invocable variable values are set and the invocable method is called in the same place, creating a much more intuitive flow.

When comparing a standalone invocable method to an invocable method plus invocable variables, the difference is clear. The code becomes much more readable, and the flow is much easier to understand in the latter.

There is one more advantage of using an invocable method plus invocable variables, not shown in this example. While a standalone invocable method can only accept a collection of primitive data types, an invocable variable makes it possible to indirectly pass sObjects into the invocable method. An inner class with variables to hold accounts and opportunities would look something like this.

public class Requests{
        @invocableVariable(label='Accounts' required=true)
        public List<Account> accountsToProcess;
        @invocableVariable(label='Opportunities' required=true)
        public List<Opportunity> opportunitiesToProcess;     

Hopefully this post has convinced you not to build a standalone invocable method again, opting instead for an invocable method plus invocable variables. Thanks for reading, and feel free to send me any questions or feedback.

Compare & Contrast Two Record Collections with ‘FindCommonAndUncommonRecords’


Do you hate food recipe blogs that go on and on about a special story about how they grew up smelling grandma’s cooking and getting frustrated because you just want to get the recipe? Your kids are crying, the garlic and onions are burning, and you finally get to the recipe but it’s covered by an ad on your phone and the browser crashes.

This is not that post. Let’s get right to it!

I recently wrapped up work on a nifty action called ‘FindCommonAndUncommonRecords’ that can compare two like/unlike record collections based on a unique identifier that you specify. It then can provide 4 outputs providing records unique and shared between the two collections based on the identifying fields you provide.

The thoughts and solutions in this post are my own and not that of Salesforce.


The action can take a bit of effort to wrap your head around so I created a scenario showing off a hypothetical model involving Accounts and two custom objects, Gas Stations and Gas Station Relationships. An Account can be related to many Gas Stations through the Gas Station Relationships custom object.

Let’s say you build a screen flow that lets the user associate whatever Gas Station they want to the Account (using Datatable or a Lookup component). You will be creating these Gas Station Relationship records to set up that relationship. The catch here is that there are already Gas Stations tied to the account, but you have no way of filtering those out of the datatable/Lookup easily.

The user picks 4 gas stations to associate to Blackbeards Grog Emporium:

  1. Station 17
  2. Station 55
  3. Station 23
  4. Station 3

We then run this new action to get the Gas Stations not tied to the Account, or in other words the records unique to the selected collection by the user.

Here are the Inputs:

  • sourceRecordCollection – gasStationRelationshipsTiedToAccount
  • sourceUniqueID – Related_Gas_Station__c
  • targetRecordCollection – gasStationsSelectedFromDatatable
  • targetUniqueID – Id

Here’s the output:

targetUniqueCollection (Gas_Station__c) – The gas stations unique to the selected gas stations compared to the account’s related gas stations. Returned as Gas Station records.

  1. Station 55
  2. Station 23
  3. Station 3

targetCommonCollection (Gas_Station__c) – The gas stations shared between the account and the selected gas stations. Returned as Gas Station records.

  1. Station 17

sourceUniqueCollection (Gas_Station_Relationship__c) – The gas stations unique to the account compared to the selected gas stations. Returned as Gas Station Relationship records.

  1. Station 45
  2. Station 66

sourceCommonCollection (Gas_Station_Relationship__c) – The gas stations shared between the account and the selected gas stations. Returned as Gas Station Relationship records. 

  1. Station 17

From here, you can safely loop over the ‘targetUniqueCollection’ (the unique records selected by the user) to build your junction records without dupes!

Tutorial Video


  1. sourceRecordCollection  – The first collection you want to compare
    • Type – Record Collection
  2. sourceUniqueID – The field API name / identifier you want to use to find records in the ‘target’ collection (Case Sensitive)
    • Type – Text
  3. targetRecordCollection – The the second collection you want to compare
    • Type  – Record Collection
  4. targetUniqueID – The field API name / identifier you want to use to find records in the ‘source’ collection (Case Sensitive)
    • Type – Text


  1. sourceUniqueCollection – The unique records in the source collection when compared against the target collection
    • Type – Record Collection
  2. sourceCommonCollection – The shared records between the two collections, returned as the specified source Output sObject type.
    • Type – Record Collection
  3. targetUniqueCollection – The unique records in the target collection when compared against the source collection 
    • Type – Record Collection
  4. targetCommonCollection – The shared records between the two collections, returned as the specified target Output sObject type.
    • Type – Record Collection


This is part of Collection Actions package, version 1.23 and higher.


Salesforce Record Automation Benchmarking by Luke Freeland

Luke Freeland from the blog has published a post comparing process automation methods and measuring benchmarks Salesforce Record Automation Benchmarking
Part II compares Single VS Multi-Object Flow Field Update

Use Flows to Track Unread Emails on Cases and Increase Case Management Efficiency by Vibhor Goel

Vibhor Goel from the blog has published a post showing how to Use Flows to Track Unread Emails on Cases and Increase Case Management Efficiency

From Tamar Erlich: Powerful new OpportunityPartner Solutions using Flow with Apex-Defined Types

I was recently tasked with a requirement to display a list of opportunity partners on a custom object related to opportunity. I planned to use a flow to get the opportunity partner records and then display them using the Datatablev2 Flow Screen Component available on this site. When I tried to create the flow, I encountered some issues:
1. The Opportunity Partners object is a legacy object and cannot be customized. I could not create formulas for getting the account name or link to the account
2. I could not use dot notation to get the account name from the account ID in the get records component in the flow
3. I could not use the partner account ID as a filter criteria on a new get records element to get the account records.

Not Possible

I knew I had to create a custom way to get the data I needed to display. Eric Smith referred me to his blog post on how to use Apex defined data types and an invocable action to create data for the datatable component.
How to Use an Apex-Defined Object with my Datatable Flow Component
I create my Apex defined data type for opportunity partners.

* @author       : Tamar Erlich
* @date         : October 07, 2020
* @description  : Wrapper class for opportunity partners
* Note          : This class is called by the GetOpportunityPartnersAction.
* *************************************************
*       <runTest><runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR                | DATE               | DESCRIPTION
* 1.0     | Tamar Erlich          | October 07, 2020   | Created
public with sharing class OpportunityPartnersWrapper {

    // @AuraEnabled annotation exposes the methods to Lightning Components and Flows
    public String accountName;
    public String partnerRole;
    public Boolean isPrimary;

    public String accountLink;

    // Define the structure of the Apex-Defined Variable
    public OpportunityPartnersWrapper(
            String accountName,
            String partnerRole,
            Boolean isPrimary,
            String accountLink
    ) {
        this.accountName = accountName;
        this.partnerRole = partnerRole;
        this.isPrimary = isPrimary;
        this.accountLink = accountLink;
    // Required no-argument constructor
    public OpportunityPartnersWrapper() {}


I then created an invocable action that received the opportunity and account IDs and returns a JSON string that can be displayed in my flow table.

 * @author       : Tamar Erlich
 * @date         : October 07, 2020
 * @description  : Invocable method that given opportunityId and accountId, returns a list of opportunity partners to display on opportunity plan record page
 * Note          : This class is called by the Flow.
 * *************************************************
 *       <runTest>GetOpportunityPartnersActionTest<runTest>
 * *************************************************
 * @History
 * -------
 * VERSION | AUTHOR                | DATE               | DESCRIPTION
 * 1.0     | Tamar Erlich          | October 07, 2020   | Created
global with sharing class GetOpportunityPartnersAction {
  // Expose this Action to the Flow
  global static List<Results> get(List<Requests> requestList) {
    // initialize variables
    Results response = new Results();
    List<Results> responseWrapper = new List<Results>();
    String errors;
    String success = 'true';
    String stringOutput;
    List<OpportunityPartner> opportunityPartners = new List<OpportunityPartner>();
    List<OpportunityPartnersWrapper> opportunityPartnersSet = new List<OpportunityPartnersWrapper>();
    Set<Id> setOppIds = new Set<Id>();
    Set<Id> setAccIDs = new Set<Id>();
    Map<Id, List<OpportunityPartner>> mapOppOppPartners = new Map<Id, List<OpportunityPartner>>();

    // create sets of Ids to use as bind variables in the query
    for (Requests rq : requestList) {

    // query all request opportunities and their partners and create a map with opportunityId as the key and a list of parrtners as the values
    for (Opportunity opp : [
          SELECT AccountTo.Name, IsPrimary, Role
          FROM OpportunityPartnersFrom
          WHERE OpportunityId = :setOppIds AND AccountToId != :setAccIds
      FROM Opportunity
    ]) {
      mapOppOppPartners.put(opp.Id, opp.OpportunityPartnersFrom);

    for (Requests curRequest : requestList) {
      String accountId = curRequest.accountId;
      String opportunityId = curRequest.opportunityId;

      try {
        if (accountId == null || opportunityId == null) {
          throw new InvocableActionException(
            'When using the GetOpportunityPartners action, you need to provide BOTH an Account Id AND an Opportunity Id'

        if (!accountId.startsWith('001') || !opportunityId.startsWith('006')) {
          throw new InvocableActionException(
            'Invalid Account or Opportunity ID'
        // get the list of opportunity partners matching the current opportunityId from the map
        if (accountId != null && opportunityId != null) {
          opportunityPartners = mapOppOppPartners.get(opportunityId);

        // populate the Apex defined wrapper type with opportunity partner information
        for (opportunityPartner op : opportunityPartners) {
          OpportunityPartnersWrapper opw = new OpportunityPartnersWrapper();

          opw.accountName = op.AccountTo.Name;
          opw.partnerRole = op.Role;
          opw.isPrimary = op.IsPrimary;
          opw.accountLink = '/' + op.AccountToId;


        // Convert Record Collection to Serialized String
        stringOutput = JSON.serialize(opportunityPartnersSet);
      } catch (InvocableActionException e) {
        System.debug('exception occured: ' + e.getMessage());

        errors = e.getMessage();
        success = 'false - custom exception occured';
      } catch (exception ex) {
        System.debug('exception occured: ' + ex.getMessage());

        errors = ex.getMessage();
        success = 'false - exception occured';

      // Prepare the response to send back to the Flow
      // Set Output Values
      response.errors = errors;
      response.successful = success;
      response.outputCollection = opportunityPartnersSet;
      response.outputString = stringOutput;


    // Return values back to the Flow
    return responseWrapper;

  // Attributes passed in from the Flow
  global class Requests {
    @InvocableVariable(label='Input the Related OpportunityId' required=true)
    global String opportunityId;

    @InvocableVariable(label='Input the Opportunity\'s AccountId' required=true)
    global String accountId;

  // Attributes passed back to the Flow
  global class Results {
    global String errors;

    global String successful;

    public List<OpportunityPartnersWrapper> outputCollection;

    public String outputString;

  // custom exception class
  global class InvocableActionException extends Exception {
* @author       : Tamar Erlich
* @date         : October 07, 2020
* @description  : Test class for GetOpportunityPartnersAction Invocable method
* Note          :
* *************************************************
*       <runTest>GetOpportunityPartnersActionTest<runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR                | DATE               | DESCRIPTION
* 1.0     | Tamar Erlich          | October 07, 2020   | created
public with sharing class GetOpportunityPartnersActionTest {

    public static void opportunityPartnersFound(){

        // initialize variables
        List<GetOpportunityPartnersAction.Requests> requestList = new List<GetOpportunityPartnersAction.Requests>();
        String accountId;
        String opportunityId;

        // create test account
        List<Account> accounts = new List<Account>();

        for (Integer j = 0; j < 1; j++) {

            Account a = new Account(
                Name = 'email' + j + '.com'

        insert accounts;

        // create test opportunity
        Date closeDate =;
        Opportunity testOpp;

        testOpp = new Opportunity(
            Name = 'test opp',
            StageName = 'Open In-Progress',
            Amount = 100,
            CloseDate = closeDate
        insert testOpp;

        accountId = accounts[0].Id;
        opportunityId = testOpp.Id;

        // create test opportunity partner
        OpportunityPartner testPartner;
        testPartner = new OpportunityPartner(
            OpportunityId = opportunityId,
            AccountToId = accountId,
            Role = 'Dealer',
            IsPrimary = false
        insert testPartner;

        // prepare request for GetOpportunityPartnersAction
        GetOpportunityPartnersAction.Requests request = new GetOpportunityPartnersAction.Requests();
        request.accountId = accountId;
        request.opportunityId = opportunityId;

        // run test and assert results
        List <GetOpportunityPartnersAction.Results> results = GetOpportunityPartnersAction.get(requestList);
        System.assertEquals(results[0].errors, null,'Errors not expected');
        System.assertNotEquals(results[0].successful, 'false', 'Success expected');
        System.assertEquals(true, results[0].outputCollection.size()>0, 'Opportunity Partners expected');


I then used my new action in the flow to get the opportunity partner records.

Invocable action

I then configured my datatable on the flow screen

Flow Screen

Some notes on the datatable configuration:
1. User Defined had to bet set to true
2. The Datatable Record String must be set to the variable returned from the action
3. The column field types must be set for all columns. I set the first column to the url type
4. My first column has special type attributes for displaying as a clickable link: 1:{label: { fieldName: accountName}, target: ‘_blank’}
5. Since this is for display only, I hid the checkbox columns
6. I used the new features available in v2.46 to apply an icon and title to the table

The View All link uses a formula to create the link

View All Formula

Here is the entire flow

And here is the end result

Opportunity Partners on custom object

This use case can be easily adapted to display other opportunity related information like opportunity team members or opportunity contact roles.