Salesforce Spring 21 Release – New Lightning Flow features

Salesforce Winter 21 Release – New Lightning Flow features

This posts summarizes most Winter 21 new features related to Lightning Flow. To test these new features now, you can sign up for a pre-release org here.

1- Trigger a Flow to Run Before a Record is Deleted

In Flow Builder, you can now configure a new record–triggered flow to run before a record is deleted. You no longer need to write Apex code to set this up. This autolaunched flow runs in the background and updates related records when a record is deleted.

2- Debug Autolaunched Flows Directly on the Canvas (Beta)

Now you can debug an autolaunched flow without opening a new browser tab. Just click Debug on Canvas (Beta) in Flow Builder and view the path the flow took during the debug run.

3- Build Multi-Column Screens in Flow Builder (Pilot)

Multi-column layouts have hit Flow Builder! You can use the new Section component to arrange your flow screen components into multiple columns without touching a single line of code. Divide each flow screen into multiple sections, and easily modify the number of columns in each section to create effective, visually appealing layouts. Contact your Account Executive if you’re interested in participating in the pilot.

4- Debug Flow Errors in Sandbox Org as Another User:

Debugging a flow in a sandbox org is now easier than ever with the ability to debug a flow as another user. When you debug a flow as another user, you can catch flow exceptions such as unexpected permission, sharing, and profile configurations before they occur in production orgs.

Note: to enable this feature, go to Setup – Process Automation Settings, otherwise you will receive the below. 

5- Improve the Performance and Precision of Record-Triggered Flows with Entry Criteria

Salesforce added entry criteria to record-triggered flows, giving you more control over your automation. Instead of configuring the flow to run every time a record is created or edited, you can now configure it to run only when condition requirements are met. This optimization can also significantly reduce your record-triggered flows’ impact on your org’s performance, depending on the flow.

6- Create Decision Outcomes that Only Execute When Certain Changes are Made

Now a flow triggered by a record update can take different paths if the record that triggered the flow was edited to meet certain criteria. When you configure a Decision outcome, you can now set that outcome to execute only when the triggering record is updated to meet the condition requirements. This feature allows you to filter out record updates that are unrelated to your flow’s use case, and to avoid reprocessing records that previously triggered the flow.

7- Use Auto-Layout to automatically place and connect elements in flow

The new auto-layout feature makes building flows and aligning elements easier. When auto-layout is enabled on a flow, elements in the canvas are spaced and connected automatically. No more elements positioned slightly out of alignment!

8- Access Related Record Data In Autolaunched Flows With Triggers Using $Record.

We do not need to have a Get Records flow element to retrieve associated record information. You can access the related data via the $Record element.

9- Quickly See the Triggers of Your Flows on the Flows Page in Setup

Now see which trigger your autolaunched flow has with just a glance at the Flows list view in Setup. In the new Trigger column, see if a flow’s trigger is a record, a schedule, or a platform event. For a record-triggered flow, see if it makes before- or after-save updates. You can also discover which trigger an autolaunched flow has by viewing the flow’s detail page.

10- Lightning Flow Run-Time Improvements in API Version 50.0

ISBLANK function returns true for empty string or null values

In flow and process formulas, the ISBLANK function returns true for empty string or null values. In flows and processes that run in API versions earlier than 50.0, the ISBLANK function returns true only when the value is null.

Enforce each flow screen’s Next or Finish control navigation setting

When Next or Finish is deselected in a flow screen’s control navigation settings, the screen no longer displays the Next navigation option to users. For flows that run in API versions earlier than 50.0, the Next or Finish setting isn’t always enforced. Specifically, screens always display the Next option when an element is in the flow after the screen.

Let merge fields support null record variables

Merge fields that reference record variables no longer cause errors when the record variables are null at run time. For flows that run in API versions earlier than 50.0, a merge field such as {!Account.Name} causes an error when the account record variable is null at run time.

Enforce the running user’s data access when a flow uses a merge field

This versioned update enables the Enforce Data Access in Flow Merge Fields release update for flows that run in API version 50.0. After the release update’s auto-activation, the update will be enforced for all flows regardless of their run-time API version. 

Treat invalid references to global variables as errors instead of string values

At run time, an invalid reference to a global variable now results in an error. In flows that run in API versions earlier than 50.0, invalid references to global variables are treated as string values. For example, suppose that a flow screen contains a Display Text component with this typo: Hello {!$User.FirstNaame}! If the flow runs in API version 50.0 or later, an error occurs when the Screen element is executed. If the flow runs in an API version earlier than 50.0, your users see “Hello {!$User.FirstNaame}!” on the screen.


To get the full Winter 21 release notes for Lightning Flow, please visit this link

All about Date and Time in Salesforce

Rule #1 – Every User in Salesforce has a Time Zone which is based on the region and adjusts automatically for DST

Whenever you create a new User in Salesforce, the Time Zone field should be populated as it is required, which means that every User has an associated Time Zone. In other words, no User can be created without a Time Zone! On top of that, the Company has a default Time Zone. 

You can set a User’s Time Zone in the User’s record, and to get your User’s Time Zone, run this in Anonymous Apex:

TimeZone tz = UserInfo.getTimeZone();
System.debug('Time Zone ID: ' +tz.getID());
System.debug('Time Zone Display name: ' +tz.getDisplayName());
// Returns:
// DEBUG|Time Zone ID: America/New_York
// DEBUG|Time Zone Display name: (GMT-05:00) Eastern Standard Time (America/New_York)

And to get the Organization default Time Zone, we use a SOQL query which returns the Time Zone Id, in my case “America/New_York”:

SELECT TimeZoneSidKey 
FROM Organization

Also, as you can see, the Time Zone is displaying a region, and not a difference in hours. So, the difference in hours will change according to the Daylight Saving Time! A Time Zone of “America/New_York” will change from UTC/GMT-04:00 to UTC/GMT-05:00 according to the DST. 

So what? What does this affect DateTime objects in Apex? Let’s visit the second Rule below. 


Rule #2 – DateTime objects are saved in GMT

In Salesforce, every time you instantiate and insert a DateTime object, it gets saved in the database in GMT Time Zone and it is translated to the user’s time zone when reading it. 

There are many ways to instantiate a DateTime object in Salesforce: using the DateTime class with many methods! The most popular method is DateTime.newInstance(year, month, day, hour, minute, second). This method constructs a Datetime from Integer representations of the specified year, month (1=Jan), day, hour, minute, and second in the User’s time zone. My User’s Time Zone is America/New_York, so everytime I instantiate a DateTime object using the newInstance method, I use this Time Zone as my reference, but remember, Salesforce only deals with GMT DateTime! To make it clear, check the below code, and keep in mind my User’s Time Zone. What do you think the debug outcome will be?

DateTime dt = DateTime.newInstance(2020, 01, 20, 15, 0, 0);

I am instantiating a new DateTime object of January 20, 2020, 3 PM. This will be based on my User’s Time Zone because I am using the method newInstance, and will be saved in the database in GMT! Yes, the input is based on the User’s Time Zone, but the actual record is saved in the equivalent GMT time, because GMT is used to save ANY DateTime object! For that, and because the difference between Eastern Time and GMT is 5 hours, the debug outcome is:

DEBUG|2020-01-20 20:00:00

But there are other ways to instantiate a DateTime object. For example, using DateTime.newInstanceGMT will treat the parameters as GMT, and the below 2 objects yield the same retult:

DateTime dtGMT = DateTime.newInstanceGMT(2020, 01, 20, 20, 0, 0);
DateTime dtLocal = DateTime.newInstance(2020, 01, 20, 15, 0, 0);
System.debug('GMT DateTime: ' +dtGMT);
System.debug('Local DateTime: ' +dtLocal);
// Returns:
// DEBUG|GMT DateTime: 2020-01-20 20:00:00
// DEBUG|Local DateTime: 2020-01-20 20:00:00

There is also a way to read the DateTime in any Time Zone that you want using the method DateTime.format. This method can take 2 parameters. The first one is the date format string, and the second is the Time Zone ID. The date format string specifies the format of the returned DateTime string. For more information on the Java simple date format, see Java SimpleDateFormat. The Time Zone Id should be a valid time zone of the Java TimeZone class that correspond to the time zones returned by the TimeZone.getAvailableIDs method in Java.

Datetime GMTDate = Datetime.newInstanceGmt(2011,6,1,12,1,5);
String strConvertedDate = GMTDate.format('MM/dd/yyyy HH:mm:ss', 'America/New_York');
// Date is converted to the new time zone and is adjusted for daylight saving time.
System.assertEquals('06/01/2011 08:01:05', strConvertedDate);


Rule #3 – If possible, try to avoid Datetime field, and use Date field instead

If your use case only requires a Date without a specific Time, do use the Date type instead of the DateTime type, that’s because the Date type does not care about anything related to Timezone. 

For example, let’s instantiate a Date and a DateTime objects:

DateTime dt = DateTime.newInstance(2020, 01, 20, 22, 0, 0);
Date d = Date.newInstance(2020, 01, 20);
System.debug('DateTime: ' +dt);
System.debug('Date: ' +d);
// Returns:
// DEBUG|GMT DateTime: 2020-01-21 03:00:00
// DEBUG|Local DateTime: 2020-01-20 00:00:00



  • Every User has an associated TimeZone
  • DateTime objects are saved in GMT in Salesforce 
  • A User will read a DateTime object depending on his/her TimeZone
  • Try to avoid DateTime if what you need is just a Date 

Scheduling a Salesforce autolaunched Flow

In the Winter ’20 release, Salesforce introduced scheduling autolaunched flows, which made it possible to schedule actioons decalatively within Salesforce. Prior to that, the only way to schedule anything in Salesforce was through Asynchgronous Apex, specificaly by implemeting the Schedulable interface. 

To schedule autolaunched flows, you need to specify the date and time the flow will run as well as its run frequency: once, daily, or weekly. Optionally, you can specify a specific Object with a filter that returns specific records of that Object, where the flow would run against. 

Here’s the session explaining scheduling Salesforce autolaunched flows, with 2 demos ar the end:

  1. Scheduling sending an email using the “Send Email” action
  2. Scheduling mass-updating Account records on a daily basis

These considerations apply to scheduled flows.

  • A scheduled flow starts at the specified time and frequency. You can’t launch a scheduled flow by any other means.
  • The Start Time field value is based on the org’s default time zone.
  • The View All Data permission is required to activate an autolaunched flow that has a trigger.
  • The maximum number of scheduled flow interviews per 24 hours is 250,000 or the number of user licenses in your org multiplied by 200, whichever is greater.

    If you specify an object so the flow runs for a batch of records, set the time, frequency, and record conditions to avoid reaching this limit. You can use debug logs to check how many records a scheduled flow runs on. Track the number of records with the FLOW_START_SCHEDULED_RECORDS event. If your org reaches the limit, Salesforce sends a flow error email.

  • If you delete a scheduled flow from the Scheduled Jobs page in Setup, all future recurrences of that flow are canceled. To enable future runs, deactivate and reactivate the flow.
  • A scheduled flow doesn’t run if it’s scheduled to run once with a date and time that has already passed.
  • The Automated Process user runs scheduled flows.
  • If you need a scheduled flow to invoke Apex code, don’t activate the Require User Access to Apex Classes Invoked by Flow critical update. When that critical update is activated, the scheduled flow fails when it invokes Apex.
  • If you configure an Update Records element to use the ID and all field values from the $Record global variable, enable Filter inaccessible fields from flow requests in your org’s process automation settings. Otherwise, the flow fails because the Update Records element tries to set the values for system fields and other read-only fields.
  • When you define multiple filters, the filter logic usually defaults to AND. However, if multiple filters have the same field selected and use the equals operator, the filters are combined with OR.

    For example, your filters check whether a case’s Type equals Problem (1), Type equals Feature Request (2), and Escalated equals true (3). At run time, the filters are combined to (1 OR 2) AND 3.

Round Robin Assignment using Flow

Use Case:

Marie Sloan, a Salesforce Admin, is asked to build a Round Robin assignment tool to assign Survey records to Agent records following a Round Robin way, which means that  the total number of Survey records to assign should be divided by the number of Agents, and each Agent should be assigned an equal number of Survey records. For example, if we have 3 Agents and 36 Surveys, each Agent will be asigned exactly 12 Surveys. 


Object Model:

To tackle this requirement, let’s first explain the data model required to illustrate this example:

  1. Custom object called Round_Robin_Assigner__c  This is the object that will be used to initiate the Round Robin assignment. The fiels are:
    1. Name: auto number field with display format: RR-{000}
    2. Number_of_Agents__c: Rollup-Summary field that counts the number of Agent__c records associated with this Round_Robin_Assigner__c record
    3. Number_of_Surveys__c: Rollup-Summary field that counts the number of Survey__c records associated with this Round_Robin_Assigner__c record
  2. Custom object called Agent__c with the following fields:
    1. Name: auto number field with display format: A-{000}
    2. User__c: lookup field to the User object
    3. Round_Robin_Assigner__c: Master-Details field to the Round_Robin_Assigner__c object.
  3. Custom object called Survey__c with the following fields:
    1. Name: auto number field with display format: S-{000}
    2. Tag__c: auto number field without any display format. This field simply represents the tag, or Id of eacj Survet record, it is automatically added to each survey, and no 2 surveys will have the same Tag number. This field will be used to calculate the Assignment_ID__c field below.
    3. Assignment_ID__c: formula field used to give the assignment ID to each Survey record, based on t he total number of Agents and the Tag on each Survey. More details to follow below.
    4. Round_Robin_Assigner__c: lookup field to the Round_Robin_Assigner__c object. This is the field that will be filled to assign an Agent to the Survey.

Here is the ERD:

To futher explain what’s going on,

  1. the main object (1) Round_Robin_Assigner__c is the parent of both:
  2. the object to assign, in this case (2) Survey__c
  3. the object to assign to, in this case (3) Agent__c.

In other words, if we want to assign Agent__c records to Survey__c records, first, we create a Round_Robin_Assigner__c record, second, we add to it all the Survey__c records that we want to assign, and third, we  add all the Agent__c records that should be assigned to Survey__c records. The Round_Robin_Assigner__c record will hav a button that will launch a flow to assign the Surveys to the Agents in a Round Robin fashion! Simple!

Now, let’s explain the fields on each object…

First, the Round_Robin_Assigner__c object has 2 Rollup-Summary fields that count the number of Agent__c and the number of Survey__c associated with it. The field names are Number_of_Agents__c and Number_of_Surveys__c


On the Survey__c object:

  • The Tag__c field is simply an auto-number field that gives a unique number to the record. The first Survey__c record has Tag__c = 1, and so on.
  • The Assignment_ID__c field is the field used to assign the Survey__c to the Agent__c. It is a Fomrula field with this Formula:
1 + MOD(Value(Tag__c), Round_Robin_Assigner__r.Number_of_Agents__c)

This MOD function will take the MOD of (1) the Tag number value of the Survey__c record – we uses the Value() function to take the number value of this auto-number field, and (2) the total number of Agent__c records associated with this Round_Robin_Assigner__c record. So, if we have 5 Agent__c records, and 100 Survey__c records,

  • For example, the record wih tag 50 would have 1 + MOD(50,5) = 1 + 50 MOD 5 = 1 + 0 = 1,
  • the next record would have 1 + MOD(51,5) = 1 + 51 MOD 5 = 1 + 1 = 2
  • the next 1 + MOD(52,5) = 1 + 52 MOD 5 = 1 + 2 = 3
  • the next 1 + MOD(53,5) = 1 + 53 MOD 5 = 1 + 3 = 4
  • the next 1 + MOD(54,5) = 1 + 54 MOD 5 = 1 + 4 = 5
  • the next 1 + MOD(55,5) = 1 + 55 MOD 5 = 1 + 0 = 1
  • Etc…

This way, the Assignment_ID__c field will dictate which Survey record goes to which Agent, based on the above math.

Next, we will add a Flow to assign the Agent__c field to the Survey__c records. 

Create the Flow:

Create a new Screen Flow, and in it first, create the variable that will hold the Id of the Round_Robin_Assigner__c record that will have the button to launch the flow. Remember, the variable name should exactly be ‘recordId‘ with the letter ‘I’ in a capital case.

To start, let’s create 3 “Get Records” elements: 

  • Get Assigner: to get the details of the 1 Round_Robin_Assigner__c record that has the Id recordId. Store the variable in the Record Variable sov_Assigner
  • Get Agents: to get all Agent__c records that are children of the above Round_Robin_Assigner__c record. Store the variable in the Record Variable socv_Agents
  • Get Sutveys: to get all Survey__c records that are children of the above Round_Robin_Assigner__c record. Store the variable in the Record Variable socv_Surveys
  • I will only show the screenshots of the first and second “Get Records” elements above. 

Now, create 2 Number variables and an Assignment element to assign them values:

  • v_Agent_Total_count = {!sov_Assigner.Number_of_Agents__c}. This is the rollup summary field that countsthe number of Agents related to this Round_Robin_Assigner__c record.  
  • v_Agent_Counter = 1. This will set the counter to 1. 

Create the first loop:

  • Purpose: loop through all the Agent__c records that are related to the Assigner__c record. For each agent, get all the Survey__c records with an Assignment Id equivalent to the Agent. 
  • Name: Loop through Agents 
  • Collection Variable: socv_Agents 
  • Iteration Direction: First item to last item
  • Loop Variable: socv_agents_single


Inside this loop, get the Survey__c records with an Assignment Id equivalent to the Agent. For that, we will use use variable v_Agent_Counter. All these syrveys will be stored in a Record Variable called socv_Specific Surveys

Create a second loop inside the first loop:

  • Purpose: loop through all the Survey__c records with an Assignment Id equivalent to the Agent, and add these to a “Record Variable” to assign them at the end to the Agent. 
  • Name: Loop through specific Surveys 
  • Collection Variable: socv_Specific_Surveys 
  • Iteration Direction: First item to last item
  • Loop Variable: socv_Specific_Surveys_single

Inside this second loop, assign the Agent Id to the Agent__c field of the Survey. Then add this Survey to a new Record Variable called socv_Surveys_to_Update. To do so, create the Record Variable, then create an Assignment element to add the single Survey__c record to the socv_Surveys_to_Update. 

After exiting the second loop, increase the Agent Counter variable by 1

Finally, update all the Survey__c records at once, using a Record Update element on the socv_Surveys_to_Update record variable. 

You can then add a Screen element with any information you want:

And this is the final Flow:

Now, activate the Flow, then add a New Action  that calls this Flow from within the Round_Robin_Assigner__c object. 

Let’s run thre Assigner! Here is a screen before clicking on the button, and then the result Notice that each Survey ois now assigned to a specific Agent!

You can modify the objects based on your requirements, but the idea is the same! 



VS Code Extension: Uncrustify Code Formatter

This is the second post about VS Code Fomatters for Salesforce. In this post, I will explain about Uncrustify.

Download and install Uncrustify:

The first step is to get Uncrustify on our computer

  • Go to:
  • Depending on your OS, download the right package. In my case, I got the Windows 64-bit ZIP file: 
  • Unzip the ZIP file to a folder. I picked C:\Uncrustify\
  • Add this folder to the Windows PATH environment variable:
    • Open the “Control Panel”
    • Go to “System and Security”, then to “System”
    • Click on “Advanced system settings” 
    • Go to the Advanced tab, and click on “Environment Variable” button at the bottom
    • In the System Variables section, select the “Path” variable and click on Edit 
    • Nowm click on New, and add the folder above. In my case: C:\Uncrustify\. Click Ok on all open windows. 


Add Uncrustify to VS Code:

Now, it’s time to add the Uncrustify extension on Visual Studio Code. For that:

  • Go to the Extensions tab in VS Code
  • Search for “Uncrustify
  • Add it to VS Code
  • Restart VS Code


Configure Uncrustify in VS Code:

  • In Visual Studio Code, press Ctrl +Shift + P, and search for “Uncrustify: Create Default Config File”. Select it and press Enter
  • This will create a file that you can now access on the Explorer pane pon the left. The file name is “uncrustify.cfg”
  • Click on this file to open it in VS Code
  • Note: to see the Save menu, hover to the right side.
  • Go to the “General Options”
    • Set the value 4 in Input_Tab_Size
    • Set the value 4 in Output_Tab_Size
    • Click on “Save”
  • Go to the “Indenting”
    • Set the value 4 in indent_columns
    • Set to true indent_class
    • Click on Save
  • Feel free to go through al the settings and changing them accordingly 


Run Uncrustify:

  • To Run Uncrustify to format code, open any Apex file, then press Ctrl + Shift + P
  • Search for the word “Format” 
  • Choose “Format Document” 
  • Select Uncrustify. This option will only be displayed if you have multiple formatters. In my cae, I have both Prettier and Uncrustify. 
  • You can avoid all these steps by simple pressing Shift + Alt + F to format the whole document, or Ctrl + K Ctrl + F to format the selection only. 



  • You can use the same “uncrustify.cfg” file in other projects. Just copy it from the source project folder and paste it in the new project folder. 
  • Just as the Prettier post before, you can set the Default formatter in VS Code Settings:
    • In VS Code, go to File – Preferences – Settings.
    • Search for Format.
    • This is where you can also configure many settings for Formatting, like the efautl Formatter, fortmat on Paste and so on…

VS Code Extension: Prettier Code Formatter

VS Code is becoming the most popular Salesforce IDE, but unfortunately, VS Code does not have a default code formatter for Apex! For that, we can use some extensins like Prettier and/or Uncrustify. In this post, I will explain how I installed Prettier, and in the next post I will explain about Uncrustify.


Install Node.Js on your computer:

To begin, Node.JS should be installed on your computer. If it is not installed yet, follow these steps: 

  • Go to:
  • Depending on your OS, download the right package. In my ase, I got the Windows 64-bit MSI file. 
  • Choose the installation folder. I picked: C:\Program Files\nodejs\
  • Make sure that “Add to PATH” installation option is checked. 

Now, add the Node.JS Extension Pack extension to VS Code:

  • Go to the Extensions tab in VS Code
  • Search for “Node.js Extension Pack
  • Add it to VS Code
  • Restart VS Code 

Once VS Code is restarted, let’s install Prettier.


Install Prettier:

  • First, you don’t already have a package.json in your project, run the command “npm Init”. Check the left Explorer pane to see if this file exists, or search for it usong Ctrl + P. 
    • Go to the Terminal tab, and type “npm init”. Or, press Ctrl + Shift + P, and choose “npm: Run Init”
    • Accept all the defaults
  • Note that this step should be done once per org.
  • Install Prettier by running in the Terminal: npm install --save-dev --save-exact prettier prettier-plugin-apex
  • This should be done for each org. 
  • Create a Prettier configuration file called .prettierrc, in the root of your project, by right clicking on the left Explorer pane, and selecting “New File”. The file should be called .prettierrc, with a dot at the beginning. 
  • Copy and paste the below content and paste them int the file. Save the file.
    "trailingComma": "none",
    "overrides": [
        "files": "**/lwc/**/*.html",
        "options": { "parser": "lwc" }
        "files": "*.{cmp,page,component}",
        "options": { "parser": "html" }
  • Install the Prettier extension for VS Code:
  • Go to the Extensions tab in VS Code
  • Search for “Prettier Code Formatter
  • Add it to VS Code
  • Restart VS Code


Run Prettier:

  • To Run Prettier to format code, open any Apex file, then press Ctrl + Shift + P
  • Search for the word “Format” 
  • Choose “Format Document” 
  • Select the Prettier Formatter. This option will only be displayed if you have multiple formatters. In my case, I have both Prettier and Uncrustify. 
  • You can avoid all these steps by simple pressing Shift + Alt + F to format the whole document, or Ctrl + K Ctrl + F to format the selection only. 

As you can see, my Apex file has been formatted! 



  • If you want to ensure that all your files are formatted, enable the setting editor.formatOnSave in VS Code. For information about configuring your settings, see User and Workspace Settings in the Visual Studio Code docs.

  • Apex Prettier runs slower than most other formatters. In some cases formatting will not succeed because VS Code will time out the operation after 500ms. In order to ensure Apex code has enough time to format your documents we recommend changing the VS Code settings as follows.

  "editor.formatOnSaveTimeout": 5000

This can be done also from the File – Preferences – Settings. Search for Format. This is where you can also configure many settings for Formatting, liek the efautl Formatter, fortmat on PAste and so on…

The Salesforce ID, a Deep Dive!

Ever wondered what this long string of characters called the Salesforce ID means? How is it built? Will we ever run out of IDs? This post will dive into that!


What is a Salesforce ID and what is it composed of? 

In simple terms, a Salesforce ID is an identifier to a Salesforce record. As you know, Salesforce has Objects, like Account, Contact, Opportunity, Case, Custom Objects, etc… These are equivalent to Database tables: they are composed of different fields, or columns in Database terms, and records, or rows in Database terms. Each one of these row has a unique identifier: this is the ID! As an example the Account object:

  ID   Name   Type   Phone
  KeJo Solutions   Partner   6475622230
  Salesforce   Vendor   2023433443
  Universal Containers   Customer   6023345091

On top of records, the ID can be used to reference metadata elements, like Group, Queue, Record Type…etc. So, for example, whenever you create a Queue, guess what is associated to it? A Salesforce ID. 

Each ID is either a 15-character case-sensitive string, or an 18-character case-insensitive string. Let’s start by how a 15-character ID is composed:

Each one of the 15 characters can be:

  • A lowercase letter (a-z) – that is 26 possibilities 
  • An uppercase letter (A-Z) – that is 26 possibilities again
  • A Number (0-9) – that is 10 possibilities 

That would give us a total of (26 + 26 + 10 = 62) possibilities for each character, therefore the ID is a base-62 string. 


Now, let’s talk about the different components of the ID. To do that, we will use the above Account record ID:


Which we can divide into these distinct parts:

001 55 0 0000WO1Zi AAL


Here’s the meaning of each part:  

  Character    Example   Meaning
  char 1-3 (3 chars)   001   Key Prefix
  char 4-5 (2 chars)   55   Instance
  char 6 (1 char)   0   Reserved
  char 7-15 (9 chars)   0000WO1Zi   Unique identifier
  char 16-18 (3 chars)   AAL   Case-insensitivty checksum
  • Key Prefix (3 characters): Determines the type of record. With the first 3 characters, you can know which type this ID belongs to! For example, when you read 001, this is an Account ID, 005 is a User ID, 006 is an Opportunity, 00Q is a Lead, 500 is a Case, etc…


  • Instance (2 characters): Determines the Instance or server on which the record has been created on. For example, I just created an Account on my developer org, which is hosted on the instance UM3, the 4th and 5th characters of this Account were 4H, which means that 4H idetifies the UM3 instance. Note that a while back, only the 4th character was used to identify the originating Instance, and the 5th was reserved, but think of what happens when Salesforce was about to get 62 Instances? Clearly a single character was not sufficient to identify the originating Instance anymore, hence the use of the 5th character in addition to the 4th. That would give a theoretical max of 62^2 = 3,844 possible Instances.


  • Reserved (1 character): Reserved for future use! Would Salesforce ever tun out of instance identification with 2 characters? You never know! (I did the math, if we add the 3rd character to the Instance identifier, that would give a whoping 238,328 possible Instance identifications). 


  • Unique identifier (9 characters): this is what identifies the record, and the possible combinations is HUGE: 62^9 possible combinations, to be precise 13,537,086,546,263,552! 


  • Unique identifier (3 characters): Used to allow for an ID to become case insensitive. Why we need this? Well, imagine working with applicaitons like Access which do not recognize that 50130000000014c is a different ID from 50130000000014C, an 18-digit, case-safe version of the ID was introduced. The case-insensitive ID is identical to the 15-character case-sensitive ID, but with these 3 extra characters appended to indicate the casing of each of the original 15 characters. This way, 18-character IDs can be safely compared for uniqueness by case-insensitive applications. Here is a way to convert 15-char to 18-char IDs. And here’s a website for the same. Are you interested to know the technical details of how these 3 characters are calculated? Check this page!


Some Record ID Key Prefixes

Here are some Key Prefixes. Next time you read an ID that starts with 001, know this is an Account ID!

 Entity   Key Prefix
  Account   001
  Contact   003
  User   005
  Organization   00D
  Group   00G
  Report   00O
  Task   00T
  Event   00U
  Profile   00e
  Lead    00Q
  ContentDocument   069
  ContentDocumentLink   06A
  WorkOrder   0WO
  ServiceAppointment   08p
  Dashboard   01Z
  PermissionSet   0PS
  Campaign   701
  CaseComment   00a
  Order   801


Determine the record type using some code:

Finally, here is a tiny code snippet that can be used to get the record type from the ID. You can invoke it from any class, or from Anonymous Apex: 

// Sample Id
Id myID = '00561000000Mjya'; 
System.debug('This record is a '+ myID.getsobjecttype());
// Output is: "This record is a User"

Lightning Flow to copy File Links from Opportunity to Account


Use Case: 

As a Salesforce Admin, you are requested to copy all the Files attached to an Opportunity to its parent Account whenever the Opportunity is Closed-Won. This way, the Account record will have each file added to any of the Opportunities of the Account. 


A little bit of background:

To tackle this requirement, we should first understand the data model behind Files and how they are attached to records in Salesforce. 

The Data model for Content Document is:


The key objects to understand

  • ContentDocument: Represents a document that has been uploaded to a library in Salesforce CRM Content or Salesforce Files
  • ContentDocumentLink: Represents the link between a Salesforce CRM Content document or Salesforce file and where it’s shared. A file can be shared with other users, groups, records, and Salesforce CRM Content libraries. Fields of this object:
    • ContentDocumentId: is the Id of the ContentDocument
    • LinkedEntityId: ID of the linked object. Can include Chatter users, groups, records (any that support Chatter feed tracking including custom objects), and Salesforce CRM Content libraries.
    • Visibility: picklist with 3 values. ‘V’ for Viewer permission – the user can explicitly view but not edit the shared file. ‘C’ for Collaborator permission – the user can explicitly view and edit the shared file. ‘I’ for Inferred permission. The user’s permission is determined by the related record.
    • ShareType: picklist with 3 values. AllUsers – the file is available to all users who have permission to see the file. InternalUsers – the file is available only to internal users who have permission to see the file. SharedUsers – the file is available to all users who can see the feed to which the file is posted.

So, to simplify, anytime you upload a File via the Files related list, you create a ContentDocument and a ContentDocumentLink that links the ContentDocument to the record! Simple. 



To tackle this requirement, we’re going to use Lightning Flow (Process + Flow). The process will be simply used to call the Flow when the Opportunity is Closed-Won, and to pass the Id of the Opportunity to the Flow. The process will be created AFTER the flow.

The Flow will be used to:

  1. Get the Opportunity details (mainly the AccoundIt field)
  2. Get all ContentDocumentLinks that are associated to the Opportunity and put them all in a single sObject Collection Variable
  3. Loop through these ContentDocumentLink records
  4. On each pass, add a new ContentDocumentLink record to a Collection Variable – but specify that the LinkedEntityId is the AccountId
  5. Finally, create the collection of ContentDocumentLink. This way, the links will be available on the Account

Here’s the final flow:


To begin, let’s create the Autolaunched Flow:


The first thing to do is to create the recordId variable. This variable will be used by the process to pass the Id of the Opportunity. Make sure you make it “Available for Input” as the process will pass the Opportunity Id to this variable. 


Now, we’ll use recordId to the the Opportunity record and store it in sov_Opportunity. Field to include is AccountId. 


Next, let’s get the ContentDocumentLink records that reference the Opportunity via the LinkedEntityId field. We store all these records in the variable socv_CDL_Opty.


We’ll now loop hrough this list of ContentDocumentLink records, and on each pass, we’ll assign a new record of ContentDocumentLink, then add it to the collection. The variable socv_CDL_Opty_Single is a single ContentDocumentLink record that will be used on each pass. Make sure to define it.


On each pass, 2 assignments will happen. The first is to assign a new ContentDocumentLink record, while specifyign the LinkedEntityId field to be the AccountId field retrieved from the first step.

The second assignment is to assign this single record to a collection of records.


And finally, we should create the ContentDocumentLink collection variable using the Record Create element:


The flow is done, we’ll now create the process that will simply call the Flow when an Opportunity is in the Closed-Won stage:


Here’s the Unmanaged Package that contains both the Flowand the Process. You can use it and modify it as needed, you can also use the same logic to apply for other objects and requirements.

For Production/Dev Edition:

For Sandbox:



Trigger to convert FSL Service Appointment time to the Service Territory timezone

Use case:

In Field Service Lighting, and upon scheduling Service Appointment to Resources, the system uses the local user’s Timezone to populate the Scheduled Start and Scheduled End date/time fields: 

But what if the Serivce Appointment location is in a Territory with different Timezone than the logged-in User’s Timezone? We need to find a way to capture the Scheduled Start Time in this Timezone as opposed to the default behavior that displays the Scheduled Start date time following the logged-in User’s timezone. Why? Well, what if we want to send a notification email to the customer informing them of the Scheduled Start time? It wouldn’t make perfect sense if it was not based on their local Timezone! 

As as example, if the Salesforce Dispatcher is in Toronto (Eastern Time), and the Service Appointment belongs to a Service Territory that follows the Pacific Time, then we need to see the Scheduled start date time in the Pacific, and not only in Eastern Time, and then we will use this local Scheduled Start in the email Template sent to the customer upon Dispatching for example. 

The data model is as follows:

  • Work Order is first created. It has a lookup to the Service Territory
  • The Service Territory has a lookup to the Operating Hour
  • The Operating Hour has a field called TimeZone that specifies the Timezone of this Territory 
  • Service Appointment can be created directly with the Work Order or after the Work Order. 
  • Service Appointment has a lookup to the parent Work Order 
  • Service Appointment has a lookup to the Service Territory, which has a lookup to the Operating Hour record, which has the TimeZone 



Upon creating the Service Appointment, the Scheduled Start date time is saved as the current Salesforce User Timezone! There is no way to get the Scheduled Start time on the Territory timezone!


A little bit of background:

There exists a method that deals with time conversion based on Timezones! Passing a day time field to a Format method returns the date time in the format specified, and following the timezone specified. For example: 

System.debug('YYYY-MM-dd HH:mm:ss', 'America/Los_Angeles'));


Returns the current time of the America/Los_Angeles timezone. 

17:35:08:002 USER_DEBUG [1]|DEBUG|2019-06-13 14:35:08


You can find all the available Timezone names here:



To sort this, we will use a trigger that will use the format method to give the date time text value.

  • On the Service Appointment object, let’s create a custom field called Local_Scheduled_Start__c,
  • Let’s then create an After Insert and After Update Service Appointment Trigger 
  • Finally, we will create the Trigger Handler class that contains the method called by the Trigger


The Trigger is used to simply call a method on the Handler class:

trigger ServiceAppointmentTrigger on ServiceAppointment (after insert, after update) { 
    If (trigger.isAfter && trigger.isUpdate) {
    If (trigger.isAfter && trigger.isInsert) {


And now the Apex Class Handler that contains the method called by the Trigger:

public class ServiceAppointmentTriggerHandler {
    //Boolean to control recursion as this is after update
    public static Boolean boolStopRun = false;
    public static void handleAfterInsertUpdate(List<ServiceAppointment> triggerNew) {
        //Stop the run if this code was already run
        if (boolStopRun) return;
        //Make boolStopRun true in order to stop the run and prevent recursion - should be here not at the end 
        boolStopRun = true;
        //1- get all SA Ids
        Set<Id> setSAId = new Set<Id>();
        for (ServiceAppointment sa : triggerNew) {
            if (sa.SchedStartTime != Null) {
        //2- Get List of SAs including the ServiceTerritory OperatingHours TimeZone
        List<ServiceAppointment> lstSA = [SELECT Id, AppointmentNumber, SchedStartTime, ServiceTerritory.OperatingHours.TimeZone
                                          FROM ServiceAppointment 
                                          WHERE Id IN : setSAId];
        //3- create thge list with Local Scheduled Start and update it at the end 
        List<ServiceAppointment> lstSaToUpdate = new List<ServiceAppointment>();
        for (ServiceAppointment sa : lstSA) {
            //get the Timezone, returned value example "America/Los_Angeles"
            String strSTTZ = sa.ServiceTerritory.OperatingHours.TimeZone;
            //Passing a day time field to a Format method returns the date time in the format specified, and following the timezone specified
            String strLocalSchedStartTime = sa.SchedStartTime.format('MM/dd/YYY HH:mm', strSTTZ);
            ServiceAppointment saUpdated = new ServiceAppointment();
            saUpdated.Id = sa.Id;
            saUpdated.Local_Scheduled_Start__c = strLocalSchedStartTime;
        update lstSaToUpdate;       

And here’s the result: