Posts

From Shantinath Patil: Schedule a support advisor

Let’s think of you as a support agent. You get a call from a customer asking for an appointment for some questions he has on the case and wants to talk with the case owner. So what can do you What will you do in this “case?”

Salesforce Scheduler can help you in this “case”. Let’s drill down on what all we need to accomplish this:

  1. Case owner as a Service Resource
  2. Case reference to Appointment
  3. A flow
  4. Quick action on Case to initiate the flow.

A logical flow of the approach we will take in this use case will look like the following:

Get Case Details

Since this flow will run on the case, we need to get the details of it. First, we can set the “recordId” input parameter to get the current case, automatically giving us the case Id. Then, using this variable, we can query the case using the “Get Records” element in the flow.

Note that we need AccountId to map it to the ParentRecordId of the Service Appointment. OwnerId is needed to get the related Service Resource.

If we want to tag the current CaseId, we can create a new lookup on Service Appointment. A custom field is needed since Service Appointment can only support Account, Lead, and Opportunity except for Case! To ensure that value is populated in that lookup, we can override the JSON we pass in the final Save Action defined in the flow. For more details, you can refer to the blog here: https://unofficialsf.com/build-your-own-appointment-review-screen/. For now, we can skip it.

Get Service Resource

If we want to set the case owner as of the person who will work on this case, we need to make sure that he is a service resource. For more details, please check this help doc: https://help.salesforce.com/s/articleView?id=sf.ls_create_resources.htm&type=5.

To fetch the service resource, we can add the “Get Records” element like below. In this, we are fetching Service Resource based on Case OwnerId.

Leverage filterByResourceIds

Scheduler flow allows you to filter the resources based on the list of Ids you pass to it. You can read more details here: https://help.salesforce.com/s/articleView?id=sf.ls_enable_service_resource_filtering.htm&type=5.

We can leverage this functionality for our use case. Since we want our case owner to be the Service Resource, we can add the Service Resource Id from the previous step to filterByResourceIds. Additionally, we can make anonymous booking as true and resource booking as false. This will make sure that we skip the OOB triage screen and resource selection screen.

The advantage we get from this is if the case owner is not a Service Resource, existing logic will fetch any available resource anonymously. You will not have to worry about resource selection. End to end experience will be very seamless for the agent who is booking that appointment.

Once all the nodes and assignment is done, you can test the flow using the “Debug” option and activate it once it’s working as expected.

Setup

After this step, it’s up to you how you want to distribute the flow. For this blog, we are using Case Action to create a new appointment. Since the “recordId” flow variable is set for “Available for input” in the flow, this action will automatically set it to the current record on the detail page.

You can add this action to the case layout. The end-to-end flow should work something like this:

We have added all the metadata to this git repo for your reference. Please feel free to deploy it to your Sandboxes and try it out! Link: https://github.com/snathpatil/case-appointment-booking

Note if you’re looking for a virtual advisor then do have a look at this blog: https://unofficialsf.com/tele-or-virtual-scenarios-using-salesforce-scheduler/

Scheduler is not to be positioned in cases where the Service Resource is travelling to a customer’s location

From Ankit Srivastava: Event integration with Salesforce Scheduler’s Appointment Distribution

Salesforce Scheduler is getting capability to equitably distribute meetings between eligible Service Resources in Winter ’22 release. It will support an implementation of Load Balancing logic working on Service Appointment records to do the Appointment Distribution.

In following sections we will see the architecture of Appointment Distribution feature and how can we enhance it to include Events (Salesforce Calendar) also in the utilisation calculation for Appointment Distribution.

Design of Appointment Distribution

Appointment Distribution feature is built upon two main sObjects:

  1. AppointmentScheduleAggr
  2. AppointmentScheduleLog

For all practical purposes, individual utilizations that make it into AppointmentScheduleLog entity are rolled up as summary records in AppointmentScheduleAggr entity (if IsUsedForResourceUtilization parameter is set to true).

AppointmentScheduleLog entity supports any of Service Appointment, Resource Absence or Event as a polymorphic lookup in its RelatedRecord field. Out of the box, Salesforce Scheduler only inserts records for corresponding Service Appointments in AppointmentScheduleLog entity.

Incorporating Event in Appointment Distribution’s utilisation

As evident from the design showcased above we can incorporate Event in Appointment Distribution’s utilisation with some coding and customisation. Let us now create a demo trigger which can be used to demonstrate this.

Salesforce Scheduler creates Event entries if “Event Management” setting in turned ON. If “Aggregate Resource Use” is also ON in same org then we will get 2 entries instead of 1 in AppointmentScheduleLog entity; one due to ServiceAppointment record and other through its associated event.
Therefore, as a prerequisite before deploying this trigger we should turn OFF “Aggregate Resource Use” in Salesforce Scheduler settings if “Event management” is turned ON.

Pseudocode

Let us first discuss the pseudocode for trigger when new Event records are created.

  • Create a trigger which runs after Event records are inserted. This trigger should run in global context.
  • Loop through individual event records: For each evt in Inserted Events:
    • Is evt != Recurrence Pattern? (I am not handling recurring events as part of this PoC)
      • Yes →
        • Does evt belong to a User who is also a Service Resource (SR)?
          • Yes →
            • Calculate Event Date (D1)
            • Does AppointmentScheduleAggr record (ASA) for SR & D1 exist?
              • Yes →
                • Get ASA.Id
              • No →
                • Create ASA record for SR & D1 combination & return ASA.Id
            • Calculate Event Duration (d)
            • Create AppointementScheduleLog record and insert it

Demo Code

Disclaimer: The following code is meant to be verbose and easily understandable from a Salesforce Developer perspective. Given a choice between performance vs readability I have strived for the latter. It is a proof of concept to demonstrate the feature and should be modified and tested thoroughly as per different data shapes and existing code in the org.

Trigger:

/**
* Created by Ankit Srivastava.
*/

trigger EventUtilization on Event (after insert, after update, after delete, after undelete) {
if (Trigger.isInsert) {
List<Event> events = Trigger.new;
//Insert records in AppointmentScheduleLog and AppointmentScheduleAggr entities for all newly created Events
EventUtilizationUtil.processInsertEvents(events);
} else if (Trigger.isUpdate) {
List<Event> previousEvents = Trigger.old;
List<Event> updatedEvents = Trigger.new;
//Modify records in AppointmentScheduleLog and AppointmentScheduleAggr entities for all updated Events
EventUtilizationUtil.processUpdateEvents(previousEvents, updatedEvents);
} else if (Trigger.isDelete) {
List<Event> deletedEvents = Trigger.old;
//Delete records from AppointmentScheduleLog entity for all deleted events
EventUtilizationUtil.processDeleteEvents(deletedEvents);
} else if (Trigger.isUndelete) {
List<Event> undeletedEvents = Trigger.new;
//Insert records in AppointmentScheduleLog and AppointmentScheduleAggr entity for all Events removed
//from Recycle Bin
EventUtilizationUtil.processUndeleteEvents(undeletedEvents);
}
}

Helper Class:

/**
* Created by Ankit Srivastava
*/

public without sharing class EventUtilizationUtil {
public static void processInsertEvents(List<Event> events) {
//I am not handling recurring events as part of this PoC code
List<Event> nonRecurringEvents = removeRecurringEvents(events);

//Remove events that meet following condition:
// Events that are for any User who is not a Service Resource
Map<Event, ServiceResource> relevantEvents = filterNonRelevantEvents(nonRecurringEvents);

List<AppointmentScheduleLog> appointmentScheduleLogs = new List<AppointmentScheduleLog>();
for (Event evt : relevantEvents.keySet()) {
Date eventDate = getEventDate(evt.StartDateTime, evt.ActivityDate, evt.IsAllDayEvent);
ServiceResource sr = relevantEvents.get(evt);

//Get existing AppointmentScheduleAggr record for current event's
//Service Resource and Date. If no such record exists then create one
//This method call is doing SOQL calls & DML in a loop, this is for
//ease of understanding & will be less performant than Bulk code
AppointmentScheduleAggr asa = getAppointmentScheduleAggrRecord(sr, eventDate);

AppointmentScheduleLog asl = new AppointmentScheduleLog();
asl.ServiceResourceId = sr.Id;
asl.RelatedRecordId = evt.Id;
asl.AppointmentDate = eventDate;
asl.UsageType = 'LightningScheduler';
asl.IsUsedForResourceUtilization = true;
asl.AppointmentScheduleAggrId = asa.Id;
asl.ResourceUtilization = getEventDuration(evt.DurationInMinutes, evt.StartDateTime, evt.EndDateTime, evt.IsAllDayEvent);

appointmentScheduleLogs.add(asl);
}
//Insert all AppointmentScheduleLog record.
//Rollup to AppointmentScheduleAggr will happen automatically
insert appointmentScheduleLogs;
}

public static void processUpdateEvents(List<Event> previousEvents, List<Event> updatedEvents) {
//TODO
}

public static void processDeleteEvents(List<Event> deletedEvents) {
//TODO
}

public static void processUndeleteEvents(List<Event> undeletedEvents) {
//TODO
}

/*
This method is used to remove Events without attached user record who is also a service resource
*/
private static Map<Event, ServiceResource> filterNonRelevantEvents(List<Event> events) {
Map<Event, ServiceResource> resp = new Map<Event, ServiceResource>();
if (events != null && events.size() > 0) {
Set<String> ownerIds = new Set<String>();
for (Event event : events) {
ownerIds.add(event.OwnerId);
}
ServiceResource[] serviceResources = [SELECT Id, RelatedRecordId FROM ServiceResource WHERE RelatedRecord.Id IN :ownerIds AND isActive = TRUE];

for (Event event : events) {
for (ServiceResource sr : serviceResources) {
if (sr.RelatedRecordId == event.OwnerId) {
resp.put(event, sr);
break;
}
}
}
}

return resp;
}

private static List<Event> removeRecurringEvents(List<Event> events) {
List<Event> filteredEvents = new List<Event>();
for (Event e : events) {
if (!isRecurringEvent(e)) {
filteredEvents.add(e);
}
}
return filteredEvents;
}

private static Boolean isRecurringEvent(Event evt) {
/*
https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_event.htm
*/
return evt.IsRecurrence == true || evt.IsRecurrence2 == true;
}

private static Date getEventDate(Datetime startDateTime, Date ActivityDate, Boolean isAllDayEvent) {
/*
https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_event.htm
*/
if (isAllDayEvent) {
if (startDateTime == null) {
return ActivityDate;
}
}

return startDateTime.date();
}

private static AppointmentScheduleAggr getAppointmentScheduleAggrRecord(ServiceResource sr, Date appointmentDate) {
AppointmentScheduleAggr[] asa = [SELECT Id FROM AppointmentScheduleAggr WHERE ServiceResource.Id = :sr.Id AND AppointmentDate = :appointmentDate];

if (asa != null && asa.size() > 0) {
//Return existing AppointmentScheduleAggr record
return asa[0];
}

AppointmentScheduleAggr asaToInsert = new AppointmentScheduleAggr();
asaToInsert.AppointmentDate = appointmentDate;
asaToInsert.ServiceResourceId = sr.Id;
insert asaToInsert;

//Return newly created AppointmentScheduleAggr record
return asaToInsert;
}

private static Integer getEventDuration(Integer durationInMinutes, Datetime startDateTime, Datetime endDatetime, Boolean isAllDayEvent) {
/*
https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_event.htm
*/
if (isAllDayEvent) {
return 1440;
}

if (durationInMinutes != null) {
return durationInMinutes;
}

return Integer.valueOf((endDatetime.getTime() / 1000 / 60) - (startDateTime.getTime() / 1000 / 60));
}
}

The above code is for handling creation of Event records. Similar code can be added in processUpdateEvents(), processDeleteEvents & processUndeleteEvents() methods to handle complete CRUD of Event entity.

The source code is also present at https://github.com/anardana/salesforce-scheduler–appointment-distribution-events-integration

P.S. We can write similar logic for including ResourceAbsence in utilisation for Appointment Distribution.

From Apeksh Dave: Setting up Webex Meetings with Salesforce Scheduler

  • Use Case
    • Company ABC LLC wants to have a self-service tool to allow the customer to schedule an appointment in person or via virtual conference (Voice/Video) to have a more interactive experience
      • COVID crisis has amplified the need to do video conferencing
    • Company ABC LLC wants to have the flexibility to send a notification to customer about the appointment
      • if Virtual appointment send virtual conferencing links
    • Company ABC LLC wants the flexibility to allow to add Cohost for a virtual conference or add additional invite on the customer side (eg: Spouse/friend) or employee side (Specialist, Product manager, etc) on a virtual conference – This is optional

  • Technology Considered
    • Salesforce Scheduler
    • Webex Virtual Conferencing when a customer chooses to do Virtual Conference ( Voice/Video)
      • Leveraging Webex API v1

  • Personas
    • Customer scheduling meeting ( in person /Virtually) and Customer’s Addl. invitee
    • Employee as Host/CoHost
    • Employee as additional Attendee

  • User Flow
    • Client/Company is leveraging Salesforce Scheduler to schedule appointments in person or virtual from available spots
    • Clients’ Customer clicks on Schedule an Appointment
    • He inputs his basic information (name, email and phone) and selects the in-person or virtual option
    • He picks up time and date and clicks next
      • For a virtual appointment, SF will send WEBEX API call with Oauth token, meeting invite details ( title, day, time, meetingpassord etc) along with Customer/invitee information (name and email)
      • Webex will create a meeting and send API response with a Webex link and behind the scene notify the Customer and host ( For host it leverages email tied to user associated for authentiication )
        • optionally add multiple regular invitee/cohost invitee during creation
      • TechnicalComponent Flow
        • Salesforce Lightning scheduler Flow → Apex → Webex API (Oauth Flow ). .
        • Salesforce- Webex OpenID/Oauth integration is leveraged for Authentication/Authorization purpose

  • Deep Dive and Develop Steps
    • Step 1: Setup Salesforce Org and Webex Account
    • Step 2: Setup Oauth in Webex and Salesforce
      • Step 2a: Verify Webex Oauth Setup via Postman Webex Collection
    • Step 3: Setup/Verify Oauth with Salesforce
    • Step 4: Invoke Webex API…Invoke Webex API vai Apex/Named Cred

  • Optional steps outside of Scheduler
    • Optional – SF will make Webex API call outside of scheduler with employee information as Co host/invitee or it can make Webex API call to add additional invitee on the customer side or employee side without being co-host
    • Edit Existing Webex invite by Adding Specialist banker as Cohost
  • How can i troubleshoot with Postman

  • Step 1 -Pre Requisite
    • Get Salesforce Org where you can run flows
    • Email id ( preferably new one so it does not have webex account on this from your corporation side )
    • Webex Developer Account to Create meetings
      • How to get this
        • Send email to devsupport@webex.com to create Trial Account with License with your Name and email address
        • Account userid( your email id ) and steps to setup password will be sent to your Email id
        • TIPS
          • If you just go to webex and create webex account it will NOT work
          • if you have Corp userid/pwd with Webex it may not work
    • Verify your userid and pwd works
      • Go to https://developer.webex.com/docs/api/v1/meetings/create-a-meeting
      • Top right corner .. Login with your Email and pwd
        • Use Bearer Token generated toggle button ( BTW u can use this in Postman if u want )
        • Title – Sample Meeting
        • Password – SFSchFlow123
        • Start – 2020-06-25T11:00:00-04:00 ( Use this format )
        • End – 2020-06-25T12:00:00-04:00. (Use this format)
        • Invitee – {“email”: “XXXXX@XXXXXX.com”,”displayName”: “Apeksh Dave”,”coHost”: false}
      • Click Run
      • Response
        • it will create meeting with webex URL
        • It will send meeting invite
      • Common errors
        • Unauthorized – Token expired – Re login
        • Bad request. – Most probably u did not put Start and end properly or invitee format was messed up
        • Start time is after end time ( vice versa)

  • Step 2a -Verify Webex Oauth Setup via Postman Webex Collection -OPTIONAL
    • Run in Postman
      • Environment variables are already set ( {{MEETINGS_API_URL}} , “{{TIMEZONE_STRING}}”
    • Go to Create non recurring meetings
      • Tab – Authorization
        • Pick Oauth 2.0 .. Get New Access Token
        • Add Authorization headers. – Request Headers
          • Populate following

  • Step 3 – Setup/Verify Oauth with Salesforce
    • Setup Auth provider in salesforce
      • Setup – Auth provider
        • Create OpenID Connect
  • Make sure the Callback url salesforce exactly in Webex integration Redirect URI ( Single space can be problem)
    • Setup Named Credential and Remote settings using Authprovider
      • Create new and populate accordingly
        • URL https://webexapis.com/v1/meetings
        • Id type -Named principal
        • Auth protocol. – Oauth. 2.0
        • Scope spark:kms meeting:schedules_write
        • click on Checkbox – Start auth on Save
image.png
image.png
  • When you Save it
        • it will invoke USERid and pwd for the Webex Dev account you create
          • if successfull
            • it will say Authorized
        • Common Errors
          • Endpoint uri mismatch
            • Salesforce Callback url MUST match Webex Redirect URL. in dev account
          • if Userid screen comes up but says No Oauth Generated
            • Salesforce Auth provider -Uncheck –Send client credentials in header

  • Step 4 -Invoke Webex API vai Apex/Named Cred
    • Apex Code with @InvocableMethod for flows (Sample Apex code – Not Prod ready)
      • GetWebexMeetingURLv1
      • global class GetWebexMeetingURLv1 {
        @InvocableMethod(label='Get Webex Meeting URL v1 Sample flow' description='Returns Unique
        URL v1 Sample flow')
        global static List<String> makeApiCallout(List<List<String>> inputwebexParms) {
        Http httpProtocol = new Http();
        HttpRequest request = new HttpRequest();
        system.debug('Array size =' + inputwebexParms.get(0).size());
        String inTitle = '"' + inputwebexParms.get(0).get(0) + '"';
        system.debug('inTitle =' + inTitle);
        String inAgenda = '"' + inputwebexParms.get(0).get(0) + '"';
        system.debug('inAgenda =' + inAgenda);
        String inWebexPwd = '"' + inputwebexParms.get(0).get(1) + '"';
        system.debug('inWebexPwd =' + inWebexPwd);
        String inStart = '"' + inputwebexParms.get(0).get(2) + '"';
        system.debug('inStart =' + inStart);
        String inEnd = '"' + inputwebexParms.get(0).get(3) + '"';
        system.debug('inEnd =' + inEnd);
        String inInviteeEmail = '"' + '"';
        String inInviteeName = '"' + '"';
        if (inputwebexParms.get(0).size() == 6)
        {
        inInviteeEmail = '"' + inputwebexParms.get(0).get(4) + '"';
        system.debug('inInviteeEmail =' + inInviteeEmail);
        inInviteeName = '"' + inputwebexParms.get(0).get(5) + '"';
        system.debug('inInviteeName =' + inInviteeName);
        }
        String reqInviteeString = '"invitees": [{"email": ' + inInviteeEmail + ',"displayName": ' + inInviteeName+ ',"coHost": true}]';
        system.debug('reqInviteeString =' + reqInviteeString);

        String endpoint = 'callout:XXXXXWebex';
        request.setEndPoint(endpoint);
        request.setMethod('POST');
        request.setHeader('Content-Type', 'application/json');
        String reqHTTPString = '';
        if (inInviteeEmail.length() > 4 )
        reqHTTPString = '{"title": '+ inTitle + ',"agenda": ' + inAgenda + ',"password": ' + inWebexPwd +
        ',"start": ' + inStart + ',"end": ' + inEnd + ',"enabledAutoRecordMeeting":
        false,"allowAnyUserToBeCoHost": true,' + reqInviteeString + '}';
        else
        reqHTTPString = '{"title": '+ inTitle + ',"agenda": ' + inAgenda + ',"password": ' + inWebexPwd +
        ',"start": ' + inStart + ',"end": ' + inEnd + ',"enabledAutoRecordMeeting":
        false,"allowAnyUserToBeCoHost": true}';

        System.debug(reqHTTPString);
        request.setBody(reqHTTPString);
        HttpResponse response = httpProtocol.send(request);
        System.debug(response.getBody());
        JSONParser parser = JSON.createParser(response.getBody());
        String webLink;
        webLink = 'WebexNotSetup';
        while (parser.nextToken() != null) {
        if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
        (parser.getText() == 'id')) {
        parser.nextToken();
        webLink = parser.getText();
        system.debug('webLink =' + webLink);
        }
        }
        return new List<String>{webLink};
        }
        }

  • Flow invokes code (Will get DX info )
image.png

  • Salesforce Salesforce Scheduler calls this subflow
image.png

  • How can i Edit Webex Invite – Add Specialist banker as Cohost
  • Add Specialist banker as Cohost AFTER webex meeting is scheduled
image.png
  • Create FLOW and call Apex code
    • Leverage MeetingID you got in
    • Leverage new named credential and send the request as below
      • inInviteeEmail, inInviteeName, inMeetingID with cohost=true
    • Populate ‘{“email”: ‘ + inInviteeEmail + ‘,”displayName”: ‘ + inInviteeName + ‘,”meetingId”: ‘ + inMeetingID + ‘,”coHost”: true}’;

How to setup Postman with Webex for Troubleshooting – Optional

image.png
  • Headers
image.png

  • Body
    • {
      "title": "Sample Title",
      "agenda": "Sample Agenda",
      "password": "P@ssword123",
      "start": "{{_start_time}}",
      "end": "{{_end_time}}",
      "timezone": "{{TIMEZONE_STRING}}",
      "enabledAutoRecordMeeting": false,
      "allowAnyUserToBeCoHost": false,
      "invitees": [
      {
      "email": "xxxxxx@xxxxx.com",
      "displayName": "xxxxxx Dave",
      "coHost": false
      }
      ]
      }
  • Response
    • {
      "id": "eXXXXXX571a84047da9662725e4fXXXXXXX",
      "meetingNumber": "1463039999",
      "title": "Sample Title",
      "agenda": "Sample Agenda",
      "password": "P@ssword123",
      "meetingType": "meetingSeries",
      "state": "active",
      "timezone": "America/New_York",
      "start": "2020-06-24T12:00:00-04:00",
      "end": "2020-06-24T12:30:00-04:00",
      "hostUserId": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS83Mjc1MjdhNi1iOWE3LTQ5NjYtOTc3YS1hMmFkMmNiZmQyNTA",
      "hostDisplayName": "XXX XXXX",
      "hostEmail": "xxxxxxxx@gmail.com",
      "hostKey": "708678",
      "webLink": "https://xxxxxxxx.webex.com/xxxxxxdemo/j.php?MTID=m18676e904aa243acec205df2248a68fd",
      "sipAddress": "1463999999@xxxxxxxxxxxx.webex.com",
      "dialInIpAddress": "999.99.2.68",
      "enabledAutoRecordMeeting": false,
      "allowAnyUserToBeCoHost": false,
      "telephony": {
      "accessCode": "1463037896",
      "callInNumbers": [
      {
      "label": "US Toll",
      "callInNumber": "+1-999-999-9999",
      "tollType": "toll"
      }
      ],
      "links": [
      {
      "rel": "globalCallinNumbers",
      "href": "/v1/meetings/e43d0571a84047da9662725e4f354ae5/globalCallinNumbers",
      "method": "GET"
      }
      ]
      }
      }
  • Verify with Salesforce Code with Bearer token for Troubleshooting
    • Create Named Credential with no authentication ( as we are passing bearer token )
    • Create Apex class with Bearer Token

Considerations

  1. This blog has been tested with WebEx API version v1
  2. This blog is for a specific scenario where we don’t persist the webex links within Salesforce

References
https://developer.webex.com/blog/real-world-walkthrough-of-building-an-oauth-webex-integration

From: Saikiran Gogurla: Spring 21 Feature: Preset the Territory’s Timezone on the timeslot screen

The default time zone for all times shown in a Salesforce Scheduler flow would be the timezone set for the user scheduling the appointment. See more details on how this would work for the various flows here.

In Spring 21 feature, a feature provided an ability to (pre)set the timezone across multiple screens on the Outbound, Inbound & Guest User flows (Create & modify) across mobile & desktop.

One common scenario this helps you to cover is, when a Customer care agent is booking an “On-behalf” appointment and wants to see the time-slots in the timezone of their selected territory.

Now, this is easy to achieve with Clicks, not code!


Imagine a customer care agent based in Chicago (Central time) is booking an appointment for a customer who wants to meet a banker in the Denver branch (Mountain time) of Cumulus bank.

Wouldn’t the customer care agent wants to see the time slots in Mountain time automatically and book the appointment?


With Salesforce Scheduler, now the agent can easily look at the available time slots in the bank operating hours timezone by default and book the appointments. Don’t worry about the Timezone in which the territory is operating and all; it is taken care of!!
All the timings shown in my flow are configured to Cumulus Bank (Denver) operating timezone.

Question: What if some other bank is at a different location or any Service Territory for that matter? Will my flow behave like it is configured to that Service Territory dynamically?

Answer: Oh Yes!!

Let’s look at how it is done,

Configuration is the same for Outbound, Inbound, and Guest flows, illustrating for Outbound Flow.

  1. Add a new Assignment Element called Set Territory Timezone, after the Location Screen.
image
  1. Set the DefaultTimeZone variable to Timezone from ServiceTerritory Operating Hours.
Config3.png
  1. Save the flow.

— Now the flow is configured to the timezone based on the Service Territory chosen —

Screens that have an impact :

  1. Candidate Screen now shows Next Availability in the Service Territory Timezone.
Candidate Screen.png
Debug Item.png
  1. Flow Time Slot Screen, now by default shows Time Slots in Territories Timezone.
Time Slot Screen.png
  1. Flow Review Screen shows the selected time slot in the timezone in which Territory Operates.
Review Screen2.png
  1. For Inbound and Guest Flow., Next Availability and Time Slots are configured to Territory Timezone on Resource Time Slot Screen, and it looks like,
Screenshot 2021-06-09 at 5.11.57 PM.png

These are some of the use-cases that can be solved but not limited to,

  • Agents booking on-behalf appointments, want to see all time-slots in territory timezone by default.
  • Branch Managers and above, no matter from where customer books an appointment, he/she visits our site and tries to book appointment and all time-sots should be shown in my Territory supported Timezone by default.
  • Guest appointment booking, are you sending out an email to customer to book an appointment using the link and wants him to see time-slots in your territory timezone?

Your customer will never miss his/her appointment time, when booked with Territory timezone!

From Shantinath Patil: Email signature booking

Ever wondered if you can offer a link to your customers to book an appointment with you? Or a situation where you want everything to be preselected, and you will immediately jump to the timeslot screen? Well, wait no more! In this blog, we will show you how you can achieve this!

Let’s consider the first scenario. You give a link to a customer OR add a link to your email signature. Here is what you have to do.

Part 1: Prepare!

We need to build a flow such that our landing page should be the timeslot screen. To make that work, we should be able to populate all the required values. In current out of the box flows, all the screens we traverse are used to populate values required for the timeslot screen. Let’s see what all values we need to populate.

  • Work Type Group: This is the outcome from the OOB Work Type Group Selection page. This helps the internal logic to get which topic/template we are trying to book.
  • Service Territory: This is the location of your appointment.
  • Service Resource: This is the resource that will cater to the appointment request.

Since these values are needed as inputs for our flow, we can create flow variables and mark them available for input.

image.png

Once all these flow variables are created, we can create the assignment for those. Note that we need to assign Service Territory Id to a Service Appointment instance. This makes sense if you look at location screen output. That screen is giving selected territory output as “{!ServiceAppointment.ServiceTerritoryId}”. And since we cannot just set one field on the Service Appointment instance, we will have to initialise all other fields as an empty string. For this, we can refer to the OOB assignment stage.

image.png

The next step is to link all required values to the timeslot screen. Again, we can refer to the OOB timeslot screen configuration. We should be doing the only change to pass our input flow variables for Service Resource and Work Type Group.

image.png

After that, configure the review screen. Make sure you map all the required attributes.

image.png

And finally, we should configure the save action. Here you can pass “{!serviceAppointmentFields}” as input, which is an output of the Review Screen. For now, we are considering invoking this flow for guest flow, so we are adding Lead as input as well.

image.png

Optionally you can configure the confirmation screen. Done! Your flow should be ready to be tested. Save and debug the flow by providing the input attributes!

image.png

Part 2: Distribute!

In a nutshell, we created a flow that takes all the parameters needed for the TimeSlot screen and process it. Once such a flow is ready, we can then use any mechanism of distribution of the flow.

Here is the link to Salesforce documentation which explains to you how can we create a flow page in the community and pass values: https://help.salesforce.com/articleView?id=sf.flow_distribute_internal_url_variable.htm&type=5

This is not the only way! You can even embed the flow in a lightning component and embed that to an external website. More detail: https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/components_using_flow_inputs_set.htm

If you take the approach of exposing this flow in a public community, consider adding a ReCaptcha. More details: https://unofficialsf.com/protect-a-flow-on-a-public-community-with-the-google-recaptcha-component/

The below demo shows all things in action. You can see that a service resource has sent details to a prospect related to Salesforce Scheduler. That email includes a link. Once the user clicks on the link, they will get redirected to our public community page, which hosts the flow we created from the above steps. All the user have to do it to select a slot, add their details and submit!

Part 3: One More Thing!

Now that we have created a flow that takes input, we surely don’t expect our Service Resources to generate the link manually! Besides, it’s easier to use existing components to build a flow that will give output as an URL to proudly put in their email signature or give to customers! Heck, they can even include that in their digital business cards!

To do this, we will reuse existing OOB components. It’s like the Service Resource will perform initial actions on behalf in OOB flow on behalf of the end-user. Following are the steps to create such a flow:

  1. We will configure the “Generate Signature URL” button on the Service Resource page. From there, one can initiate the signature generation flow. Hence we need “recordId” as an input flow variable.
  2. We will pass this record id to the existing WorkTypeGroup selection page. This will ensure that only the WTGs which are related to that resource are displayed.
  3. Next, configure the Service Territory screen. Here we need Service Resource Id and WTG Id, which will be the output of the previous screen. Having both the inputs will ensure that search results for the territory will give us the correct territory.
  4. Finally, we will generate the URL from all the selection with the help of a formula flow variable. The formula looks like this:
image.png

Note that we have considered the scenario that actual booking flow is configured in a community, hence the “communityURL” variable.

Once you build out the flow, it looks like this:

image.png

Test this flow in the flow debug, and once done, activate it. You can configure a new quick action on Service Resource and add that to its layout.

Both the flows are added to this git repo:https://github.com/snathpatil/signatureflow. You can deploy it to your salesforce org and validate!

From Shantinath Patil: Pre-filling values on the Appointment Review Screen

So you want to preload some values on the final Review screen provided by Scheduler?

Check this out…


Let’s say you want to prefill the email & phone details.

  1. Set the Email and Phone in the first assignment stage:
image (22).png

2. Assign Service Appointment record variable in the review screen. In an Outbound flow template that input is blank and that’s why making changes only mentioned in step 1 will not work.

image (23).png

That’s it!

image (24).png

Note this method works for the Outbound flow & Inbound flow and is restricted to the following fields:

  • “AdditionalInformation”
  • “AppointmentType”
  • “Comments”
  • “ParentRecordId”
  • “ServiceTerritoryId”
  • “Street”
  • “City”
  • “State”
  • “Country”
  • “PostalCode”
  • “SchedStartTime”
  • “SchedEndTime”
  • “WorkTypeId”
  • “Id”
  • “Description”
  • “Subject”
  • “Phone”
  • “Email”
  • “ContactId”
  • “IsAnonymousBooking”

To prefill other fields or for other flows (eg Guest Flow), you need to do it build your own screen & logic: https://unofficialsf.com/build-your-own-appointment-review-screen/

Also check out other cool changes that you could with the review screen here: Review Appointment screen: Quick Flow Customisations – Salesforce Scheduler

Summer 21 Feature: Specify the Hours that a Resource can work on a particular Topic

Below is how Operating Hours or Working hours for a Service Resource are determined in Scheduler today.

As you can see the Scheduler’s algorithm considers the intersection of operating hours set at various levels. The Operating Hours set at Service Territory is mandatory but at other levels is optional and an intersection is considered only if the respective operating hour has been added. Typically customers use the Operating Hours set at the Service Territory & Service Territory Member level. Read more about it here

In Summer 21, a feature provides setting up these working hours (or Operating hours are a more granular level.) It allows you to define the Operating Hours that a Service Territory Member can work for a Work Type Group. This helps solve the following needs.

So how does it work?

Now lets take an example of a scenario I would like to model and how the setup would work in Salesforce Scheduler

Scenario

The Market St Branch, San Francisco (Service Territory) is open from 9am to 5pm on Monday to Friday. All appointments are for 60 mins (for simplicity.)

Misha Clayton working as a banker (Service Resource) in this branch has the skills to manage the following topics (Work Type Groups): Investment Banking, Business Banking & General Banking.

Below is her schedule

  • Monday
    • General Banking & Business Banking topics from 10am to 12pm
    • General Banking & Investment Banking topics from 1pm to 3pm
  • Tuesday
    • Business Banking & Investment Banking topic from 10am to 4pm
    • No General Banking appointments
  • Wednesday
    • All topics from 9am to 5pm
  • Thursday
    • Only General Banking from 9am to 5pm
  • Friday
    • Banker does not take any customer appointments on any topics

Setup

First we will setup the Market St Branch working hours by adding the Operating Hours on respective Service Territory

Now we setup Misha’s schedule for the Market St Branch by going into the respective Service Territory Member record and adding the needed Operating Hours

Since Friday doesn’t have any hours setup in this Operating Hours record, Friday hours will not show up for Misha in the Market St Branch.

So lets see what the time slots show up when a customer selects Misha for the Market St Branch for different Work Type Groups

  1. Business Banking

2. General Banking

3. Investment Banking

Similarly, I could have set up Dr John (who has the skills to practice multiple specialities but only practices one speciality in each clinic.)

From Chris Albanese: Summer 21 Feature: Get Resources and Available Time Slots Through New Apex Methods

Salesforce Scheduler introduced a new feature in the Summer ’21 Release which allows developers to easily make custom time slot screen flow components that interact with external systems. The new Apex methods call the Get Appointment Candidates and Get Appointment Slots APIs. This capability helps you easily get all the service resources and available time slots or get available slots for a resource.

With this new feature developers have the choice of using the Scheduler REST APIs and the Scheduler Apex methods. The REST APIs are a powerful tool that allows you to develop custom user interfaces and applications that run on your own websites. The Apex methods offer a great option for developers building custom UI components and processes that are run by users that are already logged into Salesforce, such as running a Lightning Flow launched from a button on the Account Screen or viewing a page inside of a Salesforce Experience site.

Check this out below.

Calling the Scheduler Get Appointment Candidates API with Apex

Here’s a little concept illustrator to show you how easy it is to call the API using Apex.

Let’s say you want to ask Scheduler to return the next time slot available for a Work Type Group and Territory. The user is a call center user, talking to a customer and the customer says “give me your next available appointment please”.

Solution Architecture

  • A simple Flow embedded on the account page
  • The flow calls an @invocableMethod which uses the new API
  • The method returns the 1st time slot found
  • The flow creates a service appointment using the time slot returned

User Experience

The top left of the 2 screen shots below illustrate how the call center user can ask the scheduler to return the next available time slots and create a service appointment.

image.png
image.png

Screen Flow

As you can see in the screen shot below, the flow is pretty simple. It takes in the account id from the Account Page. It prompts the user for Work Type Group, Territory and Earliest Start Time. It passes these parameters into the Apex Action. If a slot is returned, it creates a service appointment, if not, an informational message is displayed.

image.png

Apex Class

As you probably already know, you can call Apex Methods from a Lightning Flow using the Action component. There are lots of great articles and tips on building @invocableMethods, which I won’t go into here. For this example, the method I created takes in the parameters, calls the Scheduler Apex Method named lxscheduler.SchedulerResources.getAppointmentCandidates, and returns the first result found, or returns nothing if no slots are found.

Receiving the Input Parameters from the Flow

In the example code below, to accept the input parameters from the flow, I created a class called payloadIn. It contains variables for Work Type Group Id, Service Territory Id, Scheduling Policy Id, Account Id and Start Date Time. We use these values to call the Scheduler Method. Variables like Account Id are not mandatory, I’ve just included it in my example.

Setting the Input Parameters in the Apex Method

The Get Appointment Candidates API provides an easy to use class to set the parameters that you want to pass to the Get Appointment Candidates API, lxscheduler.GetAppointmentCandidatesInput. As you can see in the example code, I simply set the parameters to the values passed in from the flow. I’ve hard coded the EndTime to be the Start Time plus 3 days. You can of course change this to be more dynamic.

Calling the Apex Method

It is super simple and is represented by this line in the example code:

String response = lxscheduler.SchedulerResources.getAppointmentCandidates(input);

Note, that no authentication code, connected apps, named credentials and other items are required to call the Apex Method. Since the running user is already authenticated, they can call the Scheduler API, just like they can call any other Apex Method they have access to.

Hooray! This is what is so awesome about this new API and makes code based development for Scheduler so much easier for these types of use cases.

Parsing the Results – they’re different from the REST API results

The method returns the results as a JSON string. I won’t go into the ins and outs of JSON and you can certainly find out more with a quick web search, but what you need to know is that the results from the lxscheduler.SchedulerResources.getAppointmentCandidates call are slightly different from the REST API call.

The REST API call for getAppointmentCandidates returns this format:

{
"candidates" : [ {
"endTime" : "2019-01-23T19:15:00.000+0000",
"resources" : [ "0HnB0000000D2DsKAK" ],
"startTime" : "2019-01-23T16:15:00.000+0000",
"territoryId" : "0HhB0000000TO9WKAW"
}, {
"endTime" : "2019-01-23T19:30:00.000+0000",
"resources" : [ "0HnB0000000D2DsKAK" ],
"startTime" : "2019-01-23T16:30:00.000+0000",
"territoryId" : "0HhB0000000TO9WKAW"
}, {
"endTime" : "2019-01-23T19:45:00.000+0000",
"resources" : [ "0HnB0000000D2DsKAK" ],
"startTime" : "2019-01-23T16:45:00.000+0000",
"territoryId" : "0HhB0000000TO9WKAW"
}]
}

The Apex Method for getAppointmentCandidates returns this format:

{{
"endTime" : "2019-01-23T19:15:00.000+0000",
"resources" : [ "0HnB0000000D2DsKAK" ],
"startTime" : "2019-01-23T16:15:00.000+0000",
"territoryId" : "0HhB0000000TO9WKAW"
}, {
"endTime" : "2019-01-23T19:30:00.000+0000",
"resources" : [ "0HnB0000000D2DsKAK" ],
"startTime" : "2019-01-23T16:30:00.000+0000",
"territoryId" : "0HhB0000000TO9WKAW"
}, {
"endTime" : "2019-01-23T19:45:00.000+0000",
"resources" : [ "0HnB0000000D2DsKAK" ],
"startTime" : "2019-01-23T16:45:00.000+0000",
"territoryId" : "0HhB0000000TO9WKAW"
}
}

Did you notice the difference? The outer wrapper for “candidates: [ …]” is missing from the results, so you need to make sure you deserialize the results using the proper format.

Here’s the class I used to deserialize the results:

public class schedulerCandidates {

public datetime startTime;
public datetime endTime;
public List<String> resources;
public String territoryId;


public static list<schedulerCandidates> parse(String json) {
return (list<schedulerCandidates>) System.JSON.deserialize(json, list<schedulerCandidates>.class);
}
}

Example Code

public with sharing class LXScheduleFirstAvailable {

@invocableMethod(label='Schedule to First Available' description='Schedule to whomever is next')
public static list<event> ScheduleIt(list<payloadIn> payloadList ){
if(payloadList == null || payloadList.size() <> 1) return null;
payloadIn pl = payloadList[0];
lxscheduler.GetAppointmentCandidatesInput input = new lxscheduler.GetAppointmentCandidatesInputBuilder()
.setWorkTypeGroupId(pl.workTypeGroupId)
.setTerritoryIds(new List<String>{pl.serviceTerritoryId})
.setStartTime(pl.startDateTime.format('yyyy-MM-dd\'T\'HH:mm:ssZ'))
.setEndTime(pl.startDateTime.addDays(3).format('yyyy-MM-dd\'T\'HH:mm:ssZ'))
.setAccountId(pl.accountId)
.setSchedulingPolicyId(pl.schedulingPolicyId)
.setApiVersion(Double.valueOf('50.0'))
.build();
//call the Apex Method - no REST API authentication or API user needed!!!
String response = lxscheduler.SchedulerResources.getAppointmentCandidates(input);
//parse the results using JSON.deserialize
if(response==null) return null;
list<schedulerCandidates> allslots = schedulerCandidates.parse(response);
//slots found, return just the first one
if(allslots!=null) {
event thisevent = new event();
thisevent.startdatetime = allslots[0].startTime;
thisevent.enddatetime = allslots[0].endTime;
thisevent.description = allslots[0].territoryId;
thisevent.whoid = allslots[0].resources[0];
return new list<event>{thisevent};
}
//no slots found, return null
return null;
}
public class payloadIn{
@invocableVariable(required=true)
public string workTypeGroupId;
@invocableVariable(required=true)
public string serviceTerritoryId;
@invocableVariable(required=true)
public string schedulingPolicyId;
@invocableVariable(required=true)
public string accountId;
@invocableVariable(required=true)
public datetime startDateTime;
}

}

Example Code Test Method

@isTest
private class LXSchedulerFirstAvailableTest {
static testMethod void getAppCandidatesTest() {
String expectedResponse = '[' +
' {' +
' \"startTime\": \"2021-03-18T16:00:00.000+0000\",' +
' \"endTime\": \"2021-03-18T17:00:00.000+0000\",' +
' \"resources\": [' +
' \"0HnRM0000000Fxv0AE\"' +
' ],' +
' \"territoryId\": \"0HhRM0000000G8W0AU\"' +
' },' +
' {' +
' \"startTime\": \"2021-03-18T19:00:00.000+0000\",' +
' \"endTime\": \"2021-03-18T20:00:00.000+0000\",' +
' \"resources\": [' +
' \"0HnRM0000000Fxv0AE\"' +
' ],' +
' \"territoryId\": \"0HhRM0000000G8W0AU\"' +
' }' +
']';
lxscheduler.SchedulerResources.setAppointmentCandidatesMock(expectedResponse);
Test.startTest();
LXScheduleFirstAvailable.payloadIn pl = new LXScheduleFirstAvailable.payloadIn();
pl.workTypeGroupId = '0VSB0000000LB9TOAW';
pl.serviceTerritoryId ='0HhB0000000TrsdKAC';
pl.startDateTime = datetime.now();
pl.accountId = '001B000001KYUM3IAP';
pl.schedulingPolicyId = '0VrB0000000Kz6Z';
list<LXScheduleFirstAvailable.payloadIn> plList = new list<LXScheduleFirstAvailable.payloadIn>();
plList.add(pl);
List<event> candidateList = LXScheduleFirstAvailable.ScheduleIt(plList);
System.assertEquals(1, candidateList.size(), 'Should return only 1 record!');
Test.stopTest();
}
}

Summary

This new Apex API is awesome and simplifies calling the Scheduler API for use cases where the user is already logged into Salesforce.

Additional Notes about this Example

In my example flow, I’m using 2 create record steps to create the Service Appointment and Assigned Resource. In production, you probably want to use the Scheduler flow component called Save Appointment. I’ve described how to use that component here. This component gives you added capabilities to ensure that the time slot selected is still available at the time of record save.

Reading External Calendars

Several of your schedulable resources (Bankers, Advisors, Doctors, Store associates) work out of External calendars like Outlook, Google Calendar or even external systems (EHRs) for their daily work. Their non-customer meetings like “Out for training for 2 hours” or “Out for lunch” are setup in an external calendar. Obviously you would want Scheduler to stop showing up these times slots to your customers.

Below are two ways to achieve this

1. Using a Syncing solution

Here you can use a Syncing solution (In Exchange to Salesforce mode or X2S) which pulls that training appointment setup in the banker’s external calendar from your external calendar (Lets call it Exchange for now) to the Salesforce calendar.

Make sure, you have enabled the Scheduling Policy switch : “Check Salesforce Calendar for Resource Availability.” This ensure that Scheduler will not show any slots that have Salesforce Calendar events at that time. (Only Salesforce Calendar Events that have Show Time As = Free are not considered.)

So how does this work?

Now say your banker has booked 2 hours of training in their exchange calendar. The syncing solution will make an asynchronously call to pull this appointment from Exchange to the Salesforce calendar. When a customer loads the Scheduler interface*, Scheduler will consider existing Service Appointments and also check the events in the Salesforce calendar. Scheduler will then merge all the unavailable times from both these sources and then provide the free time slots. Hence, the 2 hour banker’s training will not show up as a free slot in the Scheduler’s interface*.

Documentation Link: https://help.salesforce.com/articleView?id=sf.ls_read_salesforce_calendar.htm&type=5

Usually syncing solutions gives an end to end solution which can connect to your external calendars via an easy configuration.

Salesforce has a product called Einstein Activity Capture which can do this.

2. Using Apex Interface capability

This helps get on-demand availability by building an apex class which pulls availability from an external calendar.

Obviously, here customers will have to make investment to build & maintain this middleware that reads from their external calendar. This needs significant expertise on the customer side but gives more control of the security model and could give more flexibility in case the customers have a complex external calendar architecture.

There are already partners out there who have built out adaptors to connect to this apex interface. (Check the comments 🙂 )

One thing to keep in mind is that this solution is NOT a replacement for the standard syncing solution. This will exclusively be used with Scheduler & only for service appointments.

So how does it work?

Once you have built this middleware, make sure, you have enabled the Scheduling Policy : “Check External Systems for Resource Availability” and called this interface that you have built. This ensures that Scheduler is checking your external system for availability.

Now say your banker has booked 2 hours of training in their exchange calendar. When a customer loads the Scheduler interface*, Scheduler will consider existing Service Appointments and also get the availability synchronously from this Apex class which you have configured to reach out to your Exchange server and get those unavailable 2 hours. Scheduler will then merge all the unavailable times from both these sources and then provide the free time slots. Hence, the 2 hour banker’s training will not show up as a free slot in the Scheduler’s interface*.

https://resources.help.salesforce.com/images/b151bcec7c33b97037d355b9558e8a86.png

Doc link to get on demand availability (Reading from external calendars): https://help.salesforce.com/articleView?id=ls_read_external_calendar.htm&type=5

* I used Scheduler Interface generically but it actually includes multiple touch points depending on how you have built out your appointment booking experience using the various options provided by Scheduler (See below)

From Chris Albanese: Your Branding on the Salesforce Scheduler Flows

Salesforce Scheduler flows are just flows. You can supplement the screens with any information you want with simple configuration.

image.png

Adding a display field in the same Screen element where the Select Work Type Groups is. The example includes merge fields and an image field.