Posts

Four New Collection Processor Actions added as Reactive Screen Components

I’ve added 4 new Collection Processor Actions as Reactive Screen Components. The original actions were Extract Field to Collection and Calculate Field Summary. The latest additions are Filter Collection, Find Common and Unique Records, Join Collections and Sort Collections.

This means you can now do multiple types of collection processing without having to leave the flow screen. And the amazing thing is that these components are all reactive to each other and any other flow screen components that support reactivity.

Read this article to see different use cases for these actions and to get the installation links to install them in your own org.

Replacing Programmatic Tech Debt with Flow

How Playworks used Flow to replace multiple Visualforce pages and 1000s of lines of Apex code

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 I encourage you to learn more about what they do and consider supporting their work.

The Old Architecture

Playworks has a custom object called NCES Data, which contains information about 115,000 schools and 15,000 districts in the United States. The data is released by the National Center for Education Statistics each year, which the Playworks team then loads into their Salesforce org.

As Playworks establishes their presence at a new school, reps use a wizard to create an account for the school, along with a child record that stores the school’s current demographic information. The demographic information is important for various grants and funding. Districts are also represented with account records. School accounts, district accounts, and their respective demographic information records are created from data residing in the NCES Data object.



Reps interact with a Visualforce page embedded in a custom tab called Create New School, or they click an Add School button on the partner organization related list. Partner Organization is a junction object that allows Playworks to attach multiple account records to opportunities, where appropriate. A separate Visualforce page with its own controller is launched from the Add School button. When this occurs, a partner organization record is created in addition to the school or district account record. 


The rest of the logic remains the same. The search functionality is handled by Apex classes, and an Apex trigger creates the demographic information. Whether invoked by the Create New School tab or the Add New School button, each Visualforce page finishes with a redirect handled by a third Visualforce page with its own controller.

That’s a lot of custom dev work, and a lot of tech debt.

  • 9 Apex classes
  • 8 Apex test classes
  • 3 Visualforce pages
  • 1 Apex trigger

The good news is the code was written by highly competent developers, and has held up for nearly a decade. The bad news is that even a minor update requires the code to be modified.

The New Architecture

Speaking in terms of business logic, there are 5 possible outcomes when a user searches the NCES Data for a school or district. 

  1. NCES record is a school or district that already has an account record.
  2. NCES record is a district that does not have an account record.
  3. NCES record is a school that not have an existing account record. The school has an NCES record for the district, but there is no account record for the district.
  4. NCES record is a school that not have an existing account record. There is no NCES record for the district. (This can happen with some private schools.)
  5. NCES record is a school that not have an existing account record. There is an account record for the district.

Each of these outcomes has slightly different requirements, depending on if it was invoked by the Add New School button or the Create New School tab.

Regardless of the eventual path for each of the 5 possible outcomes, all flows begin the same way.

First, since there are multiple account record types, the school record type is retrieved. Next, a flow screen gathers the necessary information to perform the search.

After failed attempts to recreate the Apex search functionality using a Get Records element with filter logic, a decision is made to go with code. A small portion of the existing code base is refactored into an invocable method. The method accepts the search parameters from the previous screen, and returns a list of NCES records.

public class CreateNewSchoolOrDistrictSearch {
    
    private static String maxresults = '25';
    
    @InvocableMethod(label='Search for NCES Data')
    public static List<Results> searchForNCESData(List<Requests> requests){
    
        Requests request = Requests[0];
        
        String queryString = generateNCESDataQuery(request.schoolNCESId, request.schoolName, request.schoolCity, request.schoolState, request.schoolZip);
        
        Results result = new Results();
        result.ncesRecords = Database.query(queryString);
        System.debug('NCES Records: ' + result.ncesRecords);
        List<Results> results = new List<Results>();
        results.add(result);

        return results;
    }
    
    public static String generateNCESDataQuery(String schoolNCESId, String schoolName, String schoolCity, String schoolState, String schoolZip){
        return generateNCESDataQuery(schoolNCESId, schoolName, schoolCity, schoolState, schoolZip, false, false);
    }
    
    public static String generateNCESDataQuery(String schoolNCESId, String schoolName, String schoolCity, String schoolState, String schoolZip, Boolean exactIdMatch, Boolean isDistrict){
        
        String ncesDataQuery = 'SELECT Id, Existing_Organization_in_Salesforce__c, School_Level__c, NCES_Id__c, School_Name__c, School_Mailing_Street__c, School_Mailing_City__c, School_Mailing_State__c, School_Mailing_Zip__c, School_Physical_Street__c, School_Physical_City__c, School_Physical_State__c, School_Physical_Zip__c, NCES_District_ID__c, District_Name__c, District_Mailing_Street__c, District_Mailing_City__c, District_Mailing_State__c, District_Mailing_Zip__c FROM NCES_Data__c '; 
        
        if(!isDistrict){
            ncesDataQuery += ' WHERE NCES_ID__c != NULL AND NCES_ID__c != \'0\' '; 
        }else{
            ncesDataQuery += ' WHERE NCES_ID__c = NULL AND NCES_District_ID__c != NULL AND NCES_District_ID__c != \'0\' ';             
        }
        
        if(!String.isBlank(schoolNCESId)){
            if(!isDistrict){
                if(exactIdMatch){ 
                    ncesDataQuery += ' AND (NCES_ID__c = \'' + schoolNCESId + '\') ';                
                }else{
                    ncesDataQuery += ' AND (NCES_ID__c = \'' + schoolNCESId + '\' OR NCES_ID__c LIKE \'%' + schoolNCESId + '%\') ';
                }            
            }else{
                if(exactIdMatch){
                    ncesDataQuery += ' AND (NCES_District_ID__c = \'' + schoolNCESId + '\') ';                
                }else{
                    ncesDataQuery += ' AND (NCES_District_ID__c = \'' + schoolNCESId + '\' OR NCES_District_ID__c LIKE \'%' + schoolNCESId + '%\') ';
                }
            }
        } else {
            if(!String.isBlank(schoolName)){
                ncesDataQuery += ' AND (School_Name__c LIKE \'%' + encodeForQuery(schoolName) + '%\')';                
            }
            if(!String.isBlank(schoolCity)){
                ncesDataQuery += ' AND (School_Mailing_City__c LIKE \'%' + encodeForQuery(schoolCity) + '%\')'; 
            }
            if(!String.isBlank(schoolState)){
                ncesDataQuery += ' AND (School_Mailing_State__c LIKE \'%' + encodeForQuery(schoolState) + '%\')'; 
            }
            if(!String.isBlank(schoolZip)){
                ncesDataQuery += ' AND (School_Mailing_Zip__c LIKE \'%' + encodeForQuery(schoolZip) + '%\')'; 
            }
        }
        
        ncesDataQuery += ' ORDER BY Name LIMIT ' + maxresults;
               
        return ncesDataQuery;           
    }
    
    private static String encodeForQuery (String inputVal) {
        return String.escapeSingleQuotes(inputVal.trim());
    }
    
    public class Requests{
        
        @invocableVariable(label='NCES Id')
        public String schoolNCESId;
        
        @invocableVariable(label='School Name')
        public String schoolName;
        
        @invocableVariable(label='School City')
        public String schoolCity;
        
        @invocableVariable(label = 'School State')
        public String schoolState;
            
        @invocableVariable(label='School Zip Code')
        public String schoolZip;
    }  
        
    public class Results{
        
        @invocableVariable(label = 'NCES Records')
        public List<NCES_Data__c> ncesRecords;
    }  
}

Next, the search results are displayed in a table, and a school or district is selected. Remember the results are a list of NCES data records returned by the invocable Apex. The selected NCES data record is used to create the necessary school account, district account, and corresponding demographic information record. If the school account or district account record already exists, the user is redirected.

Recall the 5 possibilities. 

  1. NCES record is a school or district that already has an account record.
  2. NCES record is a district that does not have an account record.
  3. NCES record is a school that not have an existing account record. The school has an NCES record for the district, but there is no account record for the district.
  4. NCES record is a school that not have an existing account record. There is no NCES record for the district. (This can happen with some private schools.)
  5. NCES record is a school that not have an existing account record. There is an account record for the district.

Each one of these is represented by a path in the flow. For example, if a school or district already exists as an account record, the user is redirected to that record. Before that happens, if the flow is launched from the Add School button, a partner organization is created. If the flow is launched from the Create New School tab, then it simply redirects to the school or district.


Each of the 5 possibilities is accounted for, in different branches of the flow. The programmatic complexity of the Visualforce and Apex are replaced by the flow. While complicated in its own right, the solution is now declarative.

Conclusion

While we fell short of a completely declarative solution, the logic of the search functionality is unlikely to change. By refactoring the Visualforce pages and 95% of the Apex logic into flow, the Playworks team can safely make changes in the future, without having to hire a developer.

Reactive Screens: How to Turn a 7 Element Flow Into 1

When Winter ’23 release arrived, it brought with it Data Table and IN operator. I was excited to use them and wrote a blog on how it helped me solve a use case that was not possible to achieve with standard reports. I was happy with the 7 steps flow I created to achieve this
Now, a few releases later, Summer ‘23 is around the corner, and with it comes reactive screens and reactive formulas. I went back to my solution and was able to get the same result and reduce a 7 step flow to 1 screen. How awesome is that!!!

Reactive screens are currently in beta and you can try them out in a prerelease org running Summer ‘23 (don’t forget to opt in to the reactive screens beta under process automation settings in setup)

Here is how I did it

Reminder use case: display all open opportunities where the selected user is on the account team. Then display the account team role this user has for these accounts.

I used the new Data Fetcher component from Josh Dayment, and combined it with some complex SOQL queries that output the results into a Data Table.

To select the user, I added a lookup to the screen. This is currently the only reactive lookup element available. In order to get a list of users, you can point the component to any CreatedById lookup field from any object. I used the Account object.

My first formula (and make sure to use a formula resource, and not a variable or text template for this, as only formulas are reactive. Don’t ask me how I know 😉) queried the records for open opportunities

I fed this formula into the first data fetcher, and used a merge field to get the currently selected user from the screen.

The data fetcher queried records are displayed on screen using the Data Table component (Data Table is now GA, and also got some new enhancements in the summer release). The lookup component outputs the selected value and the selected label so I used another little formula to dynamically display the selected user name in the table title.

A similar process followed for the second table and showing the account team role

Look what I could achieve with just 4 formulas and one screen. Here is a demo of the finished flow and reactive functionality.

I can’t wait for this to be generally available to help us simplify multiple flows. For now this is only available in prerelease orgs and soon in sandboxes.

Data Fetcher on the AppExchange

Data Fetcher is a Lightning Web Component for Screen Flows that will query records based on a SOQL string, then provide an output of records (or single record) to be used on the same screen. What is so special about that you might ask? Well, let me tell you! With the reactive components beta for Screen Flows, you can now have components on a single screen react based on input from other components without ever leaving the screen. Data Fetcher was featured at TDX ’23 as part of a session on reactive screen components.

Let’s take an example using components from here on UnofficialSF. With Data Fetcher I can accept the selected choice value from Quick Choice* as my SOQL string and return records into a Data Table without having to click next. The records in the data table change depending on the choice I select – instantly. Before getting started ensure you have opted into the Reactive Screens Beta in Automation settings in your org instructions can be found here

Before reactive screens, this would have been accomplished using Lightning Messaging Service and Lightning Web Components, increasing the time to develop and go to market for this type of use case along with the costly maintenance of that LWC. With Data Fetcher an admin or developer can dynamically retrieve records on one screen without much additional lift, greatly enhancing the overall user experience and freeing up time for the admin or developer to work on other features for more important tasks.

In Summer ’23 formula resources will be reactive in flows – making this component significantly more powerful. With reactive formulas, Data Fetcher’s SOQL query string can now reference multiple components on the same screen which will create a truly dynamic experience. Pre-release orgs have already been enabled if you want to start testing now. Let’s look at a couple of examples.

The first example will use two Quick Choice components to help build my query utilizing a reactive formula. I will use the Quick Choice components to have the user select the Industry and Account Type of the Accounts I want to return on the Datatable those values will be used in a formula to return my SOQL string. (I am not doing it in this example but there is a really great video walkthrough on how you could use Quick Choice as a dependent picklist on the original post that could really enhance this use case)

//You can use this example to get started on your own formula//

"SELECT BillingCity, Id, Name, Phone, Industry, Type FROM Account WHERE Industry =" + {!choice.value} +" AND Type = " + {!Type.value}

In the below video, I am using the out-of-the-box lookup component to pass an Account Id to a SOQL string to populate all of the related contacts in a data table. A couple of years ago I presented on Automation Hour about using SOQL and SOSL in flow if you watch this video I had to build a SOQL query across multiple screens and decisions in order to perform the query inside of the flow. This Summer that will no longer be needed! Soon, an admin or developer will be able to add multiple components onto a single screen and use a formula variable to build a SOQL statement that then passes back the records on the screen as the user is interacting and/or changes different parts of the query and see the results as the changes are happening.

Note: Choices are not yet reactive as part of the beta. You will need to use Quick Choice if you want to bake choices (like a Picklist) into Data Fetcher queries.

For more information and to install Data Fetcher visit the AppExchange Listing here.

Learn more about the Reactive Screen Components Beta on Trailhead!

A Screen Component That Displays Fields in Read Only Mode

If you are building a form, you may want to make some screen components read only. In those cases, you can use this custom screen component that displays fields in read only mode.

How to Use the Component

1- Install the component using the installation links below.

2- Add a screen element and search for ReadOnlyScreenComponent. This is the name of the component that you installed.

3- This component can display 5 different data types: string (text), integer (number), checkbox, date, date/time. Enter the data type that you want to display. Valid values are string, integer, checkbox, date, and datetime.

Then provide the field label that the users will see.

Lastly, enter the value that you want it to display. Make sure that the value matches the data type that you entered.

4- Look and feel of the component changes according to the data type that you selected. Here is a read only screen that is built with ReadOnlyScreenComponent. Data types used in this flow are string, integer, date, datetime, and checkbox, respectively.

Read this post to see an example.


Installation Links

Use this link to install in production or developer edition environments.

Use this link to install in sandbox environments.


View Source

Here is the link to the source code repository.

Process approvals using a screen flow in Slack

With the Spring 23 release, screen flows in Slack will be generally available. I set out to explore one use case.

Approvals in Slack are currently possible but have some limitations

  1. It is only available with the Salesforce for Slack app but not available if you setup one of the cloud specific apps (Sales cloud for Slack, Service cloud for Slack etc.)
  2. You can only approve or reject but cannot add an approval comment.

I wanted to see if I can improve on that by using a screen flow in Slack. This blog will not cover the initial setup and connection of Salesforce to Slack. You can find details on how to do this in the official documentation.

My solution was inspired by two blogs

  1. Alex Edelstein created an invocable action for processing approval requests that includes sending an approval comment
  2. Yumi Ibrahimzade demonstrated how to copy an approval comment from the approval history related list to a field on the record

In order to send a screen flow to Slack you have to create two separate flows that will work together.

  1. A screen flow – this is what will surface to the user in Slack and process the user inputs there. This flow has to be marked as available in Slack. Marking a flow as ‘Available in Slack’ generates a separate ‘launcher’ invocable action that can be used in other flows
  2. A record triggered flow – This flow will act as a dispatcher and will use the launcher action, we created in step 1, to send the screen flow to Slack when record conditions are met. Slack actions can only be processed asynchronously, after the record is saved.

To test this, I setup a simple one step approval process, with simple actions that update an approval status picklist field on the record

I then created my screen flow which has only one screen. The screen displays an approval request to the user, along with some record details, and captures the responses. The flow will then use those responses to process the approval using the invocable action, and update the record with the approval comment. The flow has some input variables that will get their value from the dispatching flow. I marked this screen flow as available in Slack. I was not successful in passing a record link to the screen flow. Slack uses a special markup language to format links, which works well when sending a Slack message using the “Send Slack Message” core flow action, but it did not work when sending in the screen flow action.

The next step was to create the record triggered flow that will send to Slack. My trigger was based on a custom Approval status field set to submitted, when a user will submit the record for approval. This will work well for a single step approval process but if you have a multi step process, additional conditions may be needed to determine the current approval step. This flow gets the most recent approval process instance, work item, and approver userId for the triggering record and passes this information, along with the record Id, to the screen flow action.

When both flows are combined, I was able to send an approval request to Slack, process the response, and update the record with the most recent approval comment. Here is a short video of the complete process.

This is a simple use case for a single step approval process with a single approver. It can be enhanced to handle more complex approval processes.

  1. If there are multiple approvers, the dispatching flow will need to get a collection of all the work items and loop through to send the screen flow to all of them.
  2. If a queue is set as the approver, the the dispatching flow will need to get a collection of all members of the queue to find their userId 
  3. The screen flow can be updated to add a recall action

Invoke a MuleSoft RPA Process from Flow using External Services

MuleSoft Robotic Process Automation (RPA) gives you the power to automate business processes that usually require human input and interact with systems that don’t have an API, extending the reach of Salesforce’s process automation capabilities by integrating these RPA processes into flows in Salesforce as of Winter ’23!

Use MuleSoft RPA Manager to publish REST APIs for your RPA processes to Anypoint Exchange, which you can then register as External Services in Salesforce and invoke from low-code tools like Flow Builder.

This post describes the configuration steps necessary to invoke a MuleSoft RPA Process in Flow Builder. It assumes familiarity with MuleSoft RPA, Named Credentials and External Credentials, Permission Sets, External Services, and Flow Builder.

Step 0: MuleSoft RPA Pre-requisites

  • Configure the connection between MuleSoft Anypoint platform and your org to import MuleSoft RPA APIs.
    • In MuleSoft RPA Manager, publish the RPA process as a REST API to Anypoint Exchange.
    • In MuleSoft RPA Manager, copy the API key from the User Management | User API Keys page.

MuleSoft RPA Manager: User Management page

Steps 1-3: Connect Salesforce to MuleSoft Anypoint Platform

These steps create the initial connection between MuleSoft Anypoint platform and your org:

  • Step 1: Create a Connected App in MuleSoft Anypoint PlatformA connected app in MuleSoft Anypoint platform allows Salesforce to call MuleSoft APIs.
  • Step 2: Create an Authentication ProviderUse the ID and the secret from your MuleSoft Anypoint Platform connected app to create an authentication provider.
  • Step 2b: Update Your MuleSoft Anypoint Platform Connected App – Use the Salesforce authentication provider callback URL to update your MuleSoft Anypoint Platform connected app.
  • Step 3: Create a Legacy Named Credential (Retrieve APIs) – Create a legacy named credential to access/authenticate into your MuleSoft Anypoint Platform connected app from Salesforce and retrieve the APIs published to MuleSoft Anypoint Exchange. The legacy named credential stores the URL for MuleSoft Anypoint Platform. External Services uses the legacy named credential when listing the MuleSoft APIs available for import.

Note: the ability to create a ‘Legacy’ named credential is still supported, although it will be discontinued in a future release, at which point, this portion of the instructions will be updated to reflect the new capability.

Salesforce: Named Credentials Home Page

Step 4: Create a Named Credential (Runtime) and External Credential

Note: MuleSoft RPA authenticates clients invoking RPA processes through API keys. The Salesforce Winter ’23 release includes new functionality in Named Credentials and External Credentials that supports this type of authentication.

  • Create an external credential, permission set mapping and custom header
    • Overview
      • Create a second named credential that stores the endpoint for MuleSoft RPA Manager. External Services uses the second named credential when invoking “runtime” the MuleSoft RPA process in a flow.
    • External credential:
      • Before creating a second named credential, first create an external credential.
        • Create an external credential to capture the API key and other authentication details.
        • Then create a named credential to store the actual endpoint.
        • This allows for multiple endpoints to be addressed with the same authentication configuration.
      • After entering the details for the external credential, click Save. You’re taken to the Named Credentials screen. Now you need to create a permission set mapping for this new external credential. This mapping ensures that only the correct users get access to these credentials.
    • Permission set mappings:
      • Click External Credential and select the external credential you created.
      • Scroll to Permission Sets Mappings so you can link the external credential to a user’s permission set.
      • Click New to create a permission set mapping for this external credential using the details below for an external credential that uses ‘Custom’ as the authentication protocol.
    • Custom headers:
      • Lastly, create a custom header for this external credential using the the details below as guidance for how to create a custom header.

Example
Salesforce: External Credentials Detail Page

  • Create a named credential
    • Now that you have an external credential, create a named credential and then link it to the external credential you just created in the previous step. As mentioned before, this second named credential stores the endpoint for MuleSoft RPA Manager. External Services uses the second named credential when invoking “runtime” the MuleSoft RPA process in a flow. Use the details below for how to create a named credential.

Example

Salesforce: New Named Credential Window

  • Verify that the external credential and the named credential you just created are linked
    • From the Named Credentials page, click External Credentials.
    • Click the name of the external credential from this list and confirm that the named credential you just created appears in the Related Named Credentials area shown in the example below.
    • As a reminder:
      • The Named Credential stores the MuleSoft RPA Endpoint
      • The External Credential stores the MuleSoft RPA API Key + Authentication details

Example
Salesforce: External Credentials Detail Page

Step 5: Register the MuleSoft RPA API as an External Service

As you are configuring the external service, recall that you created two named credentials.

  • Legacy named credential (Retrieve APIs) – this named credential stores the MuleSoft Anypoint platform URL and is used to retrieve the APIs published to MuleSoft Anypoint Exchange. Use this named credential in the “Select a MuleSoft Anypoint Platform Account” screen.

Example

  • Named credential (Runtime) – this named credential stores the MuleSoft RPA Manager URL and is used when invoking “runtime” the MuleSoft RPA process in a flow. Use this named credential in the “Configure your MuleSoft Anypoint Platform Service” screen.

Example

Example

Step 6: Invoke a MuleSoft RPA Process in a Flow

  • Now you are ready to start and check the status of the MuleSoft RPA process from a flow.
  • If you follow the steps outlined in the help docs Invoke a MuleSoft RPA Process in a Flow, listed below are a few additional tips to help guide you through the flow configuration.

Create Resource for RPA Process Input Variables

Tip: In step 2, when creating a New Resource to store the input variables for the RPA process, you can obtain the External Service Name and Apex Class Name from the External Services Detail Page to help configure the New Resource

Salesforce: External Services Detail Page

Assign Values to RPA Process Input Variables

Tip: In step 3, you can obtain the information about each variable/input parameter that needs to be defined in the above Apex class from the External Services Detail Page as well as the OpenAPI specification (a few examples shown below).

RPA Process OpenAPI Specification

OperationId: startProcess > ProcessExecutionWithExecutionId

ProcessExecutionWithExecutionId > ProcessExecution

ProcessExecution > inputArguments

Example Custom Flow Actions for Omni Supervisor

In the Winter ’23 release, an exciting new feature was shipped allowing admins to put more power in the hands of Supervisors to run their teams’ with a new configurable action framework from Omni Supervisor. I’ve gone into some details on creating your own actions in this post, but this page is here to describe a package of custom actions that I’ve created that you can install and use make use of as you see fit.

You can install these Supervisor Flow Actions as a standalone package from this link (unmanaged) – Now updated for Spring ’23 to include Skills and bulk reassignment!

  • Ensure that Omni Channel is enabled before installing

Once installed – check for each flow below as to whether you need to make updates, before adding it to a Supervisor Configuration (as detailed in the help docs) – all Flows that reassign work will need updating.

Flow list:


Password Reset

Description:
This flow is designed to be used on the ‘Agent Summary’ tab of Omni Supervisor, and expects a list of User record Ids to be passed in (to the input variable ‘ids’ – this is done by default from Omni Supervisor). This will show the above screen – where you can choose to send a notification to the affected Users – choosing to send the notification will result in the users getting a Salesforce notification (bell icon), informing them that their password has been reset. They will also get the standard password reset email (whether you select the checkbox or not).

Note – this Flow is set to ‘Run as System’ as it is calling Apex, and the ‘system.resetPassword()’ function.

Required Updates

  • Add this action to the appropriate ‘Supervisor Configurations’ (or create one) that you want this to appear for.
  • Ensure you have a ‘Custom Notification’ in the Setup screens which is active, and applies for both Desktop and Mobile (for the checkbox to send a notification to work)
  • Ensure the user has access to the ‘ResetUserPassword’ Apex Class (via Permission set or Profile)

Optional Updates
None


Update Capacity

Description:
This flow is designed to be used on the ‘Agent Summary’ tab of Omni-Supervisor, and expects a list of User record Ids to be passed in (to the input variable ‘ids’ – this is done by default from Omni-Supervisor). This will show the above screen – where you can enter the new capacity to use for the selected user(s). Note that in doing this, it may create a new ‘Presence Configuration’ in the back end – this is why this Flow needs to be ‘Run as System’, as a standard user won’t have permission to create or edit those admin records.

If a Presence Configuration with the entered Capacity doesn’t exist, then the Flow will clone the ‘default_presence_configuration’ (created when you turn on Omni-Channel) to create a new one, using the capacity entered – therefore you may end up with a lot of different Presence Configurations when using this Flow. Therefore, please update the default presence configuration to have the values you expect/want for each of these new Presence Configurations.

Choosing to send the notification will result in the users getting a Salesforce notification (bell icon), informing them that their capacity has been updated, and they need to log out and back in to Omni-Channel.

Required Updates

  • Add this action to the appropriate ‘Supervisor Configurations’ (or create one) that you want this to appear for
  • Ensure you have a ‘Custom Notification’ in the Setup screens which is active, and applies for both Desktop and Mobile (for the checkbox to send a notification to work)
  • Update the ‘default_presence_configuration’ to have any settings you want replicated across new Presence Configurations

Optional Updates

  • You can change this to create a Presence Configuration per agent – this creates a lot of overhead, but means that their capacity change can take effect right away (i.e. without logging in & out of Omni). The risk with this is updating the capacity of an existing Presence Configuration, will result in all assigned users having the new capacity, which may not be what you expect or want.

Send Notification

Description:
This flow is designed to be used on the ‘Agent Summary’ tab of Omni-Supervisor, and expects a list of User record Ids to be passed in (to the input variable ‘ids’ – this is done by default from Omni-Supervisor). This will show the above screen – where you can enter the subject and body of the notification you want to send the selected User(s).

Sending the notification will result in the users getting a Salesforce notification (bell icon), with the entered values. Clicking on this notification will take them to their ‘Home’ screen

Required Updates

  • Add this action to the appropriate ‘Supervisor Configurations’ (or create one) that you want this to appear for.
  • Ensure you have a ‘Custom Notification’ in the Setup screens which is active, and applies for both Desktop and Mobile (for the checkbox to send a notification to work)

Optional Updates

  • You can copy/clone this Flow to send a message in your appropriate messaging platform (e.g. Slack, Teams or SMS) or to send an Email, using the various Flow actions available (either OOB, or via install).
  • You can update the navigation from the ‘Send Navigation’ action to go to a different location as appropriate for you (but a navigation target of some sort must be defined).

Reassign All

Description:
This flow is designed to be used on the ‘Queues Backlog’ tab of Omni-Supervisor, and expects a list of Queue (Group) record Ids to be passed in (to the input variable ‘ids’ – this is done by default from Omni-Supervisor). This will show the above screen – where you can select the appropriate Queue or User you want to assign all of the work in the selected Queue(s) to. Today, this only supports Queues, Skills and Agents.

Required Updates

  • Each of the Route Work actions need to be updated for your Org – select the appropriate channel and routing configuration (where appropriate) to a valid value.
  • For any other types of work you have in your org, you will need to add another ‘Collection Filter’ element to filter for that work, and loop through it.

Optional Updates
None


Reassign

Description:
This flow is designed to be used on any of the tabs in Omni-Supervisor that lists work – such as the Skills Backlog, or any of the sub-tabs of each page. The Flow expects a list of record Ids to be passed in (to the input variable ‘ids’ – this is done by default from Omni-Supervisor) – these could be of many different types. This will show the above screen – where you can select the appropriate Queue, Skills or User you want to assign all of the selected work items to.

Warnings

  • Reassigning work that is currently assigned directly to an agent to a Queue will fail – workaround is to assign to a skill first, before assigning to the queue (can be added to the Flow)
  • Using this action on the ‘Assigned Work’ page may have unexpected consequences – as that work is currently with an agent, and will be re-routed while they are still working on it, potentially leading to multiple people working on the same work item.

Required Updates

  • Each of the Route Work actions need to be updated for your Org – select the appropriate channel and routing configuration (where appropriate) to a valid value.
  • For any other types of work you have in your org, you will need to add another ‘Collection Filter’ element to filter for that work, and loop through it.

Optional Updates


Assign to Me

Description:
This flow is designed to be used on any of the tabs in Omni-Supervisor that lists work – such as the Skills Backlog, or any of the sub-tabs of each page. The Flow expects a list of record Ids to be passed in (to the input variable ‘ids’ – this is done by default from Omni-Supervisor) – these could be of many different types. This will show the above screen – where you can select the appropriate Queue or User you want to assign all of the selected work items to. Today, this only supports Queues and Agents, due to limitations with the setup of dynamically adding skills in Flow – it is on the near term roadmap to resolve this short-coming.

This is the same as the Reassign Flow above, but a faster way to assign to yourself if you want to quickly grab a work item out of the backlog.

Warning – using this action on the ‘Assigned Work’ page may have unexpected consequences – as that work is currently with an agent, and will be re-routed while they are still working on it, potentially leading to multiple people working on the same work item.

Required Updates

  • Each of the Route Work actions need to be updated for your Org – select the appropriate routing configuration to a valid value.
  • For any other types of work you have in your org, you will need to add another ‘Collection Filter’ element to filter for that work, and loop through it.

Optional Updates
None

Writing Test Classes for Invocable Methods and Invocable Variables

If you need a primer on invocable methods or invocable variables, check out this blog. To keep things consistent, this post references the same Product Selector pro-bono project for Playworks.

Here is the inner class of invocable variables.

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;
    }

And here is the invocable method, which takes a list of Requests objects as an 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;
         oppProds.add(oppProd);
     }
     insert oppProds;
}

Note: The way that this Product Selector flow is designed, only one instance of the Requests class is ever populated. Hence index 0 from List<Requests> is assigned.

Requests request = requests[0];

Typically, Apex tests follow the same basic algorithm.

  • Create the data
  • Run the test
  • Validate the assertions

When writing a test for an invocable method with invocable attributes, there are a couple of things to look out for.

Creating the Data

When the createNewOpportunityProducts invocable method is called by the flow, the flow outputs provide the data. Specifically, the flow maps its output variables to what will become attributes of the Requests object at runtime.

The test class is much more manual. Since there is no flow to pass its output variables, we have to create the necessary data.

Pricebook2 standardPricebook = new Pricebook2();
standardPricebook.Id = Test.getStandardPricebookId();
standardPricebook.isActive = true;
update standardPricebook;
        
Product2 prod = new Product2();
prod.Name = 'Test Prod';
prod.IsActive = True;
prod.Product_Selector__c = True;
insert prod;
        
PricebookEntry pbe = new PricebookEntry();    
pbe.Pricebook2Id = standardPricebook.Id;
pbe.Product2Id = prod.Id;
pbe.IsActive = True;
pbe.UnitPrice = 1000;
insert pbe;
        
Opportunity opp = new Opportunity();
opp.Name = 'Test Oppty';
opp.StageName = 'Prospecting';
opp.CloseDate = Date.today();
insert opp;

Once the data is ready to map, we instantiate an object of the Requests class. To do this, we start with the name of the class that contains the invocable method: ProductSelector_Create. That is followed by dot notation to reference the inner class Requests.

List<ProductSelector_Create.Requests> lpscr = new List<ProductSelector_Create.Requests>();

Remember, only one instance of the Requests class is ever populated. So next we instantiate a single ProductSelector_Create.Requests object, map the data that we created earlier to its attributes, and add it to the list of ProductSelector_Create.Requests.

ProductSelector_Create.Requests pscr = new ProductSelector_Create.Requests();
pscr.productId = prod.Id;
pscr.opportunityId = opp.Id;
pscr.salesPrice = 800;
pscr.quantity = 2;
lpscr.add(pscr);

Now we have an input parameter to pass into the createNewOpportunityProducts invocable method.

Running the Test

There is one more thing to be aware of. When an Apex class is called by an Apex trigger, the DML causes the trigger to fire. Since we’re testing an invocable method that is called by a screen flow, no such mechanism exists. Instead of using a DML, we have to explicitly invoke the method, passing in the input parameter we just created.

ProductSelector_Create.createNewOpportunityProducts(lpscr);

Validating the Assertions

Thankfully, there are no real gotchas here. In the Product Selector example, verify that the quantity of 2 caused 2 opportunity products to be created.

Introducing: User-aware Record Type Picker for Flows

Update October 15, 2022: install links updated to latest version
Update September 26, 2022: install links updated to latest version

Next week is the year’s big Main Event: Dreamforce. For some of us, that means packing our things and scrambling to fill our session schedules, make meetup plans, and find the perfect dinner spot. For others, like me, an sense of impending FOMO.

Fortunately, the announcements made at Dreamforce, and the Winter 23 release just a few weeks after, will be for everyone, whether you make it in to San Francisco or not. If you’ve reviewed the release notes or scoped out your preview sandbox, one item you may notice is that the Dynamic Fields feature for Screen Flows will be Generally Available, and with that, picklist fields will be Record Type-aware. This means you can simply place a record variable’s picklist field on a screen, and if you’ve set the Record Type for that record, it will correctly display only the picklist values that are active for that Record Type. However, that Record Type must be set on the record variable before the Flow arrives on that Screen with the picklist. So, then: what options do we have today for letting a user select a Record Type?

Today’s Options for RecordType Selection in Flows

  • You could build a Choice for each Record Type. That might work, but what if different users have a different permissions to different Record Types. Sounds like a pretty hairy mess to try to get right, and to maintain moving forward. Not a great option.
  • You could do a Record Choice Set or a Collection Choice Set based on the RecordType object. Of course! Perhaps by setting the Screen Flow to run in User Mode, which respects a user’s access to records, we’ll only get the Record Types that the user has permissions for. If you’ve tried this, you’ll quickly learn that the RecordType object, which holds all each object’s Record Types, is not limited by the user’s permissions. If a user has read access to an object, they’ll have access to every RecordType for that object, even if they don’t have permission to create a record for that Record Type.
  • The closest I could find is the GetRecordTypeInfo action in the UnofficialSF Flow Action Pack. Its a nifty action! But its a bit incomplete, as it requires you to pair it with another screen component, it won’t give you the Record Type Descriptions, and the Record Type information is provided in separate text collections, making it challenging to be able to use the selected option downstream in the Flow.

🎉 Welcome to the team, Record Type Picker 🎉

The Record Type Picker is an open-source, free-to-use component that can be used in Screen Flows to give users selectable choices of Record Types, automatically filtered to just the Record Types that the running user has access to!

Three display types

Visual Picker, Radio Group, Picklist, each with optional Show or Hide Descriptions

Screenshot of the three display options available for this Record Type Picker component

Optional Auto-Next Navigation on Select

Auto navigate on selection works best with the Visual Picker on a screen that doesn’t have any other inputs and has the footer and/or Next button hidden. When the user clicks on a selection, the Flow will automatically navigate to the next screen. It is also recommended to provide a “Previous” button on the next screen so that the user has a chance change their selection if they’ve made a mistake.

Easy configuration with a Custom Property Editor that has a Preview mode

Bonus Invocable Action

The package also comes bundled with an Invocable Action to get a Collection of RecordTypes available to the running user, in case you’d prefer to use other screen components like Collection Choices or Data Table

Install with Ease

This component does not depend on any other packages, and can be installed with an unlocked, namespaced package using the links below. The namespace means you don’t have to worry about conflicts with anything you’ve already got in your org, and being unlocked gives you the confidence that what you see in the source repo is what you get in the package.

Package version last updated: October 15 2022

Source

There are a handful of features that I’d like to add, but its at the point now that it should be pretty useful for anyone that needs this kind of thing. Take a look, and please add any issues or feature requests to the Issues log in the GitHub repo!