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.

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.

Extending Flow Overview Video

Hi everyone! I recently worked on a video to help give an overview on when and why you might use Flow extensions. I also cover some good sample scenarios using various actions and also cover some lessons learned.

Check out the summary (with helpful clickable timestamps) below!

Here’s the video:

Slides (with the hyperlinks to the actions / other URLs) are here: https://docs.google.com/presentation/…

A Flow Screen Component that plays sounds

Created by Yumi Ibrahimzade

ABOUT
This is a Lightning Component that plays a sound effect.
It can be used on Flow Screens and Lightning Pages.
In order to play a sound, you have to upload the sound file as a Static Resource and then enter the name of the file to the “Sound File” parameter.

INSTALLATION

PARAMETERS
Sound File – Enter the name of the sound file.

CONSIDERATIONS
This component loads the sound file from Static Resources and plays it. So it is recommended to use smaller file sizes, otherwise it might take some time to load the file.

HOW TO USE
1- Upload the sound file as a Static Resource.

2- Add “SoundCMP” to your Flow Screen.
3- Enter the name of the sound file to the parameter called “Sound File”.

Use the Next Best Action REST API to execute a strategy and retrieve recommendation reactions

Quick guide on how to use the NBA REST API.  

Executing an NBA Strategy for a Given Record

Official documentation: https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_resources_nba_recommendations.htm

Example:

POST

/services/data/v45.0/connect/recommendation-strategies/Jasmine_Yogurt_Service/recommendations

Body:

{
"contextRecordId" : "5001U000006XEpjQAG", 
"maxResults" : 4,
"strategyContext":{}
}

Response:

Raw Response (in JSON):

{
   "executionId":"23e8f474-6fec-4b5d-b2b5-667ef7ffb03a",
   "onBehalfOfId":"5001U000006XEpj",
   "recommendations":[
      {
         "acceptanceLabel":"Add Hot Waffle Press",
         "description":"Add a new Stanley Waffle Cone Press",
         "imageUrl":"https://isnba--c.documentforce.com/file-asset-public/WaffleMaker1?oid=00D1U0000012w03&v=1",
         "rejectionLabel":"No Thanks",
         "target":{
            "id":"0pr1U00000057IYQAY",
            "name":"Add a Hot Waffle Press",
            "type":"Recommendation",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IY"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "name":"Dispatch_Field_Technician",
            "parameters":[

            ],
            "type":"Flow"
         }
      },
      {
         "acceptanceLabel":"Upgrade to an X120 Soft Serve",
         "description":"Upgrade to a Twenty Gallon Single Nozzle Soft Serve Machine",
         "imageUrl":"https://isnba--c.documentforce.com/file-asset-public/SoftServeMaker1?oid=00D1U0000012w03&v=1",
         "rejectionLabel":"No Thanks",
         "target":{
            "id":"0pr1U00000057IXQAY",
            "name":"Upgrade to an X120 Soft Serve",
            "type":"Recommendation",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IX"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "name":"Dispatch_Field_Technician",
            "parameters":[

            ],
            "type":"Flow"
         }
      },
      {
         "acceptanceLabel":"Offer refinance",
         "description":"0% Refinancing on existing equipment",
         "imageUrl":"https://isnba--c.documentforce.com/file-asset-public/refinance?oid=00D1U0000012w03&v=1",
         "rejectionLabel":"No Thanks",
         "target":{
            "id":"0pr1U00000057IZQAY",
            "name":"0% Refinancing!",
            "type":"Recommendation",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IZ"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "name":"Dispatch_Field_Technician",
            "parameters":[

            ],
            "type":"Flow"
         }
      }
   ],
   "trace":{
      "messages":[

      ],
      "nodes":[

      ]
   }
}

Retrieving Recommendation Reactions

Official documentationhttps://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_resources_nba_reactions.htm#connect_resources_nba_reactions

Example:

GET

/services/data/v45.0/connect/recommendation-strategies/reactions?targetId=0pr1U00000057IUQAY

This will return back all of the recommendations reactions for the recommendation with ID 0pr1U00000057IUQAY.

Similarly you can also query for the contextRecordId (e.g. Case with ID 5001U000006XEpjQAG, account, etc.)

GET

/services/data/v45.0/connect/recommendation-strategies/reactions/?contextRecordId=5001U000006XEpjQAG

RAW RESPONSE:

{
   "currentPageUrl":"/services/data/v45.0/connect/recommendation-strategies/reactions?page=0&pageSize=100",
   "nextPageUrl":"/services/data/v45.0/connect/recommendation-strategies/reactions?page=1&pageSize=100",
   "reactions":[
      {
         "contextRecord":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "createdBy":{
            "id":"0051U000004fPpPQAU",
            "url":"/services/data/v45.0/chatter/users/0051U000004fPpPQAU"
         },
         "createdDate":"2019-02-28T19:13:43.000Z",
         "id":"0ps1U000000TNzxQAG",
         "onBehalfOf":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "reactionType":"Accepted",
         "strategy":{
            "id":"0sg1U0000004edzQAA",
            "nameAtSnapshot":"Jasmine_Yogurt_Service"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "nameAtSnapshot":"Dispatch_Field_Technician"
         },
         "targetRecord":{
            "id":"0pr1U00000057IUQAY",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IUQAY"
         },
         "url":"/services/data/v45.0/connect/recommendation-strategies/reactions/0ps1U000000TNzxQAG"
      },
      {
         "contextRecord":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "createdBy":{
            "id":"0051U000004fPpPQAU",
            "url":"/services/data/v45.0/chatter/users/0051U000004fPpPQAU"
         },
         "createdDate":"2019-02-28T19:13:43.000Z",
         "id":"0ps1U000000TNzyQAG",
         "onBehalfOf":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "reactionType":"Accepted",
         "strategy":{
            "id":"0sg1U0000004edzQAA",
            "nameAtSnapshot":"Jasmine_Yogurt_Service"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "nameAtSnapshot":"Dispatch_Field_Technician"
         },
         "targetRecord":{
            "id":"0pr1U00000057IUQAY",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IUQAY"
         },
         "url":"/services/data/v45.0/connect/recommendation-strategies/reactions/0ps1U000000TNzyQAG"
      },
      {
         "contextRecord":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "createdBy":{
            "id":"0051U000004fPpPQAU",
            "url":"/services/data/v45.0/chatter/users/0051U000004fPpPQAU"
         },
         "createdDate":"2019-02-28T19:13:43.000Z",
         "id":"0ps1U000000TNzzQAG",
         "onBehalfOf":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "reactionType":"Accepted",
         "strategy":{
            "id":"0sg1U0000004edzQAA",
            "nameAtSnapshot":"Jasmine_Yogurt_Service"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "nameAtSnapshot":"Dispatch_Field_Technician"
         },
         "targetRecord":{
            "id":"0pr1U00000057IUQAY",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IUQAY"
         },
         "url":"/services/data/v45.0/connect/recommendation-strategies/reactions/0ps1U000000TNzzQAG"
      },
      {
         "contextRecord":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "createdBy":{
            "id":"0051U000004fPpPQAU",
            "url":"/services/data/v45.0/chatter/users/0051U000004fPpPQAU"
         },
         "createdDate":"2019-02-28T19:13:43.000Z",
         "id":"0ps1U000000TO00QAG",
         "onBehalfOf":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "reactionType":"Accepted",
         "strategy":{
            "id":"0sg1U0000004edzQAA",
            "nameAtSnapshot":"Jasmine_Yogurt_Service"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "nameAtSnapshot":"Dispatch_Field_Technician"
         },
         "targetRecord":{
            "id":"0pr1U00000057IUQAY",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IUQAY"
         },
         "url":"/services/data/v45.0/connect/recommendation-strategies/reactions/0ps1U000000TO00QAG"
      },
      {
         "contextRecord":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "createdBy":{
            "id":"0051U000004fPpPQAU",
            "url":"/services/data/v45.0/chatter/users/0051U000004fPpPQAU"
         },
         "createdDate":"2019-03-07T05:11:29.000Z",
         "id":"0ps1U000000TQzqQAG",
         "onBehalfOf":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "reactionType":"Accepted",
         "strategy":{
            "id":"0sg1U0000004edzQAA",
            "nameAtSnapshot":"Jasmine_Yogurt_Service"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "nameAtSnapshot":"Dispatch_Field_Technician"
         },
         "targetRecord":{
            "id":"0pr1U00000057IUQAY",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IUQAY"
         },
         "url":"/services/data/v45.0/connect/recommendation-strategies/reactions/0ps1U000000TQzqQAG"
      },
      {
         "contextRecord":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "createdBy":{
            "id":"0051U000004fPpPQAU",
            "url":"/services/data/v45.0/chatter/users/0051U000004fPpPQAU"
         },
         "createdDate":"2019-03-18T18:21:15.000Z",
         "id":"0ps1U000000TYoYQAW",
         "onBehalfOf":{
            "id":"5001U000006XEpjQAG",
            "url":"/services/data/v45.0/chatter/records/5001U000006XEpjQAG"
         },
         "reactionType":"Accepted",
         "strategy":{
            "id":"0sg1U0000004edzQAA",
            "nameAtSnapshot":"Jasmine_Yogurt_Service"
         },
         "targetAction":{
            "id":"3001U000000oZixQAE",
            "nameAtSnapshot":"Dispatch_Field_Technician"
         },
         "targetRecord":{
            "id":"0pr1U00000057IUQAY",
            "url":"/services/data/v45.0/connect/recommendations/0pr1U00000057IUQAY"
         },
         "url":"/services/data/v45.0/connect/recommendation-strategies/reactions/0ps1U000000TYoYQAW"
      }
   ]
}

Integration Guide for Next Best Action

In this guide, we’ll walk you through all the different ways that you can connect to sources of insight from Next Best Action to generate and enhance recommendations and to pull in relevant properties for decision making. Note – the Generate and Enhance elements are new updates, shipping with the Summer ’19 Release.

There are 3 main integration points, all of which require Apex invocable actions:

  1. Generate
  2. Enhance
  3. External connections

The Generate element enables you to create new in-memory, on-the-fly recommendations, either from an external data source or from other Salesforce objects.

The Enhance element enables you to modify a given set of recommendations to include real-time updates, personalized information, or AI driven prediction scores.

And finally you can create an external connection to fetch properties in JSON format from external services. For example, you can connect to a Heroku endpoint to obtain a credit score for a given customer.

Let’s dive deeper into each one. You can install the strategies and invocable actions used in the examples by installing this package in your org. Note – your org must be using the Summer ’19 (or later) release.

Generate

Instead of defining recommendations manually as records in Salesforce, you can use the Generate element to dynamically create temporary, in-memory recommendations as a part of the current strategy execution. These dynamic recommendations can be sourced from external data sources like Commerce Cloud or a SQL database or from other Salesforce objects like Accounts or Products.

For example, you can build a dashboard of prioritized accounts to connect with.

Or create recommendations sourced from the Products object.

The Generate element has the following characteristics:

@InvocableMethod(
label='Related Wikipedia Pages'
description='Recommend wikipages that are related to the named input wikipage')
  • It can pass any number of inputs to the Apex Action, either as lists or list of lists of primitives, SObjects, and user-defined Apex objects. To provide more than one input, the input parameter MUST be a list or list of lists of a user-defined Apex object (e.g. a custom class called DataContainer).
List<String> relatedTo

OR

global class DataContainer {
@InvocableVariable
public string accountId;
}
____
global static List<List<Recommendation>> invocableMethod(List<DataContainer> inputData)
  • It returns a list of recommendations. InvocableMethods support returning either a list of an SObject type or a list of lists of an SObject type. As the Enhance element operates on a list of recommendations and not a single recommendation, the method must return a List<List<Recommendation>>.
global static List<List<Recommendation>> invocableMethod(List<DataContainer> inputData)

Example: Recommend accounts the current user should follow up on today
Suppose you want to recommend the top accounts your salespeople should meet with today because the last interaction was more than 90 days ago. With the Generate element, you can call an Apex Action which does a SOQL query for Accounts where the Owner is the logged in user (the salesperson) and identify those accounts where the last contact date was more than 90 days ago. The relevant accounts can be returned in the form of recommendations.

The strategy can be as simple as the Generate element with an Output element.

In the configuration dialog of the Generate element, select Accounts to Follow Up Today as the Apex Action and pass in ‘$User.id’ as an input parameter.

The Generate element, in turn, will call the getAccounts invocable method in the Generate_GetAccountsToFollowUp Apex class. Notice how you can fetch the relevant accounts and create a new list of recommendations where the Description includes the name of the account (account.Name) and the number of days since last contact (daysSinceLastContact).

global class Generate_GetAccountsToFollowUp {  
//NOTE: You can get the user id of the current user by also doing UserInfo.getUserId(). 
    @InvocableMethod(label='Accounts to Follow Up Today' 
                     description='Recommend accounts the current user should follow up on today')
    global static List<List<Recommendation>> getAccounts(List<String> inputData){
        List<List<Recommendation>> outputs = new List<List<Recommendation>>();
        Integer daysSinceLastContact;
        Account[] accounts = [SELECT Name, Description, LastContactDate__c, OwnerId FROM Account WHERE OwnerId = :inputData[0]];
        List<Recommendation> recs = new List<Recommendation>(); 
        for (Account account:accounts) {
            if (account.LastContactDate__c != null){
                daysSinceLastContact = account.LastContactDate__c.daysBetween(date.today());
                if (daysSinceLastContact > 90){
                    Recommendation rec = new Recommendation(
                        Name = account.Name,
                        Description = 'Connect with the ' + account.Name + ' account, the last interaction was '+ daysSinceLastContact + ' days ago.',
                        //Pre-req: Create a screen flow with the name simpleFlow                        
                        ActionReference = 'simpleFlow',
                        AcceptanceLabel = 'View'
                    );
                    recs.add(rec);
                }
            }
        }
        outputs.add(recs);
        return outputs; 
    }
}

Example: Get related Wikipedia pages as recommendations
You can also use the Generate node to create recommendations for external data sources. For example, consider a scenario where you want to show related Wikipedia pages, as recommendations, for a given topic. The Generate element enables you to call an Apex Action which in turn makes a GET request to a Wikipedia endpoint. The pages are returned in the form of recommendations.

A strategy with the Generate element can be as simple as the Generate element with an Output element.

In the configuration dialog of the Generate element, select Related Wikipedia Pages as the Apex Action and pass in ‘India’ as an input parameter.

The Generate element will call the getPages invocable method in the Generate_GetWikiPages Apex class. Notice how there is a mapping of Wikipedia articles to recommendation fields.

//Guidance around Apex class definitions: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_defining.htm
global class Generate_GetWikiPages {
    //InvocableMethod Guidance: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation_InvocableMethod.htm
    //Input parameter must be a list or list of lists of either primitive type or Object type
    @InvocableMethod(
        label='Related Wikipedia Pages'
        description='Recommend wikipages that are related to the named input wikipage')
    global static List<List<Recommendation>> getPages(List<String> relatedTo){
        
        List<List<Recommendation>> outputs = new List<List<Recommendation>>();
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        String urlName = relatedTo[0].replace(' ','_');
        // Pre-req: Set up a Remote Site Setting for this to work.
        request.setEndpoint('https://en.wikipedia.org/api/rest_v1/page/related/'+urlName);
        request.setMethod('GET');
        HttpResponse response = http.send(request);
        if(response.getStatusCode() == 200) {
            Map<String, Object> result = (Map<String, Object>) 
                JSON.deserializeUntyped(response.getBody());
            List<Recommendation> recs = new List<Recommendation>();
            for(Object item : (List<Object>)result.get('pages')) {
                Map<String, Object> page = (Map<String, Object>) item;
                Recommendation rec = new Recommendation(
                    Name = (String)page.get('displaytitle'),
                    Description = (String)page.get('extract'),
                    //Pre-req: Create a screen flow with the name simpleFlow
                    ActionReference = 'simpleFlow',
                    AcceptanceLabel = 'View'
                );
                recs.add(rec);
            }
            outputs.add(recs);
        }
        return outputs;
    }
}

Enhance

The Enhance element allows you to modify a given set of recommendations on-the-fly, every time the strategy is executed. These recommendations could be static recommendations that live as records in Salesforce or dynamic recommendations sourced from external data sources or other Salesforce objects. For example, you can use the enhance element to calculate a discount % for your customers based on how long the account has been with your company or you can use it as a means for A-B testing two branches of recommendations.

The Enhance element has the following characteristics:

@InvocableMethod(
label='Enhance with Discounts Based on Age' 
description='Returns an enhanced set of recommendations with appropriate discounts')
  • IMPORTANT – It can pass any number of inputs to the Apex Action. The input parameter MUST be a list or list of lists of a user-defined Apex object (e.g. a custom class called DataContainer) and the user-defined Apex object MUST include a List<Recommendation> variable. The List<Recommendation> variable will be automatically set with the recommendations flowing into the Enhance element.
global class DataContainer {
    @InvocableVariable
    public string accountId; 
    
    @InvocableVariable
    public List<Recommendation> recommendations;
}
________
global static List<List<Recommendation>> invocableMethod(List<DataContainer> inputData)
  • It returns a list of recommendations, List<List<Recommendation>>. Note – these recommendation edits are only in-memory and are not persisted after the strategy execution.
global static List<List<Recommendation>> invocableMethod(List<DataContainer> inputData)

Example: Enhance Recommendations with Discounts Based on Customer Age

Suppose you use Next Best Action to provide upsell recommendations. You’d like to reward your loyal customers by adding a 5 % discount to your product recommendations if the customer has been with your company for more than 1 year, 10% for more than 2 years, and 20% for more than 5 years. With the Enhance element, you can call an Apex Action which does a SOQL query to fetch the Account age and append it to the description of all incoming recommendations.

The strategy with the Enhance element can be as simple as Load → Enhance → Output. All recommendations retrieved or loaded by the Load element are implicitly passed as a list of recommendations to the underlying invocable method.

In the configuration dialog of the Enhance element, select Enhance with Discounts Based on Age as the Apex Action and pass in ‘$Record.id’ as the input parameter.

The Enhance element will in turn call the getDiscounts invocable method in the Enhance_GetAccountDiscount class. Notice how Description of each recommendation has a discount value appended to it (r.Description + ‘ with a 5% discount’).

global class Enhance_GetAccountDiscount {
    @InvocableMethod(label='Enhance with Discounts Based on Age' description='Returns an enhanced set of recommendations with appropriate discounts')
    global static List<List<Recommendation>> getDiscounts(List<DataContainer> inputData){
        
        List<Recommendation> recommendations = inputData[0].recommendations; 
        List<List<Recommendation>> outputs = new List<List<Recommendation>>();       
        Account[] accounts = [SELECT Name, Description,CreatedDate, id FROM Account WHERE id = :inputData[0].accountId];
           Double ageAccountMonths = accounts[0].CreatedDate.date().monthsBetween(date.today()); 
        Double ageAccount = ageAccountMonths/12;       
        List<Recommendation> returnedRecommendations = new List<Recommendation>(); 
        for (Recommendation r:recommendations){
            if(ageAccount > 1){
                r.Description = r.Description + ' with a 5% discount';
            }
            else if (ageAccount > 2){
                r.Description = r.Description + ' with a 10% discount';
            }
            else if (ageAccount > 5){
                r.Description = r.Description + ' with a 20% discount';
            }
            returnedRecommendations.add(r);
        }
        outputs.add(returnedRecommendations);                 
        return outputs;
        
    }

}

External Connections

External Connections have the following characteristics:

  • Like Enhance and Generate elements, they work in tandem with an Apex Action created using by adding the Invocable Action annotation to an Apex Class.
  • Unlike Enhance, External Connections do not take a set of recommendations as inputs. They return a single set of properties that can be referenced in expressions but not mapped to recommendations. Unlike Generate, External Connections do not result in the creation of new, temporary recommendations.
  • They run before the strategy is processed, and not during the processing of the strategy.
  • Their values are globally accessible (i.e you can use them in every element of the strategy).
  • They can return a broad range of different properties (Generate and Enhance always return a List of Recommendations).

Example: Calling a Heroku endpoint to fetch a credit score
Let’s suppose that you make certain upsell offers to your customers based on their credit-worthiness. For example, you offer them a 0% refinancing only if their credit score is above 750. Credit scores aren’t typically something you would store in Salesforce because they change dynamically and frequently. Using an external connection, you can call an Apex Action which will call an external endpoint and return the credit score given some information about the user.

global with sharing class GetFranchiseeData {
    @InvocableMethod
    public static List<Results> getCreditScore(List<Requests> requests)
       {
            Http http = new Http();
            HttpRequest request = new HttpRequest();
               //Make sure you add the below endpoint as a remote site setting
            request.setEndpoint('https://repair-service.herokuapp.com/api/creditScore?customerId='+ requests[0].custId);
            request.setMethod('GET');
            HttpResponse response = http.send(request);
             Results curResult = new Results();
            curResult.creditScore = Integer.valueOf(response.getBody());
           
            List<Results> resultsList = new List<results>();
            resultsList.add(curResult);
            return resultsList;
       }
    
    global class Requests {
        @InvocableVariable
      global String custId;
        
    }
    
    global class Results {
        @InvocableVariable
      global Integer creditScore;
    }
}

To call the above Apex Action, create an external connection and pass the Account’s External ID (If you pass values less than 4000, the API will return 550. It will return 810 for values greater than or equal to 4000.)

You can filter the Zero % Refinancing Offer based on the Credit Score by referencing the connection in the Filter node.

Salesforce Flow Tutorials

Starting out with Flow? Here are some great introductions

New! 6 New Videos to Help You Understand Flow Builder

Home Pages

Salesforce Admins Automation Page

Salesforce Flow Developer Center

Trailheads

Paid Courses

Udemy

Andy Utkan

Videos – General Flow Learning

Topic-Specific Videos

Developer-Centric Videos

Automation Guides

Official Sites

Community Sites

“Unofficial Flow” Community Site (unofficialsf.com)

Introduction: Using Salesforce Flow With Quip

The new Quip Flow Actions for Salesforce Flow open up new ways to combine the shared, universal accessibility of Quip with the power of Salesforce.

Video

Installation

Documentation

Here are some examples:

#1: Update Salesforce with Data From Different Quip Docs

In this example, Quip docs have been created for different Salesforce contacts. One scenario where this can be useful is when an enterprise has employees that don’t have Salesforce licenses. If each of those employees has a Quip document and a Contact representing them, the documents can be used to display data from Salesforce and collect data from the employees. (Note that this uses the new Spring ’19 Flow Builder):

A key element of this is that the name of each quip document is formed using a Flow formula:

To configure the Get Quip Sheet Data flow action, we do the following:

  1. Provide the name of the corresponding Quip doc. Start with a contact name, generate the ConstructedName using that formula, and pass it in to the flow action:

2) For each piece of data that we want to extract from the quip doc, we reference it by specifying the label value of the cell to its immediate left. This is one of three ways you can specify a specific cell value:

3) Finally, map the retrieved values from the Quip document to flow storage variables so you can use them later in the flow:

Side Discussion 1: Error Messags

Here I’ve created variables to map the Error Message and Is Success outputs. You don’t have to actually use these variables, but if you take the time to define them, then any error messages you get will show up in the debugger like this:

If I try to immediately update the Contact as soon as I get the data out of the Quip document like this:

Side Discussion 2: Mixing Callouts and Transactions

In my first attempt at setting up the flow, it looked like this:

When I ran the flow in the debugger, though, I got this error:

I had forgotten that you can’t use a callout (here, the flow action is “calling out” from Salesforce to the quip api endpoint) in the middle of an open Salesforce transaction. When flow loops, Salesforce prepares a single transaction so it can commit all of the loops with one database action, in the name of resource conservation.

I needed to restructure my flow and got a key hint from a brand new tooltip:

The tooltips in Flow Builder are completely overhauled and have a lot of extremely timely tips; they’re not just simple definitions.

The resulting flow uses a collection operator. Each time we go through the loop, the modified contact is added to a new “updatedContacts” collection variable that can later be used to carry out the update.

#2 Clone a Quip Template and Update the Resulting Quip Sheets with Salesforce Data

Now let’s change direction and go From Salesforce To Quip. The use case is this: Given a set of Contacts, generate a custom Quip sheet for each one and fill it with Contact data. Then email the URL to each of the Contacts.

This might be useful if you want to generate shareable reports for your customers using data stored in Salesforce.

This flow features 2 of the new Quip Flow Actions:

Clone Quip Document is a simple action that duplicates a named document. Store Data in Quip Document does the heavy lifting of inserting flow data into specific Quip cells.

We create a Quip document to act as our template:

The Clone action generates a new quip sheet based on this template, and give it a name using a formula technique like the one described above.

The Store Data in Quip action lets you specify up to ten pieces of data to write to 10 cells. You can use several approaches to specify a target cell:

  1. Specify a label and indicate that the target cell is immediately to the right of that label (that’s what we’re doing)
  2. Specify a label and indicate that the target cell is immediately below that label
  3. Specify both a column label and a row label, in which case the target cell is the intersection of the column and row. This is great for updating a single cell in a table.
  4. Specify a cell by its absolute address (for example: B2)

The flow produces sheets like this:

and generates emails like this:

Row Operations

You can use the AddRowToQuipSheet action to add a row to a sheet using data from Salesforce or update an existing row.

Here’s an example of an Add:

And here’s a sample showing an Update:

Walkthroughs of these are provided in the video (see link at the top of this article.)

Quip User Management with Flow

There are also Flow Actions to Add a User to a Folder and Remove them from a Folder so you can add quip folder activation to your onboarding flows.

Quip Document Management with Flow

There are also Flow Actions to Rename a Quip Folder and Add a Quip Document to a Folder to make it possible to automate management of your Quip data.

Learn More:

Installation

Documentation