From the Salesforce Heroku Team: Extend Flows with Heroku Compute and External Services

This detailed blog post from Salesforce shows how to use a specification created using the Open API Spec to generate Flow Action via External Services. So far, that’s not really new. But then the authors show how you can create a Heroku-based implementation of that API using Node.js. Finally, they show how platform events can be used to communicate effectively from Heroku back to a triggered flow.

Check it out.

We’re trying a new Comment extension. How do you like it?

At the bottom of the screen you’ll find a new commenting system. Opinions welcome….

Chance for Glory 3: Contribute to Two Cool New Flow Extensions: ProcessBuilderAndWorkflowRulesConverter and Flexcards

I wanted to invite anyone in the Flowhana who can write some code to participate in a couple of team efforts that are spinning up.

On the Apex side, we want to extend the Process Builder Converter tool. Current work items include adding support for PB processes that couldn’t be converted in Summer ’20 but can be converted in Winter ’21 (example, if you use a related object reference), and implementing the first conversion support for Workflow Rules.

On the Lightning Web Components side, we’re creating a new screen component called a Flexcard.

Here are the stories currently being focused on:

https://quip.com/LIZHAVL6hP0I/Process-Builder-and-Workflow-Rules-Converter

https://quip.com/eZGFAkkYliPw/Cards-Screen-Component

You don’t have to be an expert coder to contribute to these projects. In fact, one of the coolest things about UnofficialSF is watching noncoders become coders, beginner coders become intermediate coders, and intermediate coders build advanced skills. I’d say that the basic prerequisites are:

  1. you need to have successfully built either a flow invocable action or a flow lwc screen component.
  2. you need to understand git reasonably well (up through section 3.5 of this book)

As long as you’re willing to be responsive to code reviews and are reasonably diligent about trying to write clear code, you’re very welcome.

You also don’t have to worry that you’re making a big commitment. First of all, the work is divided into reasonably sized stories, and if you find yourself getting in too deep, don’t worry that you’ll leave others annoyed. This is hobbyist stuff and it needs to be fun and satisfying or it’s not worth doing.

Interested? Drop a comment below and we’ll get in touch.

Introducing Flow BasePacks

These reusable libraries of useful actions and components are generally used by other, more specific Flow tools.

There will be one for Actions and one for ScreenComponents. This is being done because there are really good reasons to put the Actions in a Managed Package but it’s currently impossible to create libraries of LWC’s in Managed Packages and access those LWC’s from an LWC that isn’t in the same namespace. Splitting them up does the trick.

FlowActionsBasePack

FlowActionsBasePack is a managed package. This makes it easier to install (details below).

Install and Learn More

FlowScreenComponentsBasePack

FlowScreenComponentsBasePack is an unlocked package. This is necessary because Lightning Web Components can’t use custom libraries yet if they’re managed (and thus have their own namespace)..

FlowScreenComponents BasePack requires the presence of FlowActions BasePack, because some of the screen components inside of it rely on Apex classes that are included in FlowActions BasePack. So you’ll need to install FlowActions BasePack first.

Install and Learn More

Legacy Information

There is an older FlowBaseComponents that is now obsolete, and we do not recommend the use of it as a prerequisite in new development. The existing FlowBaseComponents package version 1.3.4 will remain available for use with components that require it. We’re going to be encouraging extension developers to make use of these new base packs, going forward, and don’t plan to enhance FlowBaseComponents

Going forward, we’re breaking FlowBaseComponents into two packages:

Salesforce Record Automation Benchmarking by Luke Freeland

Luke Freeland from the blog https://metillium.com/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

From Adam White – Making a Dataloader on Guardrails in Flow: Featuring Mass Transactor

Over on my new blog Declarative Ninja I’ve made a video on on how I built a dataloader ‘on guardrails’ where you can take a CSV file and perform mass DML in a controlled environment via Flow.

Check it out!

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

Vibhor Goel from the blog https://www.accidentalcodersf.com/ 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
    @AuraEnabled
    public String accountName;
 
    @AuraEnabled
    public String partnerRole;
 
    @AuraEnabled
    public Boolean isPrimary;

    @AuraEnabled
    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
  @InvocableMethod
  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) {
      setOppIds.add(rq.opportunityId);
      setAccIds.add(rq.accountId);
    }

    // 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
        Id,
        (
          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;

          opportunityPartnersSet.add(opw);
        }

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

      responseWrapper.add(response);
    }

    // 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 {
    @InvocableVariable
    global String errors;

    @InvocableVariable
    global String successful;

    @InvocableVariable
    public List<OpportunityPartnersWrapper> outputCollection;

    @InvocableVariable
    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
**/
@IsTest
public with sharing class GetOpportunityPartnersActionTest {


    @isTest
    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'
                );

            accounts.add(a);
        }
        
        insert accounts;

        // create test opportunity
        Date closeDate = System.today();
        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;
        requestList.add(request);

        // 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.

Source

Source

From Gidi Abramovich: Using Flow and Triggers with Custom Metadata Types to Dynamically Update Highlight Panel Images

As Gidi writes:

 I wanted to display a flag image on the account object according to the ‘BillingCountry’ field. 

Check it out!

25 New Flow Exercises from SalesforceFlowLab

We came across this new content site:

It has a list of 25 different Flow posts/tutorials/exercises/walkthroughs.

Check it out!