From Chris Albanese: Spring 21 Feature: Resource Priority and Filtering

Salesforce Scheduler introduced a new feature in the Spring ’21 Release which allows you to control very precisely what resources are considered when scheduling an appointment. This provides many benefits and opens up the solution to address additional requirements.

Resource Priority and Filtering gives you an easy way to control and influence what resources are considered when scheduling an appointment. Up to this point it was the required skills of the Work Type and the Service Territory Membership that defined the pool of resources. Now you can layer on another filter. What are some use cases for this?

  • Gold/Silver/Platinum tiered service
  • Preferred Language
  • Fair distribution of work to resources (think Round Robin Lite)
  • Prioritized distribution of work (today I want all of my appointments to go to Connie)

It helps you address these requirements while, at the same time, keeps the number of Work Type Groups down to a minimum.

Check this out below write up and also this quick demo video.

Work Type Groups, Work Types and Service Territories

Let’s say you’re a bank and you offer appointment scheduling for general banking, lending and retirement and investing services. And let’s also say that you provide differentiated services based on the customer’s investments with the bank, where customers with lots of investments are serviced by ‘gold’ advisors, customers with a good amount are serviced by ‘silver’ advisors and everyone else is serviced by ‘bronze’ advisors.

By now you’ve probably figured out that for a given Service Territory, you can only have 1 work type from a given work type group.
Let’s look at a example:

  • Service Territory = Market Street Branch
  • Work Type Group = General Banking
  • Work Types = General Banking Gold, General Banking Silver, General Banking Bronze

If you want to offer Gold, Silver and Bronze General Banking Services, you might think you could add those 3 Work Types to the Market Street Branch. Well, you can’t because you can only pick 1 from the the General Banking Work Type Group. So you could create Work Type Groups for General Banking Gold, General Banking Silver and General Banking Bronze. And if you want to offer the same for Lending and Retirement Services you would wind up with 9 Work Type Groups when you really want 3.

2021-01-31_21-23-37.png

What you want the user to see – 3 Work Type Groups

2021-01-31_21-44-36.png

What they will see – 9 Work Type Groups

Resource Priority and Filtering to the Rescue

Resource Priority and Filtering allows you to offer only the 3 Work Type Groups but still ensure that Gold customers are serviced by gold bankers, silver by silver bankers, and bronze by bronze bankers.

What do you need to do? Leverage the Resource Priority and Filtering Feature – see the help and training article.
There is a new parameter the Resource Selection screen that allows you to filter the list of resources above and beyond what the required skills and service territory provide. Check out my example below for how to set this up.

2021-01-31_22-43-56.png

Example

When running the scheduling flow, use the Filter by Resource Ids parameter to consider only resources whose Tier matches the customer’s Tier.

New fields

To the account object, I’ve added a picklist field called Tier, with values of Gold, Silver and Bronze.
And to the service resource object a picklist field called Tier, with values of Gold, Silver and Bronze. I’ve also created a formula field on the service territory member object which equals serviceresource.tier__c. (this helps make the flow easier)

Flow Changes

In the flow, add a step to retrieve the customer’s tier. Also add a step to retrieve all of the service territory member records where the tier matches. Make sure that you retrieve the STMs after the location step, since we need to know the territory id. In the screen shot below, you can see that after the Location Screen, I’ve added the 2 get records, the loop and the 2 assignment steps.

2021-01-31_22-16-16.png
2021-01-31_22-18-02.png

Then loop through all of the STMs you find to concatenate them together in a string. Here’s what’s inside the add SR ids to string assignment step in the screenshot above.

2021-01-31_22-20-19.png

Since there’s an extra comma at the end of this, I created a formula field to which excludes the last character of the string.

2021-01-31_22-22-39.png

And then assign the formula to the FilterByResourcesId string.

2021-01-31_22-23-50.png

Results!

Only 3 Work Type Groups are needed yet we can still provide Gold, Silver and Bronze service. In the screen shot to the right, 5 resources would have been returned without this feature, but I was able to limit the list to just the 2 gold bankers.

2021-01-31_22-32-21.png
2021-01-31_22-37-42.png

Resource Priority and Filtering Guidelines

Like everything else in Salesforce, there are best practices, guidelines and limits to consider. For Resource Priority and Filtering, 50 and 10 are the magic numbers. 50 is the maximum number of Service Resource Ids that can be passed into the filter, BUT, you want to make sure that after the application of the relevant skills, territory and availability checks, the resulting number of service resources with availability is no greater than 10.

Part II – Using Skills and APEX to power Resource Priority and Filtering

Check this out to learn how to further extend this feature using Apex.

In the example above, we used a service resource custom picklist field to identify which resources had the Gold/Silver/Bronze “skill”. What if we wanted to use the skill object itself. In other words, we have set up skills for Gold, Silver and Bronze and we have assigned those to bankers accordingly. We want the flow to consider only those resources that have the both the skills needed through the work type group (and related work type) as well as the skill needed from the account tier.

Getting Records from 1 object where Service Resource Ids are in another object

Normally, I would say do this all in flow but in reality we need to do a loop inside of a loop – Find all resources in the territory who also possess the desired tier skill. Unfortunately, we cannot do this type of query in flow. As you might have seen with a simple web search, flow does not support the equivalent of a SOQL “IN” condition. But we can do this with an APEX class with an invocableMethod, and we can do it with just a few lines of code.

Create an invocableMethod

As you’ve probably seen in other blogs, invocableMethods extend the power of flow with Apex code. In my Apex class below, I’m accepting a list of service territory member records (the resources in the territory) and a skill record and returning a string which is a comma separated list of service resource ids of only those service resources with the skill.

Remember with invocableMethods, you pass in a list of something and it returns a list of something. In this example, we’re passing in a list of a user-defined type called ResourceFilter, which consists of a List of Service Territory Member records and a Skill record and the method returns a list of strings.

Apex class example

//this class returns a common separated string of service resource ids
//it accepts a list of service territory member records and filters those with the required skill
public with sharing class PriorityResources {

@invocableMethod
public static list<string> getResources(list<ResourceFilter> rfList) {
if (rfList==null) return new list<string>{''};
set<string> setResource = new set<string>();
set<string> setResourcewithSkill = new set<string>();
ResourceFilter rf = rfList[0];
//create a set of service resource ids from the stmlist passed in
for(serviceterritorymember stm : rf.stmList) setResource.add(stm.serviceresourceid);
try {
datetime dt = datetime.now();
//create a set of service resource ids that have the skill passed in
for(serviceresourceskill srskill : [select serviceresourceid from serviceresourceskill where serviceresourceid in :setResource and skillid = :rf.skillRequired.id and effectivestartdate <= :dt and (effectiveenddate = null or effectiveenddate >= :dt) ])
setResourcewithSkill.add(srskill.serviceresourceid);
}
catch(system.exception e) {
return new list<string>{''};
}
//convert the set of resource ids into a comma separated string
return new list<string>{String.join(new list<string>(setResourcewithSkill),',')};
}

public class ResourceFilter {
@invocableVariable(required=true)
public list<serviceterritorymember> stmList;
@invocableVariable(required=true)
public Skill skillRequired;
}
}

Flow Example

Like in the previous example, in the flow, add a step to retrieve the customer’s tier. Also add a step to retrieve all of the service territory member records where the territory matches. And also retrieve the skill record that matches the customer’s tier. Make sure that you retrieve the STMs after the location step, since we need to know the territory id. In the screen shot below, you can see that after the Location Screen, I’ve added the 3 get records and the Apex action. You can also see how the Apex action is setup: I’m passing in the skill object and the STM objects (this is a list) and the Apex class will return the CSV string of Service Resources into the variable FilterByResourcesId. That’s it, you will have a list of Service Resources who work in the Territory who have the Tier Skill and these will be passed to the resource selection screen. This screen will further filter out only those resources which also have the skills needed for the selected Work Type Group (and work type).

2021-02-03_22-34-27.png
2021-02-03_22-42-43.png

From Chris Albanese: Build your own appointment review screen

Do check out the existing possibilities to make changes to the appointment review screen before trying this out.

When you look at a Salesforce Scheduler flow, there’s a core action called saveAppointment. Did you ever wonder what it does, what the inputs are and whether you can tailor it?

2021-02-11_08-48-35.png
2021-02-11_08-47-44.png

What does it do?

It takes the values gathered during the flow interview and creates or updates a ServiceAppointment record and an associated AssignedResource record. If you provide optional attendees, it will also create AssignedResource records for each optional service resource id provided.

The help and training article is locate here, but let’s look at what’s inside the inputs provided this action.

What is contained in the input fields?

The easiest way to see what is passed into the Save Action is to run the flow in debug mode. There you will see the values that are stored into each field passed into the Save Action.

2021-02-11_13-45-58.png
  • Look in the right hand panel of the debugger. You can see the value of each field.
  • On a given screen, you will have to press next to see what the values are for that screen.
  • You can scroll up and down in the debug details panel to review the values and every step.

After you press finish, scroll up and check out what is in the Save Appointment step.

2021-02-11_13-48-07.png

What do we see here?
Inputs:

  • optionalAttendees = a list of comma separate values of Service Resource Ids.
  • serviceAppointmentFields = a JSON string of the Service Appointment fields, including fields such as:
    • start and end times
    • service territory id
    • Service Resource ID for the service resource the appointment is for
    • ParentRecordId = the Account Id (or the opportunity or lead id)
    • other fields on the page layout – there’s a custom field I added to the Service Appointment object and page layout called My_Custom_Field
  • selectedTimezone – the timezone selected by or defaulted for the user

Output:

  • the Service Appointment Id

How do these values get set?

In a Salesforce Scheduler flow, the Screen steps each have components on them that save values to variables in the flow. For example, the Select Location screen has a screen component that saves the Service Territory Id and Address fields to the corresponding fields in the ServiceAppointment variable, as you can see in the screen shot below.

2021-02-11_14-34-04.png

But how does the serviceAppointmentFields variable get set?

This field is the bulk of what is passed into the Save Action.

It gets set by the Review Screen component that is located on the Review Screen.

2021-02-11_14-41-42.png
  • This component is located in the Review Screen.
  • It gives the user the ability to review the appointment prior to saving. For example, they can enter a value into the Subject field or other fields, including custom ones.
  • But this component is really a black box.

What if I wanted to set the values for the serviceAppointmentFields variable myself?

So as I have written above, the Review Screen component is really a black box. It takes the fields from the review screen along with other fields in the ServiceAppointment object variable and the WorkTypeGroupId variable and creates a JSON string from them.

What is JSON you ask? You’ve probably heard of it and you can definitely Google it and find out more, but it’s essentially a collection of name:value pairs, and you see that in the debugger output above. It’s very powerful and allows you to store lots of rich information, including arrays, in a text field.

You can create your own JSON string using a formula field. You can pass that formula field into the Save Appointment action. Or you can assign it to the serviceAppointmentFields variable in an assignment step.

Here’s an example of a formula field that I have used to pass into the Save Appointment action:

'{"Description":"' &{!ServiceAppointment.Description}& 
'",'& '"SchedStartTime":"' & substitute(substitute(text({!ServiceAppointment.SchedStartTime}),' ','T'),'Z','.000Z')&
'",'& '"SchedEndTime":"' & substitute(substitute(text({!ServiceAppointment.SchedEndTime}),' ','T'),'Z','.000Z')&
'",'& '"Subject":"' & {!ServiceAppointment.Subject}&
'",'& '"AdditionalInformation":"' & {!ServiceAppointment.AdditionalInformation}&
'",'& '"AppointmentType":"' & text({!ServiceAppointment.AppointmentType})&
'",'& '"Comments":"' & {!ServiceAppointment.Comments}&
'",'& '"ParentRecordId":"' & {!ServiceAppointment.ParentRecordId}&
'",'& '"Street":"' & {!ServiceAppointment.Street}&
'",'& '"City":"' & {!ServiceAppointment.City}&
'",'& '"State":"' & {!ServiceAppointment.State}&
'",'& '"PostalCode":"' & {!ServiceAppointment.PostalCode}&
'",'& '"Country":"' & {!ServiceAppointment.Country}&
'",'& '"WorkTypeGroupId":"' & {!WorkTypeGroupId}&
'",'& '"ServiceTerritoryId":"' & {!ServiceAppointment.ServiceTerritoryId}&
'",'& '"ServiceResourceId":"' & {!ServiceResourceId}&
'",'& '"Phone":"' & {!ServiceAppointment.Phone}&
'",'& '"Email":"' & {!ServiceAppointment.Email}&
'",'& '"IsAnonymousBooking":"' & if({!ServiceAppointment.IsAnonymousBooking},"True","False")&
'",'& '"isSlotChanged":"' & "False"&
'"}'

What do you see in the field above?

Lot’s of care taken to make sure each name:value pair is enclosed in double quotes and that each pair has a colon between it and is separated by a comma from the next pair. And the whole string is wrapped in curly braces {}. Note the added complexity of the SchedStartTime and SchedEndTime fields, to convert them from datetime data types to a text type in a “”yyyy-MM-dd’T’HH:mm:ss.SSSZ”” format. I’ve also had to use a text() function to convert the picklist field called AppointmentType to a string value.

Can I replace the out of the box review screen with my own review screen?

Yes you can! You can display whatever content you like using standard flow components such as Display Text and you and capture user input using standard flow components such as Input Text or Date.

You just need to make sure you create the JSON string to pass into the Save Appointment action.

Minimum values required: make sure the at least the following fields are passed in order to get the desired results :

{"ParentRecordId": "001xx0000000000000",
"ServiceTerritoryId": "0Hhxx0000000000000",
"ServiceResourceId": "0Hnxx0000000000000",
"WorkTypeGroupId": "0VSxx0000000000000", (for new)
"WorkTypeId":"08qxx0000000000000" (for modify)
"SchedStartTime": "2021-02-21T20:30:00.000Z",
"SchedEndTime": "2021-02-21T21:00:00.000Z",
"IsAnonymousBooking": <VALUE SET IN FLOW VARIABLE>,
"isSlotChanged": <false for new, true for modify>,
"schedulingPolicyName": "<same policy name set in flow>"
}

Details of the Save Appointment inputs

  • Lead – use this for guest inbound flows (unauthenticated) only. This is a record (single) variable containing the new Lead record created for the guest user running the flow.
  • Optional Service Resource Ids – comma separated list of Service Resource Ids. This represents the optional resources for the service appointment, if present.
  • Selected Timezone – the timezone selected by or defaulted for the user – see this article for the list and use the value for the Timezone (the part of the TIME ZONE NAME field in parenthesis, such as America/Los_Angeles).
  • Service Appointment Fields – a single text field containing the JSON for the Service Appointment – extensively described above. For guest inbound flows, do not provide a value for parentRecordId, as this will be provided via the Lead object described above.
  • Service Resources – if using Multi-Resource scheduling – this is a single text field containing the JSON for the list of Service Resources and Territory Members selected, including the primary resource. Example below (record ids are obfuscated in this example).
  • [{“UserPhoto”:”https://xxxx–c.documentforce.com/profilephoto/729B0000XXXXXXXX/F”,”ServiceTerritoryMemberId”:”0HuB0000000XXXXXXXX”,”IsActive”:true,”ResourceType”:”T”,”RelatedRecordId”:”005B000000XXXXXXXX”,”Id”:”0HnB0000000XXXXXXXX”,”Name”:”Connie Ruiz”,”sobjectType”:”ServiceResource”,”AttendanceType”:”Primary”},
    {“UserPhoto”:”https://xxxx–c.documentforce.com/profilephoto/729B0000XXXXXXXX/F&#8221;,”ServiceTerritoryMemberId”:”0HuB0000000XXXXXXXX”,”IsActive”:true,”ResourceType”:”T”,”RelatedRecordId”:”005B000000XXXXXXXX”,”Id”:”0HnB0000000XXXXXXXX”,”Name”:”Anita Gonzalez”,”sobjectType”:”ServiceResource”,”AttendanceType”:”Required”},
    {“UserPhoto”:”https://xxxx–c.documentforce.com/profilephoto/729B0000XXXXXXXX/F&#8221;,”ServiceTerritoryMemberId”:”0HuB0000000XXXXXXXX”,”IsActive”:true,”ResourceType”:”T”,”RelatedRecordId”:”005B000000XXXXXXXX”,”Id”:”0HnB0000000XXXXXXXX”,”Name”:”Mark Mayo”,”sobjectType”:”ServiceResource”,”AttendanceType”:”Required”}]
  • WorkType – if using a Modify flow, this is a record (single) variable containing the Work Type for the Service Appointment being rescheduled. It is not used for new appointments.