SFDC Stop - Always the latest about Salesforce


Full Tutorial Series with videos, free apps, live sessions, salesforce consulting and much more.


Telegram logo   Join our Telegram Channel

Tuesday, 25 October 2022

How to pass data from lwc to screen flow in salesforce?

Hello Trailblazers,


In the previous post, we learned: How to pass data from screen flow to lwc in salesforce? In this post we're going to learn, how we can pass data from lightning web component (LWC) to the parent screen flow in salesforce. Let's begin by creating our LWC first.


LWC: createLead

This LWC will be used to create an instance of lead with some basic information (first name, last name, company) and pass it to the flow, it won't insert the lead record in salesforce. Once the lead record is received by the flow, we'll populate some other fields with default values like: Lead Source and Rating and then create the lead record from the flow itself. Let's Begin!

createLead.html

We have a simple lightning-card element and 2 lightning-input-field elements (to store first and last name). We also have 1 lightning-combobox element (to store company) as shown below:
<template>
    <lightning-card title={title}>
        <lightning-layout multiple-rows="true">
            <lightning-layout-item size="12" padding="around-small">
                <lightning-input type="text" name="FirstName" label="First Name" required></lightning-input>
            </lightning-layout-item>
            <lightning-layout-item size="12" padding="around-small">
                <lightning-input type="text" name="LastName" label="Last Name" required></lightning-input>
            </lightning-layout-item>
            <lightning-layout-item size="12" padding="around-small">
                <lightning-combobox label="Company" name="Company" placeholder="Select Company" options={companies} required></lightning-combobox>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>
Notice that all the fields in our form, are marked as requied. We'll pass the title of lightning-card card from our screen flow which will be stored in title property in js and is referenced here in html by the title attribute as title={title}. Let's see the JavaScript code now:

createLead.js

Have a look at the code snippet below. The default value for our title property is Create Lead. We also have another public attribute named leadRecord, this attribute will be passed to our screen flow with all the lead details. We have a companies array in our code which is used by lightning-combobox, so that the user can select a company for the lead. It's a text field in salesforce but let's consider hypothetically that this companies array is populated by doing a callout to an external system from LWC. We don't want our users to write any company name for our lead record, therefore, the flow to create a lead is as follows:

Screen flow launched -> LWC  component rendered inside screen flow -> Get valid company names by performing a callout to an external system from LWC -> User enter the first name, last name, select a company and click next -> Lead record passed from LWC to screen flow -> Screen flow add some default values for other fields -> Screen flow creates a new lead record in salesforce
import { api, LightningElement } from 'lwc';

export default class CreateLead extends LightningElement {

    @api title = 'Create Lead';
    @api leadRecord = {};

    companies = [
        {
            "label": "Apple",
            "value": "Apple"
        },
        {
            "label": "Saudi Aramco",
            "value": "Saudi Aramco"
        },
        {
            "label": "Microsoft",
            "value": "Microsoft"
        },
        {
            "label": "Alphabet (Google)",
            "value": "Alphabet (Google)"
        },
        {
            "label": "Amazon",
            "value": "Amazon"
        },
        {
            "label": "Tesla",
            "value": "Tesla"
        },
        {
            "label": "Berkshire Hathaway",
            "value": "Berkshire Hathaway"
        },
        {
            "label": "UnitedHealth",
            "value": "UnitedHealth"
        },
        {
            "label": "Johnson & Johnson",
            "value": "Johnson & Johnson"
        },
        {
            "label": "Exxon Mobil",
            "value": "Exxon Mobil"
        },
        {
            "label": "Visa",
            "value": "Visa"
        },
        {
            "label": "Walmart",
            "value": "Walmart"
        },
        {
            "label": "JPMorgan Chase",
            "value": "JPMorgan Chase"
        },
        {
            "label": "Meta Platforms (Facebook)",
            "value": "Meta Platforms (Facebook)"
        },
        {
            "label": "Chevron",
            "value": "Chevron"
        },
        {
            "label": "TSMC",
            "value": "TSMC"
        },
        {
            "label": "Eli Lilly",
            "value": "Eli Lilly"
        },
        {
            "label": "LVMH",
            "value": "LVMH"
        },
        {
            "label": "NVIDIA",
            "value": "NVIDIA"
        },
        {
            "label": "Procter & Gamble",
            "value": "Procter & Gamble"
        },
        {
            "label": "Mastercard",
            "value": "Mastercard"
        },
        {
            "label": "Nestlé",
            "value": "Nestlé"
        },
        {
            "label": "Tencent",
            "value": "Tencent"
        },
        {
            "label": "Home Depot",
            "value": "Home Depot"
        },
        {
            "label": "Bank of America",
            "value": "Bank of America"
        },
        {
            "label": "Samsung",
            "value": "Samsung"
        },
        {
            "label": "Roche",
            "value": "Roche"
        },
        {
            "label": "Kweichow Moutai",
            "value": "Kweichow Moutai"
        },
        {
            "label": "AbbVie",
            "value": "AbbVie"
        },
        {
            "label": "Pfizer",
            "value": "Pfizer"
        },
        {
            "label": "Merck",
            "value": "Merck"
        },
        {
            "label": "Coca-Cola",
            "value": "Coca-Cola"
        },
        {
            "label": "Pepsico",
            "value": "Pepsico"
        },
        {
            "label": "Novo Nordisk",
            "value": "Novo Nordisk"
        },
        {
            "label": "Costco",
            "value": "Costco"
        },
        {
            "label": "Reliance Industries",
            "value": "Reliance Industries"
        },
        {
            "label": "ICBC",
            "value": "ICBC"
        },
        {
            "label": "Oracle",
            "value": "Oracle"
        },
        {
            "label": "Thermo Fisher Scientific",
            "value": "Thermo Fisher Scientific"
        },
        {
            "label": "Alibaba",
            "value": "Alibaba"
        },
        {
            "label": "Shell",
            "value": "Shell"
        },
        {
            "label": "McDonald",
            "value": "McDonald"
        },
        {
            "label": "Walt Disney",
            "value": "Walt Disney"
        },
        {
            "label": "ASML",
            "value": "ASML"
        },
        {
            "label": "Toyota",
            "value": "Toyota"
        },
        {
            "label": "Broadcom",
            "value": "Broadcom"
        },
        {
            "label": "Danaher",
            "value": "Danaher"
        },
        {
            "label": "Cisco",
            "value": "Cisco"
        },
        {
            "label": "T-Mobile US",
            "value": "T-Mobile US"
        },
        {
            "label": "Astrazeneca",
            "value": "Astrazeneca"
        },
        {
            "label": "Wells Fargo",
            "value": "Wells Fargo"
        },
        {
            "label": "Accenture",
            "value": "Accenture"
        },
        {
            "label": "Novartis",
            "value": "Novartis"
        },
        {
            "label": "Abbott Laboratories",
            "value": "Abbott Laboratories"
        },
        {
            "label": "L'Oréal",
            "value": "L'Oréal"
        },
        {
            "label": "Salesforce",
            "value": "Salesforce"
        },
        {
            "label": "ConocoPhillips",
            "value": "ConocoPhillips"
        },
        {
            "label": "Bristol-Myers Squibb",
            "value": "Bristol-Myers Squibb"
        },
        {
            "label": "Verizon",
            "value": "Verizon"
        },
        {
            "label": "China Construction Bank",
            "value": "China Construction Bank"
        },
        {
            "label": "Texas Instruments",
            "value": "Texas Instruments"
        },
        {
            "label": "Linde",
            "value": "Linde"
        },
        {
            "label": "United Parcel Service",
            "value": "United Parcel Service"
        },
        {
            "label": "Adobe",
            "value": "Adobe"
        },
        {
            "label": "Nextera Energy",
            "value": "Nextera Energy"
        },
        {
            "label": "China Mobile",
            "value": "China Mobile"
        },
        {
            "label": "Tata Consultancy Services",
            "value": "Tata Consultancy Services"
        },
        {
            "label": "Nike",
            "value": "Nike"
        },
        {
            "label": "CATL",
            "value": "CATL"
        },
        {
            "label": "Agricultural Bank of China",
            "value": "Agricultural Bank of China"
        },
        {
            "label": "Amgen",
            "value": "Amgen"
        },
        {
            "label": "Comcast",
            "value": "Comcast"
        },
        {
            "label": "Morgan Stanley",
            "value": "Morgan Stanley"
        },
        {
            "label": "Hermès",
            "value": "Hermès"
        },
        {
            "label": "Philip Morris",
            "value": "Philip Morris"
        },
        {
            "label": "TotalEnergies",
            "value": "TotalEnergies"
        },
        {
            "label": "Charles Schwab",
            "value": "Charles Schwab"
        },
        {
            "label": "Raytheon Technologies",
            "value": "Raytheon Technologies"
        },
        {
            "label": "QUALCOMM",
            "value": "QUALCOMM"
        },
        {
            "label": "Netflix",
            "value": "Netflix"
        },
        {
            "label": "BHP Group",
            "value": "BHP Group"
        },
        {
            "label": "Royal Bank Of Canada",
            "value": "Royal Bank Of Canada"
        },
        {
            "label": "PetroChina",
            "value": "PetroChina"
        },
        {
            "label": "Honeywell",
            "value": "Honeywell"
        },
        {
            "label": "Elevance Health",
            "value": "Elevance Health"
        },
        {
            "label": "AT&T",
            "value": "AT&T"
        },
        {
            "label": "CVS Health",
            "value": "CVS Health"
        },
        {
            "label": "Lockheed Martin",
            "value": "Lockheed Martin"
        },
        {
            "label": "Intuit",
            "value": "Intuit"
        },
        {
            "label": "Union Pacific Corporation",
            "value": "Union Pacific Corporation"
        },
        {
            "label": "Bank of China",
            "value": "Bank of China"
        },
        {
            "label": "IBM",
            "value": "IBM"
        },
        {
            "label": "Deere & Company",
            "value": "Deere & Company"
        },
        {
            "label": "Toronto Dominion Bank",
            "value": "Toronto Dominion Bank"
        },
        {
            "label": "Lowe's Companies",
            "value": "Lowe's Companies"
        },
        {
            "label": "Unilever",
            "value": "Unilever"
        },
        {
            "label": "HDFC Bank",
            "value": "HDFC Bank"
        },
        {
            "label": "Goldman Sachs",
            "value": "Goldman Sachs"
        },
        {
            "label": "Intel",
            "value": "Intel"
        },
        {
            "label": "Medtronic",
            "value": "Medtronic"
        }
    ];

    @api
    validate() {
        const inputFields = this.template.querySelectorAll('lightning-input');
        const comboBox = this.template.querySelector('lightning-combobox');
        const validity = {
            isValid: true,
            errorMessage: 'Please fill the required fields!'
        };
        inputFields.forEach(inputField => {
            if(inputField.checkValidity()) {
                this.leadRecord[inputField.name] = inputField.value;
            } else {
                validity.isValid = false;
            }
        });
        if(comboBox.checkValidity()) {
            this.leadRecord[comboBox.name] = comboBox.value;
        } else {
            validity.isValid = false;
        }
        return validity;
    }
}
We're not actually performing the callout here as our main task is to focus on data validation and pass data from lwc to screen flow in salesforce but you can think of it as a requirement and the solution which we've implemented for the same.

validate()

Let's discuss about our validate() method now. As you can see in the above code snippet, it's a public method with @api annotation. This method will be called automatically when the user clicks on Next button of our screen flow. You can perform any kind of validation here, for now, we'll just see if all the fields are filled or not. Another important part we're doing here is: We'll populate the leadRecord property which will then be passed to our screen flow.

If you see in the above code snippet, first of all, we got all the lightning input fields using this.template.querySelectorAll('lightning-input') and stored it in inputFields constant. Similarly, we queried the lightning combobox field using this.template.querySelector('lightning-combobox') and stored it in comboBox constant. Finally, we initialized a constant named validity with an object which has two properties isValid and errorMessage. This object will be returned by our validate method and can notify the flow that there's an error in LWC and the flow should not proceed ahead. We've initialized isValid to true, however it should be false if there is a validation error and the errorMessage that we provide here will be used by the flow to show a validation error on the flow screen. For our component, we've the error message as Please fill the required fields! as we'll show this error only if any of the required fields is not filled.

Next we've iterated over all the input fields using a forEach loop and we've checked validity of each input field using the checkValidity() method. This method will return true if the input field is valid, otherwise, it'll return false

If the input field is valid, we're populating the lead record's property with the correct value as: this.leadRecord[inputField.name] = inputField.value. Notice by going back to the HTML section that each input field has a name attribute associated with it and the value of that name attribute is nothing but the API name of the actual salesforce field like: FirstName, LastName, Company. Therefore, here we're populating leadRecord['FirstName'] = <value of FirstName field> and leadRecord['LastName'] = <value of LastName field>.

If the input field is not valid, we've setup isValid property of validity to false as: validity.isValid = false

A similar check is added for our lightning-combobox input as well. Finally, we returned our validity object. If you notice, we've done two things here:

  1. Validated the input in our lwc using the validate() method and returned the proper output.

  2. Populated the leadRecord object which will be passed to our screen flow

Out JavaScript part is complete here. Let's move on to the meta file now:

createLead.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="title" type="String" label="Title" role="inputOnly"></property>
            <property name="leadRecord" type="@salesforce/schema/Lead" label="Lead Record" role="outputOnly"></property>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
As you can see above, this is very much similar to the meta file that we added in our previous tutorial. We've added a target named lightning__FlowScreen so that the component can be used in flows. Under the configurations for this target, we have two properties:

  1. title: The title of our lightning-card and will be passed by our flow to lwc. Notice that it's of type String. The label is Title which is fine and role is inputOnly as we just want to get this variable’s value from the screen flow as an input, we don't want to pass it's value back to the flow if it's updated in our lwc.

  2. leadRecord: The type of this property is @salesforce/schema/Lead because it'll store the actual salesforce lead record as per the inputs by the user. The label is Lead Record and the role for this property is outputOnly. This is because we'll be passing the value of this variable from LWC to flow. We don't want the admin to set it's value from flow and pass it to LWC.

So, that's all for our LWC. Let's move on to the screen flow now!

Screen Flow: Create Lead

Our screen flow looks like this:

Variable: LeadRecord

First of all, let's create a variable named LeadRecord. It should be of data type Record and object name should be Lead as shown below:
This variable will be mapped to the leadRecord variable of our LWC. Click on Done

Screen: Populate Lead Record

In order to use it, let's configure our Populate Lead Record screen. This is where we'll embed our createLead LWC. Let's see what do we have in this:
As you can see above, on the left hand side, we can search for createLead lwc and it'll be available under the Custom section (because we've added lightning__FlowScreen in our LWC meta file). We can drag and drop this on our screen shown in the middle within our screenshot above. On the right hand side, we can specify the API name for this component like: CreateLead and the Title. This is the same title property that we marked as inputOnly in our targetConfig. This title will be displayed on our lightning-card. The title we entered here is: Let's create a new Lead

Now, it's time to configure our leadRecord property of LWC's targetConfig. Under the Advanced settings, click Manually assign variables and you'll find an option to assign Lead Record property of LWC to a flow variable as shown below:
As you can see above, we can choose our LeadRecord variable here that we created initially. This is where we're passing the output from our LWC to a screen flow variable. Isn't it so simple?

Make sure to populate Screen Properties with value Populate Lead Record as shown below:
Click on Done

Assignment: Assign Default Values

Now we have the lead record coming from LWC with FirstName, LastName and Company populated. Let's assign default values to our LeadSource and Rating as well using the Assignment component, as shown below:
Click on Done

Create Records: Create Lead

Finally, we'll use the Create Records component to create a new lead record using our LeadRecord variable as shown below:

Click on Done. Now, it's time to save our new flow, let's name it as Create Lead:

Click on Save. Make sure to activate it as well, so the highlighted button below is showing Deactivate:
Our flow is complete!

Add Screen Flow to Homepage

Now it's time to test our flow. Go to any lightning app (I am using the Sales app here). Click on the Edit Page section:
On the lightning page builder, follow the steps as shown below:
  1. Search for flow in the left sidebar under Components

  2. Drag and Drop the flow component on the lightning page

  3. Choose the Create Lead flow from the Flow Configurations

  4. Save the page

  5. Activate the page (if not done before) and set it as org default as we did in our previous tutorial.

Once the flow is ready, you can populate the information of the lead as shown below:
As you click on Next, a new lead will be created in the system. You can check the Lead record and it'll have all the information populated as shown below:
As you can see above, we have the Name (FirstName + LastName), Company, Lead Source and Rating populated as expected.

In case you don't fill any fields in our screen flow and click Next, you'll have the Error Message visible on the screen as shown below:
This error message is coming from our validate() method of LWC and is displayed by the flow automatically.

That's all for this tutorial. I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!!

Tuesday, 18 October 2022

Getting started with Composite API in Salesforce

Hello Trailblazers,


In this post we're going to learn about Composite Resources in salesforce. Composite resources can improve your application's performance by minimizing round trips between client and salesforce (server). Let's try to learn by an example.


Get account details with related contacts and opportunities from Salesforce

Let's consider this small requirement: You need to sync account records along with the related contacts and opportunities from Salesforce to an external system. Let's say you're syncing one account at a time using the account's id. You may think of a solution which consist of 3 steps:

  1. Get the account record using the account id by making a callout to standard Rest API
  2. Get the contact records related to the account record by making another callout to the standard Rest API
  3. Get the opportunity records related to the account record by making another callout to the standard Rest API

Therefore, you got your account record along with the related contacts and opportunities in 3 Rest API calls to salesforce. There can be other alternative approaches as well depending upon the use case and requirement. The goal here is to know about what's provided to us out of the box by salesforce and how can we leverage it.

Composite API in Action - Get Records

What if I tell you: You can fetch account, related contacts and related opportunities as well, all in a single API callout without writing any custom code? Yes that's possible using the Composite API. Let's see how:

I am not covering the client Authentication part (how to get access token from salesforce) in this blog as we've a full blog dedicated to it: How to connect to Salesforce with Postman?

Composite API can execute a series of REST API requests (like: the 3 requests for our use case, which we're planning to execute one by one) in a single POST request. It can also retrieve a list of other composite resources with a GET request.

You can send multiple REST API requests together using composite api and the thing to note here is: The output of one request can be used as the input to a subsequent request. All requests in a composite call are called subrequests and all subrequests are executed in the context of the same user who's calling the API. The good thing here is: All the requests mentioned in the Composite API are sent together and they count as a single call towards your API limits.

Let's solve our use case now. We need to get Account record along with it's related Contacts and Opportunities and we only have the account id that we can pass to the request.

This is my account record in salesforce along with the related records:


As you can see above, the account's name is Sample Account and under that we have 2 Contact records: Sample Contact 1 and Sample Contact 2. Similarly, we have 2 Opportunity records as well: Sample Opportunity 1 and Sample Opportunity 2. We're going to fetch this whole information using composite API. Let's have a look at the request below:
{
    "compositeRequest": [
        {
            "method": "GET",
            "url": "/services/data/v55.0/sobjects/Account/0016D00000fHjSLQA0",
            "referenceId": "refAccount"
        },
        {
            "method": "GET",
            "url": "/services/data/v55.0/sobjects/Account/@{refAccount.Id}/Contacts",
            "referenceId": "refContacts"
        },
        {
            "method": "GET",
            "url": "/services/data/v55.0/sobjects/Account/@{refAccount.Id}/Opportunities",
            "referenceId": "refOpportunities"
        }
    ]
}
As you can see above, under the compositeRequest array, I've specified various requests that I want to make to salesforce. Below are the 3 requests I've specified:

  1. Get Account record using record id: Here, I've specified record id of the account record in the request.

  2. Get Contacts related to the current account: Here, you can also specify the account id itself in the request body but I've referenced the response of previous request and used it as an input here. Our previous request will return the account record in the request body which I'm referring as refAccount. That record will have an Id key in the response body with the value as record id of the account. So, I've specified @{refAccount.Id} in the URL of the second request where we're fetching the Contacts related to the account. We've specified refContacts as the reference id for this API response.

  3. Get Opportunities related to the current account: This is exact similar to the 2nd request, the only difference is - instead of getting contacts, here we're getting the Opportunities related to the current account record. The reference id for this request's response is refOpportunities.

This is how my request looks like in postman:

You can make a POST request and the URL for your API will be in this format: https://<my-domain-name>.my.salesforce.com/services/data/vXX.X/composite

Remember to add the Authorization header to the request with Bearer<space><access token> as the value, that we got while doing authorization. Let's have a look at the Authorization header below:

As you click on Send you'll get the response similar to what's shown below:

I am also sharing the full response below for your reference:
{
    "compositeResponse": [
        {
            "body": {
                "attributes": {
                    "type": "Account",
                    "url": "/services/data/v55.0/sobjects/Account/0016D00000fHjSLQA0"
                },
                "Id": "0016D00000fHjSLQA0",
                "IsDeleted": false,
                "MasterRecordId": null,
                "Name": "Sample Account",
                "Type": null,
                "ParentId": null,
                "BillingStreet": null,
                "BillingCity": null,
                "BillingState": null,
                "BillingPostalCode": null,
                "BillingCountry": null,
                "BillingLatitude": null,
                "BillingLongitude": null,
                "BillingGeocodeAccuracy": null,
                "BillingAddress": null,
                "ShippingStreet": null,
                "ShippingCity": null,
                "ShippingState": null,
                "ShippingPostalCode": null,
                "ShippingCountry": null,
                "ShippingLatitude": null,
                "ShippingLongitude": null,
                "ShippingGeocodeAccuracy": null,
                "ShippingAddress": null,
                "Phone": null,
                "Fax": null,
                "AccountNumber": null,
                "Website": null,
                "PhotoUrl": "/services/images/photo/0016D00000fHjSLQA0",
                "Sic": null,
                "Industry": null,
                "AnnualRevenue": null,
                "NumberOfEmployees": null,
                "Ownership": null,
                "TickerSymbol": null,
                "Description": null,
                "Rating": null,
                "Site": null,
                "OwnerId": "0056D000005v9kXQAQ",
                "CreatedDate": "2022-10-15T08:29:16.000+0000",
                "CreatedById": "0056D000005v9kXQAQ",
                "LastModifiedDate": "2022-10-15T08:29:16.000+0000",
                "LastModifiedById": "0056D000005v9kXQAQ",
                "SystemModstamp": "2022-10-15T08:29:16.000+0000",
                "LastActivityDate": null,
                "LastViewedDate": "2022-10-16T07:55:17.000+0000",
                "LastReferencedDate": "2022-10-16T07:55:17.000+0000",
                "Jigsaw": null,
                "JigsawCompanyId": null,
                "CleanStatus": "Pending",
                "AccountSource": null,
                "DunsNumber": null,
                "Tradestyle": null,
                "NaicsCode": null,
                "NaicsDesc": null,
                "YearStarted": null,
                "SicDesc": null,
                "DandbCompanyId": null,
                "OperatingHoursId": null
            },
            "httpHeaders": {
                "ETag": "\"gTI7lF2oYMlDxM+gTW1a62nzqxfxxihRqAygUNh9DPs=\"",
                "Last-Modified": "Sat, 15 Oct 2022 08:29:16 GMT"
            },
            "httpStatusCode": 200,
            "referenceId": "refAccount"
        },
        {
            "body": {
                "totalSize": 2,
                "done": true,
                "records": [
                    {
                        "attributes": {
                            "type": "Contact",
                            "url": "/services/data/v55.0/sobjects/Contact/0036D00000UAXTNQA5"
                        },
                        "Id": "0036D00000UAXTNQA5",
                        "IsDeleted": false,
                        "MasterRecordId": null,
                        "AccountId": "0016D00000fHjSLQA0",
                        "LastName": "Contact 1",
                        "FirstName": "Sample",
                        "Salutation": "Mr.",
                        "Name": "Sample Contact 1",
                        "OtherStreet": null,
                        "OtherCity": null,
                        "OtherState": null,
                        "OtherPostalCode": null,
                        "OtherCountry": null,
                        "OtherLatitude": null,
                        "OtherLongitude": null,
                        "OtherGeocodeAccuracy": null,
                        "OtherAddress": null,
                        "MailingStreet": null,
                        "MailingCity": null,
                        "MailingState": null,
                        "MailingPostalCode": null,
                        "MailingCountry": null,
                        "MailingLatitude": null,
                        "MailingLongitude": null,
                        "MailingGeocodeAccuracy": null,
                        "MailingAddress": null,
                        "Phone": null,
                        "Fax": null,
                        "MobilePhone": null,
                        "HomePhone": null,
                        "OtherPhone": null,
                        "AssistantPhone": null,
                        "ReportsToId": null,
                        "Email": null,
                        "Title": null,
                        "Department": null,
                        "AssistantName": null,
                        "LeadSource": null,
                        "Birthdate": null,
                        "Description": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-15T08:29:16.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:46:44.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:46:44.000+0000",
                        "LastActivityDate": null,
                        "LastCURequestDate": null,
                        "LastCUUpdateDate": null,
                        "LastViewedDate": "2022-10-16T07:46:44.000+0000",
                        "LastReferencedDate": "2022-10-16T07:46:44.000+0000",
                        "EmailBouncedReason": null,
                        "EmailBouncedDate": null,
                        "IsEmailBounced": false,
                        "PhotoUrl": "/services/images/photo/0036D00000UAXTNQA5",
                        "Jigsaw": null,
                        "JigsawContactId": null,
                        "CleanStatus": "Pending",
                        "IndividualId": null
                    },
                    {
                        "attributes": {
                            "type": "Contact",
                            "url": "/services/data/v55.0/sobjects/Contact/0036D00000ULNcUQAX"
                        },
                        "Id": "0036D00000ULNcUQAX",
                        "IsDeleted": false,
                        "MasterRecordId": null,
                        "AccountId": "0016D00000fHjSLQA0",
                        "LastName": "Contact 2",
                        "FirstName": "Sample",
                        "Salutation": "Ms.",
                        "Name": "Sample Contact 2",
                        "OtherStreet": null,
                        "OtherCity": null,
                        "OtherState": null,
                        "OtherPostalCode": null,
                        "OtherCountry": null,
                        "OtherLatitude": null,
                        "OtherLongitude": null,
                        "OtherGeocodeAccuracy": null,
                        "OtherAddress": null,
                        "MailingStreet": null,
                        "MailingCity": null,
                        "MailingState": null,
                        "MailingPostalCode": null,
                        "MailingCountry": null,
                        "MailingLatitude": null,
                        "MailingLongitude": null,
                        "MailingGeocodeAccuracy": null,
                        "MailingAddress": null,
                        "Phone": null,
                        "Fax": null,
                        "MobilePhone": null,
                        "HomePhone": null,
                        "OtherPhone": null,
                        "AssistantPhone": null,
                        "ReportsToId": null,
                        "Email": null,
                        "Title": null,
                        "Department": null,
                        "AssistantName": null,
                        "LeadSource": null,
                        "Birthdate": null,
                        "Description": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-16T07:46:25.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:46:25.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:46:25.000+0000",
                        "LastActivityDate": null,
                        "LastCURequestDate": null,
                        "LastCUUpdateDate": null,
                        "LastViewedDate": "2022-10-16T07:46:26.000+0000",
                        "LastReferencedDate": "2022-10-16T07:46:26.000+0000",
                        "EmailBouncedReason": null,
                        "EmailBouncedDate": null,
                        "IsEmailBounced": false,
                        "PhotoUrl": "/services/images/photo/0036D00000ULNcUQAX",
                        "Jigsaw": null,
                        "JigsawContactId": null,
                        "CleanStatus": "Pending",
                        "IndividualId": null
                    }
                ]
            },
            "httpHeaders": {},
            "httpStatusCode": 200,
            "referenceId": "refContacts"
        },
        {
            "body": {
                "totalSize": 2,
                "done": true,
                "records": [
                    {
                        "attributes": {
                            "type": "Opportunity",
                            "url": "/services/data/v55.0/sobjects/Opportunity/0066D000005z3tpQAA"
                        },
                        "Id": "0066D000005z3tpQAA",
                        "IsDeleted": false,
                        "AccountId": "0016D00000fHjSLQA0",
                        "IsPrivate": false,
                        "Name": "Sample Opportunity 1",
                        "Description": null,
                        "StageName": "Prospecting",
                        "Amount": null,
                        "Probability": 10.0,
                        "ExpectedRevenue": null,
                        "TotalOpportunityQuantity": null,
                        "CloseDate": "2022-10-17",
                        "Type": null,
                        "NextStep": null,
                        "LeadSource": null,
                        "IsClosed": false,
                        "IsWon": false,
                        "ForecastCategory": "Pipeline",
                        "ForecastCategoryName": "Pipeline",
                        "CampaignId": null,
                        "HasOpportunityLineItem": false,
                        "Pricebook2Id": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-16T07:29:30.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:47:09.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:47:09.000+0000",
                        "LastActivityDate": null,
                        "PushCount": 0,
                        "LastStageChangeDate": null,
                        "FiscalQuarter": 4,
                        "FiscalYear": 2022,
                        "Fiscal": "2022 4",
                        "ContactId": null,
                        "LastViewedDate": "2022-10-16T07:47:09.000+0000",
                        "LastReferencedDate": "2022-10-16T07:47:09.000+0000",
                        "HasOpenActivity": false,
                        "HasOverdueTask": false,
                        "LastAmountChangedHistoryId": null,
                        "LastCloseDateChangedHistoryId": null
                    },
                    {
                        "attributes": {
                            "type": "Opportunity",
                            "url": "/services/data/v55.0/sobjects/Opportunity/0066D000005z3wPQAQ"
                        },
                        "Id": "0066D000005z3wPQAQ",
                        "IsDeleted": false,
                        "AccountId": "0016D00000fHjSLQA0",
                        "IsPrivate": false,
                        "Name": "Sample Opportunity 2",
                        "Description": null,
                        "StageName": "Prospecting",
                        "Amount": null,
                        "Probability": 10.0,
                        "ExpectedRevenue": null,
                        "TotalOpportunityQuantity": null,
                        "CloseDate": "2022-10-17",
                        "Type": null,
                        "NextStep": null,
                        "LeadSource": null,
                        "IsClosed": false,
                        "IsWon": false,
                        "ForecastCategory": "Pipeline",
                        "ForecastCategoryName": "Pipeline",
                        "CampaignId": null,
                        "HasOpportunityLineItem": false,
                        "Pricebook2Id": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-16T07:47:01.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:47:01.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:47:01.000+0000",
                        "LastActivityDate": null,
                        "PushCount": 0,
                        "LastStageChangeDate": null,
                        "FiscalQuarter": 4,
                        "FiscalYear": 2022,
                        "Fiscal": "2022 4",
                        "ContactId": null,
                        "LastViewedDate": "2022-10-16T07:47:01.000+0000",
                        "LastReferencedDate": "2022-10-16T07:47:01.000+0000",
                        "HasOpenActivity": false,
                        "HasOverdueTask": false,
                        "LastAmountChangedHistoryId": null,
                        "LastCloseDateChangedHistoryId": null
                    }
                ]
            },
            "httpHeaders": {},
            "httpStatusCode": 200,
            "referenceId": "refOpportunities"
        }
    ]
}
Notice that in the compositeResponse array, we have 3 objects:

The first object is the response from callout to the URL mentioned in the first object of compositeRequest array, in our request body, which is returning the account record. This API response has a body attribute which corresponds to our account record referred by refAccount as shown below:
The second object is the response from URL mentioned in the second object of our compositeRequest array, in our request body, which is returning the contact records related to this account. As you can see below, there are 2 contact records linked to the current account:

We're referring to the body of this response using refContacts. Similarly, the third object is the response from the URL mentioned in the third object of our compositeRequest array, in our request body, which is returning the opportunities related to this account. You can see a similar response as above for opportunities below:
We're referring to the body of this response as refOpportunities.

So, this is how you can make a simple callout to Composite API to get your account, it's related contacts as well as it's related opportunities in one go!

Till now, we talked about how we can get the parent record and it's child records together using composite resources. You may ask: What if I have a lookup field and I want to get that related record details as well? - You can easily do that using composite API!

For example: Le'ts say I want to get the account owner's information as well i.e. the user record related to the account using composite API while I am fetching the account record. To do that, I can just add the below object to my compositeRequest array in the request body:
{
    "method": "GET",
    "url": "/services/data/v55.0/sobjects/User/@{refAccount.OwnerId}",
    "referenceId": "refUser"
}
As you can see above, I am referring to the OwnerId of my account record using @{refAccount.OwnerId} and I am getting the details from the User object using this OwnerId. The overall request is shown below:
I am referring to the body of user API output i.e. the user record as refUser. In the response also, we'll get the user details at the end as shown below:
So, all you need to understand here is: How to refer the previous response variables in the subsequent requests to fetch related records? Once you understood this, you can easily use composite API for your use case. You can easily reference the previous API response body using the reference id you have specified in the request.

Here we had 4 subrequests in a single composite API callout. We can have a maximum of 25 subrequests in a single call. Out of these 25, 5 requests can be related to query operations or sObject collections.

Creating Records using Composite API

Before marking this blog post as complete. I would like to show you one example of creating related records using composite API. This time we're going to create one account record, two opportunities and two contacts related to it using composite api. Let's have a look at the request body below:
{
    "compositeRequest": [
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Account",
            "referenceId": "refAccount",
            "body": {
                "Name": "My Sample Account"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact1",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 1",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact2",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 2",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity1",
            "body": {
                "Name": "My Sample Opportunity 1",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact1.id}",
                "StageName": "Prospecting",
                "CloseDate": "2022-10-20"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity2",
            "body": {
                "Name": "My Sample Opportunity 2",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact2.id}",
                "StageName": "Qualification",
                "CloseDate": "2022-10-20"
            }
        }
    ]
}
As you can see above, this time, the method for each request mentioned in the compositeRequest array is POST because we're creating records here. We also have a body in each request with the data we want to store in the record we're creating. First of all we created an account record with the name My Sample Account referred by refAccount, then we created two contact records: My Sample Contact 1 referred by refContact1 and My Sample Contact 2 referred by refContact2 under that account by mentioning the AccountId as @{refAccount.id}

Finally, we created two opportunity records as well named: My Sample Opportunity 1 referred by refOpportunity1 and My Sample Opportunity 2 referred by refOpportunity2. Notice that both the opportunity records are linked to the same account using: @{refAccount.id} as the AccountId but different contacts as the first one is referring to @{refContact1.id} as the ContactId, however the second one is using @{refContact2.id} as the ContactId. Value for StageName is also different for both the opportunity records.

Below is the response for this API callout:
{
    "compositeResponse": [
        {
            "body": {
                "id": "0016D00000fSq4HQAS",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Account/0016D00000fSq4HQAS"
            },
            "httpStatusCode": 201,
            "referenceId": "refAccount"
        },
        {
            "body": {
                "id": "0036D00000ULNmoQAH",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Contact/0036D00000ULNmoQAH"
            },
            "httpStatusCode": 201,
            "referenceId": "refContact1"
        },
        {
            "body": {
                "id": "0036D00000ULNmtQAH",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Contact/0036D00000ULNmtQAH"
            },
            "httpStatusCode": 201,
            "referenceId": "refContact2"
        },
        {
            "body": {
                "id": "0066D000005z4LPQAY",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Opportunity/0066D000005z4LPQAY"
            },
            "httpStatusCode": 201,
            "referenceId": "refOpportunity1"
        },
        {
            "body": {
                "id": "0066D000005z4LQQAY",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Opportunity/0066D000005z4LQQAY"
            },
            "httpStatusCode": 201,
            "referenceId": "refOpportunity2"
        }
    ]
}
Let's verify our results in our salesforce org as well!

We have an account record named My Sample Account with two contacts and two opportunities linked to it as shown below:
Notice the contact and opportunity names, the stage and close date of the opportunity records, they're the exact same as we mentioned in the request. You can open the opportunities and verify that both My Sample Opportunity 1 and My Sample Opportunity 2 are linked with My Sample Contact 1 and My Sample Contact 2 respectively as shown below:


That's how we can Create Related Records using Composite API. Till now, we're making a POST request to the composite API. There are some other composite resources as well, that we can use and we can get a list of those by making a GET request to the composite API as shown below:
I think the information we learned today should be enough for you to get started with composite API. If you want to learn about other resources that are mentioned in the response above, let me know and I'll create new blog post(s) for the same.

Bonus Content: All or None

Thank you for reading and reaching to the end of the tutorial (almost). I hope you learned something new! Before we close this post, I want to talk about one most important flag in the composite request - the allOrNone flag. If we mark this flag as true, the whole composite request will rollback if any of the subsequent API request fail. So you can now make sure that you create all the records with correct relationships or none of them.

Considering our above example of Account, Contacts and Opportunities creation, let's mark the allOrNone flag as true in the request body as shown below:
Notice that I removed the StageName value from the second opportunity record and it's present in the first opportunity record (highlighted above). As StageName is a required field, the opportunity record creation will fail and it should rollback the whole request as we've specified the allOrNone parameter as true. Just to make it more clear, allOrNone parameter is not a part of the compositeRequest array, it's a separate key in the request body as shown below:
I am also sharing the whole request body below for your reference:
{
    "compositeRequest": [
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Account",
            "referenceId": "refAccount",
            "body": {
                "Name": "My Sample Account"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact1",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 1",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact2",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 2",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity1",
            "body": {
                "Name": "My Sample Opportunity 1",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact1.id}",
                "StageName": "Prospecting",
                "CloseDate": "2022-10-20"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity2",
            "body": {
                "Name": "My Sample Opportunity 2",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact2.id}",
                "CloseDate": "2022-10-20"
            }
        }
    ],
    "allOrNone": true
}
The response of this request is also provided below:
{
    "compositeResponse": [
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refAccount"
        },
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refContact1"
        },
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refContact2"
        },
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refOpportunity1"
        },
        {
            "body": [
                {
                    "message": "Required fields are missing: [StageName]",
                    "errorCode": "REQUIRED_FIELD_MISSING",
                    "fields": [
                        "StageName"
                    ]
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refOpportunity2"
        }
    ]
}
As you can see above, the account request, subsequent contact requests, as well as the request to create first opportunity record all were rolled back with errorCode as PROCESSING_HALTED because the 2nd opportunity record creation failed because of a required field that we removed: StageName. I am sharing the postman screenshot below as well for reference:
Notice that the status code of the response for main request is 200 whereas for subsequent requests it's 400. This is because the composite request was still successful even when the subsequent calls failed because there was no error encountered in the composite api callout (as a whole). The correct error code and messages are provided for each subsequent requests in the response body.

That's all for this tutorial. I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!!

Wednesday, 12 October 2022

How to pass data from screen flow to lwc in salesforce?

Hello Trailblazers,


In this post we're going to learn, how we can pass data from screen flow to lwc component in salesforce. Let's begin by creating our LWC component!

LWC: updateRecord

We'll create a very simple lwc where we can just pass the recordId and objectName as a parameter and it'll render the whole record for us using lightning-record-form so that we can easily update record. Let's have a look at the code snippets now:

updateRecord.html

<template>
    <lightning-record-form object-api-name={objectName} record-id={recordId} layout-type="Full" mode="edit">
    </lightning-record-form>
</template>
As you can see above, we're rendering a record form based on the objectName and recordId we've stored in the javascript variables. The layout type is full and the form will open in edit mode by default.

updateRecord.js

import { api, LightningElement } from 'lwc';

export default class UpdateRecord extends LightningElement {
    @api recordId = "";
    @api objectName = "";
}
This is even more simpler than html, we just have two variables: recordId and objectName which we'll populate from our flow so that the component can render the record form for us. We have added @api decorator in order to make these variables publically available and accessible from flow.

updateRecord.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="recordId" type="String" label="Id of the Record"></property>
            <property name="objectName" type="String" label="Name of the object"></property>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
In the above configuration, we've added the target as: lightning__FlowScreen as we're going to embed this component in a screen flow and under that target, we've mentioned two properties of type String: recordId and objectName. Note that the name of the property is exactly the same as our variable name in JavaScript. As we assign values to these properties from flow, it'll automatically populate our js variables and our component will render the record form. While configuring the flow, we'll see fields with labels assigned to these properties in the component settings.

Now, let's create our flow!

Screen Flow: Update Opportunities

Let's create a screen flow using which we can update opportunities related to an account (yes you can do it via related list :) however, our main goal here is to learn how to pass information from flow to lwc). I am going to name this flow as Update Opportunities and this is how it looks like:

Select Opportunity

In our Select Opportunity screen, we've used radio buttons to show different opportunities related to the current account record:


The user will choose one of these options and move to the next screen, where we'll open our updateRecord LWC component with the selected opportunity record. I've used a record choice set to show opportunity records related to the current account as choices, as shown below:


I am sharing the opportunity record choice set below for reference:


Notice that I've created a new flow variable AccountId using which I am querying my opportunity records related to the current account record. While embedding this flow into the record page, we'll set this AccountId variable with the current account's record id.


As you can see above, we've set the Choice Label with Name field of opportunity record and Choice Value as Id field of opportunity record. I am also sharing the AccountId flow variable below for reference:


It's Available for input as we're going to populate this variable from our lightning record page for account. Now, let's move on to the Update Opportunity screen.

Update Opportunity


Notice the highlighted sections above, on the left, you can see our updateRecord lwc component which is available under Custom section. I've embedded the same component in the screen which you can see in the middle and on the right, we've set the properties of our lwc component. Notice that the labels we provided to those properties are visible here: Id of the Record and Name of the object. In the id, I've passed the Opportunity Record Set variable which will automatically share the Id value of our selected record and in the name I've passed the object name as Opportunity.

As we open the Advanced section on the right side, we need to select the Refresh inputs to incorporate changes elsewhere in the flow option as given below:


This is important because: Let's say the user chose the first opportunity record out of the radio buttons on the 1st screen and moved ahead to the 2nd screen. Now, user decides to go back and choose the second opportunity record instead of the first one. So, in this case, the inputs provided to our component should refresh automatically so that the lwc component displays the updated form with second opportunity record based on the updated record id passed to the component.

Make sure to activate the flow before we proceed ahead.

Add Screen Flow to Record Page

Now, we're almost done with our task, let's embed our flow to the Account's Lightning Record Page and see it in action!

1. Go to any account record and click on Edit Page from the gear menu as shown below:


2. On the page builder, we'll search for flow component from the left search bar and embed it into our lightning page as shown below:


As you click on the embedded flow component, you'll have configurations on the right hand side where you can select the flow Update Opportunities. Note that I've selected Pass record ID into this variable checkbox for the AccountId variable. This will automatically pass the current account's record id to the flow.

Click on Save button and Activate the page (if required) as shown below:


Click on Activate button. Assign the current page as org default for now:



Click Next button


Click on Save button and that's all!

Demo

This is how our flow looks like on an account record's detail page:


As you select an opportunity and click Next you'll see our lwc component with the selected opportunity record in edit mode as shown below:


We have the Save and Cancel button coming from the record form using which you can save your changes. You can click on the Finish button once you're done and it'll restart the flow. 

Note: After selecting an opportunity from the first screen and clicking Next, you can also click Previous button and select a different opportunity to verify, the form should update on the second screen automatically with the new opportunity record.

That's all for this tutorial. I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!!