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

Friday, 25 November 2022

Simplifying SOQL with Polymorphic Relationships

Hello Trailblazers,

In this post, we're going to learn about TYPEOF clause in SOQL using which we can query data that contains polymorphic relationships. I recently came across this Stackoverflow Question which was asked 7 years 3 months ago considering the time I am writing this post where the user has asked about "Unable to query FederationIdentifier on case owner" The question is pretty old but is still valid. You cannot query case owner's FederationIdentifier using a simple SOQL query on case object. Let's see why?

The Problem

I have a case record in my org as shown below:

Let's say you want to query the case subject, as well as the case owner's FederationIdentifier field. As you can see above, the owner of case is a person named User User and as we open this user's record, you can see below that the Federation ID field has a value 12345.

Well, if I tell you to write a SOQL query in order to get this federation id from case, maybe the first query that comes to your mind is something like this:
SELECT Subject, Owner.FederationIdentifier FROM Case WHERE Id = '5005D000008nxIkQAI'
Now, let's run this SOQL query and see the results:
As you can see above, we're getting this error:
SELECT Subject, Owner.FederationIdentifier FROM
                ^
ERROR at Row:1:Column:17
No such column 'FederationIdentifier' on entity 'Name'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.
This error is coming because the OwnerId field on Case is a polymorphic relationship field which can refer to a Group or a User. In case of a User, we'll have the FederationIdentifier field but in case of a Group, we won't have it. Therefore, we can say that, while you're executing this query, salesforce is not sure if the owner is a Group or a User and therefore, the query could not be executed.

Ok, got the issue now but how to resolve it? Should I create a formula field on case to get the case owner's federation id? You can do it, but let's also see how we can query it!

The Solution

Salesforce has provided an optional TYPEOF clause that can be used in a SOQL query including polymorphic relationships. TYPEOF clause is available in API version 46.0 or LATER. Let's solve our issue first and then we'll see the format of TYPEOF clause in detail. In order to query FederationIdentifier from case owner we can write a query as follows:
SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'
As you can see above, I've included the statement TYPEOF Owner WHEN User Then FederationIdentifier END. It's basically saying that when the Case Owner's type is User, then query FederationIdentifier field. I've also added a Type filter for case owner at the end of query: AND Owner.Type = 'User'.  The result for this query is given below:
Starting SFDX: Execute SOQL Query...

07:30:06.358 sfdx force:data:soql:query --query SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'
Querying Data... done
 ID                 OWNER                                                              
 ────────────────── ────────────────────────────────────────────────────────────────── 
 5005D000008nxIkQAI {                                                                  
                      "attributes": {                                                  
                        "type": "User",                                                
                        "url": "/services/data/v56.0/sobjects/User/0055D000005BiagQAC" 
                      },                                                               
                      "FederationIdentifier": "12345"                                  
                    }                                                                  
Total number of records retrieved: 1.
07:30:10.900 sfdx force:data:soql:query --query SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'
 ended with exit code 0
As you can notice above, we're getting the related Owner record with FederationIdentifier field in response. Let's say you want to get the FederationIdentifier using this query in apex, you can refer to the below code:
List<Case> caseList = [SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'];
User caseOwner = caseList[0].Owner;
System.debug(caseOwner.FederationIdentifier);
You'll get the FederationIdentifier from case owner in the debug as shown below:
Awesome! Now that we've solved the issue, let's learn more about the TYPEOF clause. The TYPEOF statement in SOQL is of the format:

TYPEOF <PrimarySObjectField> WHEN <PossibleRelatedObject1APIName> THEN < PossibleRelatedObject1FieldsListToQuery> WHEN <PossibleRelatedObject2APIName> THEN < PossibleRelatedObject2FieldsListToQuery>.... ELSE <LisfOfFieldsToQueryWhenAllAboveWHENConditionsAreFalse> END

It's similar to switch case in apex. For example, we can understand our TYPEOF clause as: TYPEOF Owner WHEN User Then FederationIdentifier END, here we checked if the TYPEOF Owner (primary sObject field) WHEN (is equal to) User (possible related object) THEN FederationIdentifier (field to query) END

Notice that the ELSE part is optional and we can have multiple WHEN-THEN combinations to check for multiple objects that are possible considering our polymorphic relationship field.

Another example can be WhatId field of Task object. We know that this field can be linked with multiple sObjects. Let's consider Account, Opportunity and Case for now and query different fields from each of these objects. If the task is not linked to any of the Account, Opportunity or Case object, I'll just query the name field of that related object's record.

Let's create 4 tasks: First one related to an account, second one related to an opportunity, third one related to a case and fourth one related to a product. Have a look at the tasks I created below:
Task linked to account
Task linked to account
Task linked to opportunity
Task linked to opportunity
Task linked to case
Task linked to case
Task linked to product
Task linked to product
My apex code with the SOQL query is as follows:
List<Task> taskList = [SELECT Subject, 
    TYPEOF What 
    	WHEN Account THEN Type, NumberOfEmployees 
    	WHEN Opportunity THEN Amount, CloseDate 
    	WHEN Case THEN Subject, CaseNumber 
    	ELSE Name 
    END 
FROM Task];
for(Task taskRecord: taskList) {
    System.debug(taskRecord.Subject + ' -> ' + taskRecord.What);
}
As you can see above, I am querying Type and NumberOfEmployees when the related object is Account, Amount and CloseDate when the related object is Opportunity, Subject and CaseNumber when the related object is Case, otherwise, I'm just querying Name of the related record. The result when the above apex code is executed is shown below:
As you can see above, we're getting all the tasks with relevant related fields from each of Account, Opportunity and Case. In case of product, we're getting the Name field. 

If you remember, in our initial query on Case (SELECT Id, TYPEOF Owner WHEN User Then FederationIdentifier END FROM Case WHERE Id = '5005D000008nxIkQAI' AND Owner.Type = 'User'), we also added WHERE condition on Type field as: AND Owner.Type = 'User'. This is another good way to filter records while dealing with the specific type of related objects we would like to consider. You can even skip the ELSE part if you've already added WHEN-THEN for each related object and the corresponding conditions in WHERE clause.

So that's how you can Simplify SOQL with Polymorphic Relationships. You can have a look at this detailed article from salesforce official documentation to learn more about TYPEOF clause. Before coming to the end of this blog, let's talk about some of the important considerations of TYPEOF clause as well.

Considerations for TYPEOF

I am highlighting some of the considerations of TYPEOF clause here, you can checkout the full list in official documentation.
  • TYPEOF cannot be used with with queries that don't return objects such as COUNT() and aggregate queries
  • TYPEOF cannot be used in SOQL used in Bulk API
  •  TYPEOF cannot be used in semi-join query. Semi Join query is a query used to filter the original query - For example: SELECT Name FROM Contact WHERE AccountId IN (SELECT Id FROM Account), here the text in bold is semi-join query
  • TYPEOF cannot be used with a relationship field whose relationshipName or namePointing attribute is false.
You can checkout the namePointing and relationshipName attribute of a field very easily. For example, consider the below code:
System.debug(Case.OwnerId.getDescribe());
Here, we're getting the DescribeFieldResult for OwnerId field of Case object. The output for the same is provided below:
Schema.DescribeFieldResult[
    getByteLength=18;
    getCalculatedFormula=null;
    getCompoundFieldName=null;
    getController=null;
    getDataTranslationEnabled=null;
    getDefaultValue=null;
    getDefaultValueFormula=null;
    getDigits=0;
    getFilteredLookupInfo=null;
    getInlineHelpText=null;
    getLabel=Owner ID;
    getLength=18;
    getLocalName=OwnerId;
    getMask=null;
    getMaskType=null;
    getName=OwnerId;
    getPrecision=0;
    getReferenceTargetField=null;
    getRelationshipName=Owner; // <---- Relationship Name
    getRelationshipOrder=null;
    getScale=0;
    getSoapType=ID;
    getSobjectField=OwnerId;
    getType=REFERENCE;
    isAccessible=true;
    isAggregatable=true;
    isAiPredictionField=false;
    isAutoNumber=false;
    isCalculated=false;
    isCascadeDelete=false;
    isCaseSensitive=false;
    isCreateable=true;
    isCustom=false;
    isDefaultedOnCreate=true;
    isDependentPicklist=false;
    isDeprecatedAndHidden=false;
    isDisplayLocationInDecimal=false;
    isEncrypted=false;
    isExternalId=false;
    isFilterable=true;
    isFormulaTreatNullNumberAsZero=false;
    isGroupable=true;
    isHighScaleNumber=false;
    isHtmlFormatted=false;
    isIdLookup=false;
    isNameField=false;
    isNamePointing=true; // <---- Name Pointing
    isNillable=false;
    isPermissionable=false;
    isQueryByDistance=false;
    isRestrictedDelete=false;
    isSearchPrefilterable=false;
    isSortable=true;
    isUnique=false;
    isUpdateable=true;
    isWriteRequiresMasterRead=false;
]
If you notice, the relationshipName for this field is Owner and namePointing attribute is also true. A smaller documentation on TYPEOF clause is also available here.

That's all for this tutorial. I hope you liked it, let me know your feedback in the comments down below. You can connect with my on Connections app by scanning the QR code given below:
Or search in the code scanner screen using my username: rahulmalhotra

Happy Trailblazing!!

Saturday, 19 November 2022

Create modals using the new LightningModal component (Winter '23 Release)

Hello Trailblazers,

In this post we're going to learn about the new LightningModal component that can be extended by any lwc which you would like to use as a modal. Salesforce has provided 3 helper components to create a modal:

  1. lightning-modal-header
  2. lightning-modal-body
  3. lightning-modal-footer

Let's create a testModal component and try to use these 3 tags to see what we get as a result.

Creating a simple testModal LWC

testModal.js-meta.xml

I am specifying the meta file first of all so that we can focus on our html and js files throughout this tutorial. Our meta file is pretty simple as shown below:
<?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__HomePage</target>
    </targets>
</LightningComponentBundle>

As you can see above, I've specified lightning__HomePage as a target so that we can embed this component in our homepage. This is NOT REALLY REQUIRED as we're going to use our component as a modal, but I'm just keeping this for now, so that we can see what the helper components render for us. I've also marked isExposed as true to make this component available in our app builder.

testModal.html

The simplest HTML content for our modal is shown below:
<template>
    <lightning-modal-header>Test Modal Header</lightning-modal-header>
    <lightning-modal-body>Test Modal Body</lightning-modal-body>
    <lightning-modal-footer>Test Modal Footer</lightning-modal-footer>
</template>

We've only called the header body and footer tags with some content in them. I embedded our component on the homepage. Let's see the output below:


As you can see above, we're having 3 different sections: Header, Body and Footer coming from our lightning-modal-header, lighnting-modal-body and lightning-modal-footer tags.

It's better and easier to use label attribute of lightning-modal-header to specify the heading for our modal header. You can just update the lightning-modal-header tag as shown below:
<lightning-modal-header label="Test Modal Label">Test Modal Header</lightning-modal-header>
Let's see the output of this change as well:
You might not see any difference here but it'll be more clear as we'll start using this component as a modal.

Okay that's fine but how do I actually use this as a modal?

In order to open it as a modal, we'll create another lwc named: useModal but first of all let's update the js file of this testModal as well:

testModal.js

import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {}
Notice the two changes I did above which makes it different from other LWCs:

  1. Instead of the statement, import { LightningElement } from 'lwc'; it's importing LightningModal from lightning/modal.
  2. Instead of extending LightningElement, our component is extending LightningModal using extends LightningModal.

That's all for our testModal for now, let's create our useModal component now:

useModal.js-meta.xml

Starting with the meta file, it's the exact same as we had above because we'll be embedding this component as well in our homepage:
<?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__HomePage</target>
    </targets>
</LightningComponentBundle>
Note: You can remove the lightning__HomePage target from your testModal now and remove it from the hompage, as we'll be using it as a modal now

Now, let's move on to the HTML part of useModal LWC:

useModal.html

<template>
    <lightning-card title="Use Modal">
        <p class="slds-var-p-horizontal_small">
            <lightning-button label="Open Modal" onclick={openModal}></lightning-button>
        </p>
    </lightning-card>
</template>
As you can see above, the code is pretty simple. We have a lightning-card (for better UI) and inside it, we have a paragraph which contains a lightning-button with label Open Modal. This button will call our js function openModal as it's clicked by the user. It's time to move on to our js file now!

useModal.js

For now, we'll just try to open our testModal:
import { LightningElement } from 'lwc';
import TestModal from 'c/testModal';

export default class UseModal extends LightningElement {

    openModal() {
        TestModal.open();
    }
}
As you can see above, we've imported our TestModal using the import statement and called open() on it in order to open our modal component. Let's see how it works!

This is how our useModal component looks like on the homepage:
I just embedded it before our testModal component. As we click on Open Modal button, we get the modal as shown below:
I hope that the usage of label attribute in lightning-modal-header component is clear now. You should use label attribute to specify a label for your modal. If you want to have any custom HTML like: a button or something else, that can come between our lightning-modal-header tags.

Isn't it amazing? just a few lines of code and you have your modal ready. The close button that you see on the top right works perfectly and your modal will be closed automatically as you click on that button.

Note: You can also close the modal by pressing the ESC key.

Resizing our modal

There are some properties that you can pass in the open function. One of the property is size which supports small, medium and large values. By default, the size is medium. You can pass any sizes out of small, medium or large. The output of all 3 are shown below.

To have a small sized modal, you can just do:
    TestModal.open({
        size: 'small'
    });
And you'll have the below output:
For a medim sized modal, you can just do nothing (as the default size is medium) or pass in the medium value as shown below:
    TestModal.open({
        size: 'medium'
    });
And you'll have the below output:
Similarly, for a large modal, you can just do:
    TestModal.open({
        size: 'large'
    });
And you'll have the below output:

Defining a Custom Close Button for our Lightning Modal

A very common requirement is to have two buttons: Cancel and Save in our modal footer. If someone clicks on Cancel, we'll just close the modal and if someone clicks on Save, we'll save the information and then close our modal. Let's see how we can implement that. It's time to update our testModal.html
<template>
    <lightning-modal-header>Test Modal Header</lightning-modal-header>
    <lightning-modal-body>Test Modal Body</lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Cancel" onclick={closeModal} class="slds-var-p-right_x-small"></lightning-button>
        <lightning-button label="Save" variant="brand" onclick={save}></lightning-button>
    </lightning-modal-footer>
</template>
As you can see above, I've removed the Test Modal Footer text which was present in between lightning-modal-footer tags and I added two lightning buttons instead: one for cancel and another for save. The Cancel button is calling closeModal() from our js and have an extra small right padding so that the two buttons don't stick to each other. The Save button is having a variant as brand and is calling save() from our js file. The updated testModal.js file is given below:
import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {

    closeModal() {
        this.close();
    }

    save() {
        console.log('We will save the data and then close modal');
    }
}

You might have noticed above that I added two methods here:
  1. closeModal() which is calling close() here as this.close(). This close() is defined in the LightningModal component which we're extending and it'll close the modal.
  2. save() which is doing nothing as of now but adding a statement to the console that: We'll save the data and then close modal.

The updated modal is shown below:
As you can see, we have two buttons now: Cancel and Save. As you click on Cancel button, the modal will be closed. If you click on Save button, there will be a message in console but the modal will not close.

Let's say the user clicks on save and then while the information is being saved, the user clicks on Close button at the top right and the modal is closed, how do you ensure that the information was saved successfully? In this scenario, preventing the user from accidentally closing the modal is important, let's see how we can do that!

Prevent the user from closing Lightning Modal using disableClose attribute

For now, we'll consider a scenario that our save operation takes 5 seconds. So, we'll disable the Close operation for 5 seconds when the save button is clicked. We're not dealing with apex in this tutorial, so we'll just use setTimeout() to simulate our server call. Our testModal.js is updated as shown below:
import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {

    closeModal() {
        this.close();
    }

    save() {
        console.log('We will save the data and then close modal');
        this.disableClose = true;
        const that = this;
        setTimeout(() => {
            console.log('Information saved! You can now close the modal');
            that.disableClose = false;
        }, 5000);
    }
}
Inside the save(), we're setting disableClose to true. Then we're calling setTimeout() (you can consider it similar to calling any apex method and waiting for the response). In setTimeout(), we can pass a function and specify the time (in milliseconds) after which that function will be called. Here, we've specified the time as 5000 milliseconds i.e. 5 seconds and after 5 seconds the function passed will be called. That function will print the text Information saved! You can now close the modal in the console and set disableClose to false again. This can be considered - as our save operation is successful and we want to allow the user to close the modal now.
Notice the above image, this is how our modal looks like. Have a look at the cross in the red rectangle, it's enabled for now. As I click Save button the modal will look like as shown below:
As you can see above, the cross icon is disabled, this means we cannot close our modal and it'll automatically be enabled after 5 seconds. Even if you click on Cancel button the modal will not close because the call this.close() will not work. It's even better if we can disable the Save and Cancel buttons as well, until the save operation is performed, so that the user doesn't click these buttons again and again. Let's do that quickly!

For this, I am going to add disabled={disableClose} to both Save and Cancel buttons of my modal so that these buttons are disabled when disableClose is true. Below is the updated testModal.html:
<template>
    <lightning-modal-header label="Test Modal Label">Test Modal Header</lightning-modal-header>
    <lightning-modal-body>Test Modal Body</lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="Cancel" onclick={closeModal} class="slds-var-p-right_x-small" disabled={disableClose}></lightning-button>
        <lightning-button label="Save" variant="brand" onclick={save} disabled={disableClose}></lightning-button>
    </lightning-modal-footer>
</template>
Notice the disabled attribute applied to both the buttons above. The updated output as I click on Save button of my modal is given below:
As you can see above, all my buttons are disabled now when I clicked Save, they'll be enabled back together after 5 seconds (OR you can do it after your apex call is successful in a real implementation). You can also call the close() again once your apex call is successful so that the modal get closed automatically. In our case, it can be after 5 seconds when we set disableClose back to false as shown below:
import LightningModal from 'lightning/modal';

export default class TestModal extends LightningModal {

    closeModal() {
        this.close();
    }

    save() {
        console.log('We will save the data and then close modal');
        this.disableClose = true;
        const that = this;
        setTimeout(() => {
            console.log('Information saved! Closing the modal...');
            that.disableClose = false;
            that.close();
        }, 5000);
    }
}
Notice that we called close() after we set disableClose to false in our save(). Now, the modal will close automatically after 5 seconds as we click on the save button.

So that's how you can create modals using the new LightningModal component. I hope you have a good idea of how it works and you can go ahead and create your own modals now. Some of the information that we didn't cover here are:
  • Custom styling of modal's header, footer and body
  • Passing information to modal while opening the modal (Just create an @api attribute in your testModal and provide value to it in the TestModal.open() like we did for size attribute)
  • Passing information back to component that called the modal when the modal is closed (You can pass the value inside close() as a parameter and have a then() linked to TestModal.open() which will receive the return value as: TestModal.open({...params}).then((valuePassedToCloseFunction) => {})
  • Firing an event from modal and capturing it in parent.
You can refer to the official documentation or comment below and let me know if you would like to learn about these in detail and I'll create another post. You can also find my contact details on Connections (username: rahulmalhotra)

So that's all for this tutorial. I hoped you liked it, let me know your feedback in the comments down below.

Happy Trailblazing!!

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!!