Posts

From Faith Kindle: Generating an ICS file after Service Appointment Save in Salesforce Scheduler flows

Preface

Salesforce Scheduler provides tools needed to simplify appointment scheduling in Salesforce. With Salesforce Scheduler, it’s easy to embed appointment scheduling into standard Salesforce workflows, such as leads and referrals, opportunities, and accounts.

The outcome of the scheduling process is creation of a Service Appointment record which has the details of the appointment created by the Scheduler engine and informed by input from the end user. Salesforce Scheduler also includes out-of-the-box functionality that will generate a related event on the user’s (Service Resource’s) Salesforce calendar when ‘Event Management’ is enabled in Setup Salesforce Scheduler. Settings. Tools like Einstein Activity Capture can extend Scheduler’s “Event Management” functionality by synching these events to user’s calendar applications like Outlook, Google calendar, etc.

Problem Statement

While Einstein activity capture may be helpful for users using Salesforce, the question most implementation experts ask is – “How can we help prospects and customers scheduling externally to download the appointment confirmation to their personal calendars?”

An example can be with a new prospect interested to meet a financial advisor of a bank going ahead and scheduling an appointment using a contact us page from a company website (where the implementation done might be a guest flow exposed out on the company’s website) wants to download the appointment confirmation to their google calendar.

How do we solve for it?
The answer is with providing a ICS file – after the creation of a service appointment to download.

What is an ICS file?

Internet Engineering Task Force defined a standard in 1998 for sharing calendar events. An ICS file is a calendar file saved in a universal calendar format used by several email and calendar programs, including Microsoft Outlook, Google Calendar, etc. It allows users to share calendar information on the web and over email. Because all popular calendar applications can use ICS files to add event details to a calendar, ICS files are the most common type of files used to exchange calendar information.

ICS files are plain text files that contain information about a calendar event, including the event’s

  • Title
  • Summary and description
  • Start and end times
  • Location
  • Alert trigger

Creating ICS file in Salesforce

While we understand what goes into an ICS file, let’s look at some sample process to create one in Salesforce.

Making an ICS file available to download for Google Calendar, Microsoft Outlook, and Office 365

While there are several ways to make the ICS file available for download, we can make it EASY by making it available for download via link when (or after) we show the Appointment confirmation. In this example we will use flow to generate an ICS URL that, when clicked, will translate to an ICS file for Google Calendar, Microsoft Outlook, and Office 365. (Note: this is just one of many possible approaches. This approach can be be used in conjunction with Salesforce Scheduler’s out-of-the-box Inbound or Outbound new appointment flows, or your custom cloned versions of these flows.)

  • Create custom fields on the Service Appointment object with datatype as URL
    • In our example we created three URL fields with API names Google_ICS__c, Outlook_ICS__c, and Office_365_ICS__c
    • Note: this step is NOT necessary for displaying the URLs on your confirmation page, however may be useful later if, for example, you are sending a confirmation email to your customers and want to include the ability to download the appointment within that email.
  • On the Flow used for appointment booking
    • add Get Record action to retrieve the Service Appointment record after the Save Appointment action is executed
  • with a filter condition to retrieve the Service Appointment created in this flow
  • Add the following Formula variables to your flow. Note the reason for these different formulas is that each format we generate will expect slightly different syntax – for example, Google does not want hyphens or colons in its dates, and Microsoft uses “+” instead of spaces.
    • Note: you can modify these formulas to your business’s preferences, if they do not specifically match. For example, you may not want your subject to be “Service Appointment Subject with Service Resource first name.” In that case, you can remove the “with” and Service Resource first name from the formula to leave just the Service Appointment Subject, for example. Or, you are using a virtual web conference as your meeting place. In that case, you may want to input the web conference link in your description, or modify the text between the URLs’ location parameters in order to set the web conference as your location for your downloadable events.
    • StartDateFormatted
      LEFT(TEXT({!Get_SA.SchedStartTime}),4)+
      MID(TEXT({!Get_SA.SchedStartTime}),6,2)+
      MID(TEXT({!Get_SA.SchedStartTime}),9,2)+
      “T”+
      MID(TEXT({!Get_SA.SchedStartTime}),12,2)+
      MID(TEXT({!Get_SA.SchedStartTime}),15,2)+
      RIGHT(TEXT({!Get_SA.SchedStartTime}),3)
    • EndDateFormatted
      LEFT(TEXT({!Get_SA.SchedEndTime}),4)+
      MID(TEXT({!Get_SA.SchedEndTime}),6,2)+
      MID(TEXT({!Get_SA.SchedEndTime}),9,2)+
      “T”+
      MID(TEXT({!Get_SA.SchedEndTime}),12,2)+
      MID(TEXT({!Get_SA.SchedEndTime}),15,2)+
      RIGHT(TEXT({!Get_SA.SchedEndTime}),3)
    • GoogleICSURL
      https://calendar.google.com/calendar/r/eventedit?text=” & {!Get_SA.Subject} & ” with ” & {!Get_SA.Owner:User.FirstName} & “&details=” & {!Get_SA.Description} & “&location=” & {!Get_SA.Street} & “, ” & {!Get_SA.City} & “, ” & {!Get_SA.State} & “, ” & {!Get_SA.PostalCode} & “&dates=” & {!StartDateFormatted}&”/”&{!EndDateFormatted}
    • OutlookICSURL
      https://outlook.live.com/calendar/0/deeplink/compose?subject=” & {!Get_SA.Subject} & “+with+” & {!Get_SA.Owner:User.FirstName} & “&body=” & Substitute({!Get_SA.Description}, ” “, “+”) & “&location=” & Substitute({!Get_SA.Street}, ” “, “+”) & “+” & Substitute({!Get_SA.City}, ” “, “+”) & “+” & Substitute({!Get_SA.State}, ” “, “+”) & “+” & Substitute({!ServiceAppointment.PostalCode}, ” “, “+”) & “&startdt=” & Substitute(TEXT({!Get_SA.SchedStartTime}), ” “, “T”)& “&enddt=” & Substitute(TEXT({!Get_SA.SchedEndTime}), ” “, “T”)& “&allday=” & “false”
    • Office365URL
      https://outlook.office.com/calendar/0/deeplink/compose?subject=” & {!Get_SA.Subject} & “+with+” & {!Get_SA.Owner:User.FirstName} & “&body=” & Substitute({!Get_SA.Description}, ” “, “+”) & “&location=” & Substitute({!Get_SA.Street}, ” “, “+”) & “+” & Substitute({!Get_SA.City}, ” “, “+”) & “+” & Substitute({!Get_SA.State}, ” “, “+”) & “+” & Substitute({!Get_SA.PostalCode}, ” “, “+”) & “&startdt=” & Substitute(TEXT({!Get_SA.SchedStartTime}), ” “, “T”)& “&enddt=” & Substitute(TEXT({!Get_SA.SchedEndTime}), ” “, “T”)& “&allday=” & “false”
  • Add an Updates Records component to update your Service Appointment with the URLs you generated. Note: like the custom fields storing the URLs we generate, this step is NOT necessary for displaying the URLs on your confirmation page, however may be useful later if, for example, you are sending a confirmation email to your customers and want to include the ability to download the appointment within that email.
  • Display the URL fields on the confirmation page by dragging a Display Text component under the out-of-the-box Service Appointment Confirmation.
    • In the example below, we include a generic “Add to Calendar” graphic and hyperlinked text “Google | Outlook | Office 365.”
    • We use the hyperlink to link Google text to variable {!GoogleICSURL}
    • Similarly, Outlook text is linked to variable {!OutlookICSURL}
    • And Office 365 text is linked to variable {!Office365ICSURL}

Demo

Salesforce Scheduler Customization: Adding an ICS URL for a Clickable Link to Download Appointment

Resources

From Sunil Nandipati: Deploying Salesforce Scheduler from Sandbox to Production Environments

Preface

Customers who have a streamlined development processes typically use multiple non-production environments (one for each probably – development, QA, integration, data migration, SIT and UAT). And it is a general practice that development teams develops in development sandboxes first, check in their code to a repository and adds instructions to deployment trackers – for deployment teams to perform the deployment steps. These can include any manual steps – related to enabling features, adding data etc. Typically anything that cannot be done using the metadata API.

Context

Salesforce Scheduler provides a guided setup experience to setup the product for use. It is NOT a good practice to expect the deployment teams to repeat these steps manually in every single Org. Reasons can be many. To quote a few – prone to human errors while executing manual steps, time consuming, too much detailed documentation will be required and some customers do NOT prefer a partner or consultant to have administrator access to environments other than development.

Guidelines while deploying Scheduler

While deploying Salesforce scheduler between environments – consider categorizing your components into the following areas

Pre deployment activities

Include steps that need to be performed before migrating the codebase using the Metadata API. These can be manual steps for features that are NOT yet supported by the MetaData API (or) any dependent packages that are required for your implementation.

Deploy code using Metadata API

Step 1 – Include feature settings that are related to “Salesforce Scheduler Settings” which need to be either Enabled or Disabled, should be covered first
Note: The settings mentioned in Italic and which do NOT have a Metadata Reference are currently NOT supported by Metadata API – will be addressed in future releases (Safe harbor). If you are using these features – activate them manually as part of your pre-deployment steps

Salesforce Scheduler SettingsMetadata Reference
Event ManagmentenableEventManagementOrgPref
Block Resource AvailabilityenableBlockResourceAvailabilityOrgPref
Multi Attendee EventenableCreateMultiAttendeeEventOrgPref
Appointment Distribution
Aggregate Resource Use
Publish Appointments as Platform Events
Multi Resource SchedulingenableMultiResourceOrgPref
Concurrent SchedulingenableOverbookingOrgPref
Operating Hours for Service Territory Members for Work Type Groups
Salesforce Scheduler for Health Cloud
Resource Appointment Sharing

Step 2 – Include any customizations done to the product offering should be added to the Codebase (repository). Consider evaluating your customizations around

  1. Sharing settings – OWD and Sharing rules (include groups or roles – if sharing rules are around those)
  2. Custom fields on Scheduler objects
  3. New Picklist values on Scheduler objects
  4. Skills
  5. Custom buttons, actions and links
  6. Changes to page layouts
  7. Flows
  8. Custom components that are added to the flows
  9. Custom classes and triggers
  10. Validation Rules
  11. Experience Cloud
  12. Lightning Out for Guest user on a customer website (endpoint URLs will be different)

Post deployment activities

Include steps that need to be performed for the scheduler solution to work. This will mostly include migrating your data and setting up the users and adding scheduler PSLs. Consider leveraging data loader or an ETL tool for data migrations and ensure data integrity (order of loads) and references are maintained post migration between the records in multiple objects.

Salesforce Foundation Objects
SObjectNameLoad SequenceRelationships to Establish
Profile1
UserRole1
PermissionSet1
PermissionSetGroup2PermissionSet
User2Profile, UserRole
Product1
PermissionSetAssignment3PermissionSet Or PermissionSetGroup, User
Salesforce Scheduler CORE Objects
SObjectNameLoad SequenceRelationships to Establish
Skill1Can also be migrated as Metadata (So ensure you are NOT duplicating this step)
SkillRequirement2Skill
Holiday1
Operating Hours1
Operating Hours Holiday2
Work Type Group1
Service Territory2Operating Hours
Work Type2Operating Hours
Work Type Group Member3Work Type Group, Work Type
Service Territory Work Type3Service Territory, Work Type
Service Resource3UserIf using Assets as Service Resource, the order should be after Assets
Service Resource Skill4Service Resource, Skill
Time Slot4Operating Hours, Work Type Group
Service Territory Member4Operating Hours, Service Territory, Service Resource
Appointment Assignment Policy1
Appointment Scheduling Policy2Appointment Assignment Policy
Appointment Topic Time Slot5Work Type Group, Work Type, Time Slot
Salesforce CORE Transactional Objects
SObjectNameLoad SequenceRelationships to Establish
Account3UserOwner of the Account
Parent Account Id should be an additional Step if you manage hierarchy of Accounts
And probably any custom look-ups if you have included Custom Fields
Contact4Account, User
Opportunity5Account, Contact, User
Lead6Account, Contact, Opportunity, UserWhen migrating Converted Leads
Asset5Product, Account, Contact
Salesforce Scheduler Transactional Objects
SObjectNameLoad SequenceRelationships to Establish
Resource Absence4Service Resource
Resource Preference6Service Resource, Related (Account, Lead OR Opportunity)
Shift6Service Resource, Work Type, Appointment Topic TimeSlot
Service Appointment7Service Territory, WorkType, Account, Parent Record (Either Account, Lead OR Opportunity)
Assigned Resource8Service Resource, Service Appointment
Event9Service Appointment, WhatId (Asset, Account OR Opportunity), Owner (Users), WhoID (Lead OR Contact)
Appointment Invitation8Work Type OR Work Type Group, Service Territory
Appointment Invitee9Appointment Invitation, Service Resource

Note – even the scheduling policies and assignment policies have objects (AppointmentSchedulingPolicy and AppointmentAssignmentPolicy) – they can be migrated.

Few best practices to keep in mind

  1. Consider deactivating picklist values that come OOTB if there is NO plan to use them in your solution
  2. If you have hardcoded any IDs in your flows (for defaulting any records like Work Type Group, service territory etc.) – replace with Production IDs wherever appropriate
  3. Turn OFF Rules (Validation, Escalation, Assignment, Workflows, Process builders, Flows, Sharing Rules) to avoid getting errors while performing data loads
  4. Re-calculate Sharing Rules post migration of all data
  5. And Finally explore sfdx-cli data tree import option to load in fewer amount to steps
    1. Make sure the number of records getting loaded are fewer than 200 in a single step (thats the limitation to look for when using the data tree import option with sfdx-cli)
    2. Refer here – https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference_force_data.htm#cli_reference_force_data_tree_import

References

From Sunil Nandipati: Scheduling a Contact

Problem Statement

With Salesforce Scheduler, it’s easy to embed appointment scheduling into standard Salesforce workflows, such as leads and referrals, opportunities, accounts (and person accounts in the B2C Model).

But NOT for Contacts (with B2B model) and Cases.

Though it is synonymous that you want to schedule a Contact when we say scheduling around a case, since the case contact is the person you would prefer interacting with, we will look at options to schedule a contact.

Note: Field Service Lightning Product extends this to additional objects – Work Order, Work Order Line Item and Asset.

Understand Scheduler OOTB Behavior

Before we start looking at an option how we can customize the experience for scheduling around a contact, lets try to understand the core objects we have from Salesforce Scheduler that capture a booked appointment using the OOTB behavior and how we can reuse the existing OOTB available references.

  • Service Appointment
  • Assigned Resources
  • Event

We will keep our focus understanding the relationships and the references

ObjectFieldData TypeUsage
Service AppointmentParentRecordLookup(Account,Opportunity,Lead)Using Salesforce Scheduler we can create a Service Appointment only around Account, Opportunity or Lead.
Service AppointmentAccountLookup(Account)Defaults to the Customer account’s for inbound, Left blank for a Lead and related account if booking around an opportunity.
Service AppointmentContactLookup(Contact)Defaults to the user’s contact record when requesting from a customer community via an inbound authenticated user booking scenario. Left blank otherwise.

We will use this standard field to Stamp the Contact ID – during Service Appointment creation when doing an Outbound Scheduling from Internal Salesforce in this blog.
Assigned ResourceServiceAppointmentMaster-Detail(Service Appointment)Service Appointment object does NOT hold the service resource information, it is primarly stored in the Assigned Resource object. This is the reference to Service Appointment.
Assigned ResourceServiceResourceLookup(Service Resource)Service Resource reference (can be either a user or an asset)
Service ResourceRelatedRecordLookup(User)Service resource object reference to a User – which can be either a Platform Starter / Partner Community / Customer Community Plus / Salesforce license type user
EventWhoIdLookup(Contact,Lead)Events are like your calendar events. The person who will attend this event is captured here.
EventWhatId (Related To)Except Lead and Contact, Other objectsAnd this event is related to what in Salesforce
EventServiceAppointmentLookup(Service Appointment)When Event Management is turned ON on the Scheduler settings, every time a service appointment is created, it creates a related Event – this is to hold that references
EventOwner (Assigned To)Lookup(User,Calendar)Represents the owner of the record (internal user or an external calendar)

Customization Guidance to schedule a Contact

Having looked at the above objects and references, it is pretty much clear that we CAN reuse the existing Contact lookup on Service Appointment. When using Events, we WILL have to make sure the related EVENT gets created with this Contact reference as well.

To achieve this we need to modify the existing template slightly. Try these steps outlined below

  1. Start with a cloned flow from the template (In this example we have cloned the “Outbound New Appointment” template to create a flow called “Outbound New Appointment for Contact”)
  2. Since we plan to launch this from a Contact record, the recordId will be the Contact.Id that will be passed to the Flow. So we need to make sure we capture the Contact.AccountId as well. Create a variable to capture the AccountId. Service Appointment ParentID field can only accept records from Lead, Account and Opportunity, so lets make sure we pass the account information from the Contact
  • Assignment – Set Initial Values :: make sure the recordId is assigned to ServiceAppointment.ContactId
  • Screen – Attendees Screen :: ensure the Contact ID assignment is NOT empty – change it to {!ServiceAppointment.ContactId}
  • Screen – Review Screen :: ensure the Contact ID and Parent Record ID assignments are correct
  • Finally make sure the Event Management is turned ON
  1. Save the Flow and Activate it
  2. Add a Lightning Action to Launch this flow
  • Add this Lighting Action to your Contact Page Layout
  • Finally, test it for yourself. Here is a demo of the same configuration tested on a developer trial org

Considerations when using with Field Service

As mentioned in the preface – Scheduling an appointment gets extended to additional Objects which are NOT supported by Salesforce Scheduler when using Salesforce Field Service product (Earlier Field Service Lightning).

Some considerations to keep in mind when using both products – https://help.salesforce.com/s/articleView?id=sf.ls_limitations.htm&type=5

Especially with both the products (Scheduler and Field Service) in the same org, ensure you take extra care around Event Management using a custom route rather than turning ON the Event Management option as mentioned above.

From Chris Albanese and Chris Hartt – Scheduling 1 appointment with multiple work types

Background

What if your business offers appointment booking for multiple services with a single appointment. For example, you offer auto services and allow customers to select from a menu of options, such as oil change, tire rotation, brake service and others. The customer may select 1 service or multiple services, and depending on the services selected, the duration of the appointment should be long enough enough to accommodate the selected services.

2022-05-14_20-43-34.png

Above: Example of a flow screen prompting a user to select 1 or more auto services

What if you also had many store locations, and each store might offer a different menu of options. For example, the Wakefield store offers all services but the Reading store does not offer brake services.

With Salesforce Scheduler, you can accommodate this out of the box by creating many Work Type Groups to represent the combinations of services.

Consider the following table of Work Type Groups with its limitless number of combinations

ServicesDuration
Oil Change30
Brakes60
Tire Rotation30
Wiper Service15
Oil Change and Brakes90
Brakes and Tire Rotation90
Tire Rotation and Wiper Service45
Oil Change and Wiper Service45
Brakes and Wiper Blades75
…and many more combinations

But, this is probably not the most practical way to offer a menu of services. This document describes an alternative approach which includes 2 custom fields and a small bit of Apex code. Check out the details below.

Solution Overview

This solution allows you to run the Scheduler flows with a few minor configurations and an Apex @invocableMethod, allowing the user to select the desired services and perform precision scheduling without all of the permutations of work type groups.

The solution consists of custom fields on the Work Type object and Service Appointment object, an Apex Class, Work Type Group and Work Type data organized in a specific way and a configured flow. The flow makes use of the FilterByResourceIds parameter to ensure only those resources who have the skills needed are returned.

Objects

Work Type Groups

Create Work Type Groups only to reflect the duration of the services required. For example, Work Type Groups called 30 Minutes, 45 Minutes, 60 Minutes, 90 Minutes, etc. These are tied to Work Types with the respective durations.

Skills

Create Skills for each service required. For example, create an Oil Change Skill, a Brake Service Skill, a Tire Rotation Skill and so on.

Service Resources and Service Territories

Create Service Territories to represent the store locations and create 1 resource for each service lane or bay present in the store. Assign skills to service resources to define the services offered. For example, if the store offers oil changes and brake service, but no other services, then assign only those 2 skills to the service resources associated with the store.

2022-05-14_21-29-27.png

Service Appointment

Add a custom text (255) field called Selected Services. This will store the id’s of the work types that were selected by the user. This can be used by a rescheduling flow (not described in this document).

Work Types

Add a custom picklist field called Service Type, with values of Service and Scheduling.

Create 2 types of Work Types:

Service Work Types

These are work types that represent each service, the skill required and the expected duration. For example,

WT Name = Oil Change, Skill Required = Oil Change, Duration = 30 Minutes, Service Type = Service

WT Name = Brakes, Skill Required = Brake Service, Duration = 60 Minutes, Service Type = Service

These work types are not assigned to any work types groups or service territory work types.

Scheduling Work Types

These are work types that represent a total duration, have no skills required and the expected duration. For example,

WT Name = 30 Minute Service, Duration = 30 Minutes, Service Type = Scheduling

WT Name = 60 Minute Service, Duration = 60 Minutes, Service Type = Scheduling

These work types are assigned to Work Type Groups (see screenshot below) and they are assigned to service territory work types.

2022-05-13_15-59-47.png

Pic above: Work types for Service (with Skills required) and work types for Scheduling (no Skills required)

2022-05-14_20-46-56.png

Pic Above: Work Type Groups just for Scheduling

Apex Class

An Apex class is used to determine the scheduling Work Type Group which is just large enough to cover the list of input Services. It accepts a list of Work Type ids and returns a Work Type Group id and a text field containing a comma separated list of Service Resource ids who have the skills required for the input Services.

The code for the apex class and a test class is contained in the package file at the end of this document.

Flow

Modify the Inbound New Guest Appointment flow to prompt for the services required (service work types), call the apex class and then present the list of time slots available.

2022-05-14_21-47-10.png

Pic Above: Flow with new steps added to prompt for services and call the apex class.

2022-05-14_21-48-19.png

Pic Above: New Screen which prompts for services. Select services is a picklist tied to a record choice set which selects only Work Types where Service Type = Service.

2022-05-14_21-51-21.png

Call to Apex which returns the applicable work type group id with the duration large enough to accommodate the selected services. Also set the FilterByResourceIds field, which will be used in the select location and select time slots screens.

2022-05-14_21-50-38.png

Video of it in action

Check out the short video below.

Package

Try it yourself in your own sandbox. The custom fields, Apex Class, Test Classes and an example of a configured Inbound New Guest Appointment flow are included in this repo.

Github Repository

Notes

FilterbyResourceIds limitation

Since FilterbyResourceIds is limited to 50 service resource ids, you should ensure that you pass in a service territory id to the Apex Method so as not to exceed this limit.

Modify line 24 of the Apex code to select the specific service territory selected by the user.

listST = [select id from serviceterritory where isactive = true];

Lines 50-56 contain code to limit the total number of service resource ids to a maximum of 50. If the limit of 50 changes in a future release, you should change this code to reflect the new limit.

Rescheduling Flow

A rescheduling flow is not included here. If rescheduling is part of your use case, use the Service Appointment custom field created in this package to retrieve the services selected by the user when the appointment was scheduled. Add the custom screen and call to the Apex method to retrieve the corresponding scheduling Work Type Group, similar to how the flow in the package has been configured.

From Brad Shively: Create MS Team Meetings from Salesforce (with Salesforce Scheduler)

Introduction

The use case is fairly straightforward, as a customer of a bank, I want to schedule an online meeting with my advisor and discuss my accounts. The technology the bank wants to use is MS Teams.   

The technology usage behind this particular demo is to simply create a meeting in MS Teams via an API which returns a URL that parties can click on to launch Teams( either browser or desktop ) and have a meeting. There is a lot more that can be done, but this integration is to show the possibilities with the connection

Technology Utilized

Listened below are the main technologies and configurations used in the demo preparation.

Main SaaS Tech Stacks

  • Salesforce Scheduler
  • MS Teams Online Meeting
  • MS Graph Rest API’s

Demo Setup Technology

  • MS Azure Development Account – Needed to setup a MS Teams Environment
  • Salesforce Org 
  • Postman – To easily test the setup and API’s
  • <optional>MS Visual Studio – Used to write and deploy code
  • Apex Classes
    • Invocable Methods setup to make available in Flows
    • Test Classes
  • Auth Provider Setup in Salesforce
  • Named Credentials
  • Salesforce Flows – To provide the screen navigation, call Azure to get the meeting, and update the appropriate records. 

Demo Flow High Level

This section describes the general flow of the demo from start to finish when showing to the customer. This is a specific flow for the customer this demo was prepared for. You can tailor or make your own flow(s) for your customer. 

  • Customer goes to the portal to schedule a meeting, leveraging the Salesforce Scheduler. 
  • This demo has an authenticated flow, there is a person account associated with the demo. 
  • Enter the required information from the screen 
    • Click by previously scheduled service appointments or search for a banker. 
    • Click Next
    • Click on a Work Type Group → Select the type => Click Next
    • Select Video Call
    • Enter a address → San Francisco → Select a branch 
    • Choose a date in scheduler
    • The call to MS Teams is done at this point and a URL is returned to the “Additional Information” Section. Note, this could be put anywhere but for the purpose of the meeting we chose here. 
    • Click on next and a new Service Appointment is created with the Meeting URL in a custom field that can be accessed to launch a teams meeting. 

The integration behind the scene created a MS Team meeting valid link that can be clicked on to start a team meeting. This utilized the MS Graph API, OAuth/Open ID authentication/authorization, and Apex Callout to create and return the meeting. 

Demo Details

This section will describe the setup needed to execute the demo. What is described is the particular flow used to validate and test the integration. The entire section for Postman is completely optional, but it is suggested to do this to validate and debug. In our case, it was invaluable in determining the exact headers to place into the API. In the initial development, Postman was first used outside of Salesforce to ensure the API format was correct and the authentication/auth was setup correctly in Azure.

Microsoft Azure Setup

The first piece that is needed is a Microsoft Developer Account to be able to setup a new Azure environment. We won’t go into the details here on all of the steps, the directions are straightforward. When it gets to the point of asking what features you want to add, make sure you add in the MS Teams option that will be available. 

Setup Salesforce/Postman Application in Azure

The first thing needed is to setup a new custom application in Azure AD that will be used to generate the authentication and authorization needs for Salesforce connections and optionally Postman Connections. You can create two apps, or in my case, I just created one to handle both. 

App Setup

  • Create a new App from the App Registrations link along the left hand side and click on New Registration
  • Fill out the form and leave the defaults as they are. We will fill in multiple redirect URI in a later step. 

API Permissions Setup

  • Click on the API Permissions from the left-hand side of the application you have created. (Click the application from the previous step)
  • Create the API Permissions needed for MS Teams Online Meetings. In addition, add a few extra permissions to ensure the token can be refreshed. The permissions can be found in the Graph API docs. We added a generic one for read and Mail.Read for testing purposes, they are not fully required. Once the permissions are added click on the “Grant Admin Context for … ”. The status will be red until this is done.
    • Mail.Read offline_access OnlineMeetings.ReadWrite OnlineMeetings.ReadWriteAll User.Read

Certificate Setup

In this section we will create the client key and secret which will be used in the Postman and Salesforce integrations to authenticate into Azure and Teams.

  • Click on the Certificates & Secrets Menu item on the left hand side. 
  • Click on New client secret
  • Enter a new description and the expiration of the secret. You can choose whatever you like or keep the default.
  • Copy the Secret Key Value for use later when setting up the authentication sections of Salesforce and Postman
  • The Secret ID is an Azure ID, it will not be used anywhere. The ID that is used is the Application(client) ID from the application you created. We will show this in detail later. 

Authentication Setup

This section covers the Authentication Setup that will be needed. This section will be done when you are ready to setup either Salesforce and/or Postman to Authenticate and setup the redirect URI authorization from the client request. Additionally, the Endpoints needed for authorization and token endpoints locations are covered in this section. If this is the first time through the setup, you most likely won’t have the redirect URI’s available but for document cleanliness, we are keeping the Azure setup all in one section.

  • Click on the Authentication menu item on the left hand side. 
  • Click on Add a Platform link to add a platform. 
  • Click on Web Platform
  • Add the redirect URI from your application( the location of these URI’s from Salesforce and Postman will be shown later ).
  • The image below is an example where the Postman and Salesforce API’s have been added. In this case, 1 Postman and 2 Salesforce Demo Environments are part of this application.
  • The Endpoints needed for the configuration of the applications can be found on the overview section of the application you have configured. Click on the Endpoints link from the overview and the first two endpoints are used when configuring the authentication.

Salesforce Setup 

This section describes the Salesforce Setup needed to access MS Teams. It is strongly advised to setup Postman first to validate and test the API as well as get familiar with the API before jumping into Apex coding setup. This section will consist of Authentication setup, the Named Credential, and the Apex code needed to create a Teams meeting and retrieve a URL. Additionally, this will provide the callback referenced in the Azure section which will be needed to complete the Authentication section in Azure. 

Auth. Provider Setup

The first step is to create the Authorization Provider configuration to connect to Azure. You will need the Secret Key(Value) from the Certificate setup in Azure as well as the application id. 

  • Enter Salesforce Setup→Auth.Providers→New
  • Choose Open ID Connect from the Provider Type drop-down.
  • Name the Auth Provider. 
  • URL Suffix → Can make the same as the name.
  • Consumer Key → This is the Application ID from Microsoft Azure. This is found in the Overview Section of your App in MS Azure.
  • Consumer Secret → This is the Secret Value from the Certificate setup done previously in MS Azure. 
  • Authorization Endpoint URL – This is the OAuth 2.0 Endpoint given in the Azure Setup Above
  • Token Endpoint URL – This is the OAuth 2.0 token endpoint given in the Azure Setup above. 
  • Default Scopes<optional> – We set the scopes in the named credentials but they could also be set here. In this example we just set the online meeting read/write. When setting the scope, the values are delimited with a space.
  •  Make sure all 3 options are checked for Send Access token in Header, Send client credentials in header, and include customer secret in API response.
  • Use the defaults for the remainder.
  •  Save
  • Click on the newly defined Auth Provider to open it up
  • Copy the Callback URL that is shown in the Salesforce Configuration. If you have Experience Cloud setup and you are using those domains, then add those callback URLs as well to the Azure configuration.
  • Take the callback URL you copied above and back in the MS Azure AD setup for your application, add this to the redirect section. Do this by clicking on Add URI. 

Named Credential

Creating a named credential will perform the authentication to MS Azure and do the Oath validation. Here is where the MS Authenticator is used( I set mine to auto approve ) to validate the OAuth connection. It will prompt you to login to your instance of Azure. The login will be the MS Azure login/id that was created during the setup of the developer instance. 

A couple of notes for this setup. The example shown only accesses the one API for creating an online meeting in MS Teams. This could be a general named credential to just the Graph root level and you can append the rest of the API in the code. Alternatively, you can create a named credential for each of the API’s or perhaps the most commonly used ones. 

  • In Setup→Named Credentials click on New
  • Enter the label and the URL of the API Endpoint you wish to access. In this case, the full API is used for the onLineMeeting for Delegated access. The application access has a different signature. 
  • Identify Type will be Named Principal.
  • Auth Protocol will be be OAuth 2.0
  • Select the Auth Provider created in the previous step. 
  • Scope: very important! The scope here will match the scope you created in MS Azure for the application. It is space delimited. 
    • Values: OnlineMeetings.ReadWrite User.Read Mail.Read offline_access
  • Check on Start Auth Flow on Save
  • Generate Auth Header is checked
  • Click Save and this will trigger the actual connection to Azure and validate the Authentication
  • When you Save, the Oauth flow will initiate.
  •  If successful, you will see Authenticated in the Authentication Status in Salesforce. If it fails, you will get a failure screen from MS Azure. 
    • Common Issue: Used the wrong secret key or id. Double check in the Auth. Provider those are correct.
    • URL Endpoint is in valid: Check your URL Endpoint. (one reason to use Postman first)

Salesforce Application Code

This section will review the Apex application code needed to make the Rest API call to set up a teams meeting and retrieve the meeting URL from the Teams Server. The code is not production quality but instead is a sample to prove the concepts. The current iteration does not accommodate error handling in a meaningful way nor does it do much more than create a meeting. The goal of this good is to demonstrate the basic connection for customers and proof of concept the meeting invite is ready. The sample code will be in two parts, the first part is a test class that can be used to validate the connection and result. Once that is working, then the 2nd class uses @InvocableMethod so that it can be used in flows. Obviously this can be tailored however desired. 

This section does not instruct how to set up command line tools, deploy the source, or other development tasks. It is assumed the reader understands how to deploy, run the developer tools/debugger in Salesforce. 

Test Apex Class – Simple Class to Create the Meeting utilizing Named Credentials

public class TestAzure {

    /* Test Method for Unit Testing Connection */

    public static String getMeetingUrl()

    {

        HttpRequest req = new HttpRequest();

        Http http = new Http();

        //Setup the Endpoint and append the name of the file

        req.setEndpoint(‘callout:MS_Azure_OnlineMeeting’);

        req.setMethod(‘POST’);

        req.setHeader(‘Content-Type’,’application/json’);

        req.setHeader(‘Accept’,’*/*’);

        req.setHeader(‘Accept-Encoding’,’gzip, deflate, br’);

        //Setup the JSON Body – in the test just set a subject, can add more through Postman or other tests

        req.setBody(‘{“subject”:”Delegated User Test Meeting”}’);        

        System.debug(‘Body: ‘ + req.getBody());

        System.debug(‘Endpoint Value: ‘+ req.getEndpoint());

        System.debug(‘Request: ‘ + req);

        HTTPResponse res = http.send(req);

        System.debug(‘Response Body: ‘+res.getBody());

        /* Parse Response */

        JSONParser parser = JSON.createParser(res.getBody());

        String webLink;

        webLink = ‘MSTeamsNotSetup’;

        while (parser.nextToken() != null) {

        if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&

        (parser.getText() == ‘joinWebUrl’)) {

            parser.nextToken();

            webLink = parser.getText();

            System.debug(‘joinWebUrl= ‘ + webLink);

            }

        }

        return webLink;

    }

}   

Common Issues/Errors

  • MS Teams will return a 201 in the HTTP Response. This is a success. It is documented in the API
  • HTTP Error 500 – Internal Server Error
    • Notice the headers that are set. The MS Graph documentation does not state it explicitly, but in our testing we found that the 2 accept lines need to be in the header when sending the request. You need to add the gzip, etc and the “*/*” accept lines.
    • 401 – The 401 unauthorized generally means the scope is incorrect in the Named Credential or both in the Named Credential or the Azure setup. Make sure the permissions are correct and the scope is space delimited. 
    • 403 – Forbidden – This occurs when the secret keys are incorrect. You should have fixed this when you saved the name credential so it shouldn’t show up. 
  • Note: When using the Apex Debugger – Look for the debug line to joinWebURL populated with a long string for the meeting. It will start with something like this: 12:40:52:477 USER_DEBUG [37]|DEBUG|joinWebUrl= https://teams.microsoft.com/l/meetup-join/19%3am

Salesforce InvocableMethod Class Utilizing the Apex Code and Callout

This is the actual code used in the demonstration. This code is accessible in the Flow Builder inside of Salesforce as an Apex Action. 

global class GetTeamsMeetingURL {

    @InvocableMethod(label=’Get MS Teams Meeting URL’ description=’Returns a meeting URL For MS Teams’)

    global static List<String> makeApiCallout(List<List<String>> inputTeamsParms)

    {

        // Setup the HTTP Initial Request

        HttpRequest req = new HttpRequest();

        Http http = new Http();

        //Setup the Headers, format the body, and call the MS Graph API

        req.setEndpoint(‘callout:MS_Azure_OnlineMeeting’);

        req.setMethod(‘POST’);

        req.setHeader(‘Content-Type’,’application/json’);

        req.setHeader(‘Accept’,’*/*’);

        req.setHeader(‘Accept-Encoding’,’gzip, deflate, br’);

        /* Setup the Parameters for Meetings, subject, etc. */

        // Note: The initial demo only utilized title, further development can use other inputs.

        system.debug(‘Array size  =’ + inputTeamsParms.get(0).size());  

        String inTitle = ‘”‘ + inputTeamsParms.get(0).get(0) + ‘”‘;

        system.debug(‘inTitle =’ + inTitle);    

        String inAgenda = ‘”‘ + inputTeamsParms.get(0).get(0) + ‘”‘;

        system.debug(‘inAgenda =’ + inAgenda);              

        String inPwd = ‘”‘ + inputTeamsParms.get(0).get(1) + ‘”‘;

        system.debug(‘inPwd =’ + inPwd);                

        String inStart = ‘”‘ + inputTeamsParms.get(0).get(2) + ‘”‘;

        system.debug(‘inStart =’ + inStart);                

        String inEnd = ‘”‘ + inputTeamsParms.get(0).get(3) + ‘”‘;

        system.debug(‘inEnd =’ + inEnd);

        // Setup the Body

        String reqHTTPString  =  ”;

        reqHTTPString = ‘{“subject”:’ + inTitle +’}’;

        req.setBody(reqHTTPString);

        /* Send request to MS Teams Server */

        HTTPResponse res = http.send(req);

        /* Parse Response from MS Team Server */

        JSONParser parser = JSON.createParser(res.getBody());

        String webLink;

        webLink = ‘MSTeamsNotSetup’;

        while (parser.nextToken() != null) {

        if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&

        (parser.getText() == ‘joinWebUrl’)) {

            parser.nextToken();

            webLink = parser.getText();

            System.debug(‘joinWebUrl= ‘ + webLink);

            }

        }

    // Apex Actions Return. The method signature for invocable method requires a List of Strings to be returned.

    return new List<String>{webLink};

    }

}

Salesforce User Experience Setup

This section is optional but describes how the above code can be accessed in the low-code building tools inside of Salesforce. 

This section in a flow shows how this action is called to retrieve a URL for the MS Team Meeting and then assigns it to a Record in Salesforce. A larger flow will then do further processing as part of a full scheduling flow. The takeaway is that the Apex Code above is an option to drag and drop into the low code builder and use it when running a flow. It could also be embedded within a Lightning Web Component(LWC) and used in other places. Lastly, it can always be accessed from another Apex Class to get the required information. 

Postman Setup

This section describes how to setup Postman to access MS Graph API and test the API integration outside of Salesforce in a developer-centric manner. This is completely optional, but oftentimes if it works in Postman and not in another application or Salesforce, you can see what is different in Postman versus the other applications. This tutorial assumes the reader is familiar with Postman and has downloaded the application or is using the web version. This document will use the desktop installation for reference. This section does not require knowledge of the Salesforce Setup. We recommend starting with Postman before the setup in Salesforce. 

Microsoft Link on Postman Setup: https://docs.microsoft.com/en-us/graph/use-postman

Download the MS Graph Postman Collection

To make life easier, download the Postman Collection already created for MS Graph. It does not contain the Teams integration unfortunately, but it does have a big chunk of other API’s that can be used to make sure the authorization is all setup as well as plenty of examples. To do so:

  • Click on Explore on the Menu at the Top of Postman
  • Search for MS Graph Workspace 
    • It will be either MS Graph or MS Graph Fork Workspace
    • You can also find it by clicking on Workspaces and scrolling to it.
  • Create a new Fork from either the Graph or the Fork Graph – Right click on the Microsoft Graph Space to get the Fork Option
  • You should end up with something like this in your My Workspace or wherever you saved the forked Graph Collection to.

What this provides is a whole list of MS Graph APIs broken up into folders. In our testing, we used Delegated to validate with. 

Setup Postman Authentication

Setting up Authentication is straightforward and will require the client id and secret key from the MS Azure setup done previously. Additionally, you will need to add the callback URL to the Authentication section in MS Azure.  You also need to set up Environment variables as part of this step. 

Setup Environment Variables

Postman needs to pass in environment variables to the headers in the API. To set these up, create a new environment to be used with MS Graph. 

  • Click on Environments on the left hand side of Postman and click the Plus sign at the top of the environment list
  • Name your Environment and add in the 3 values needed
    • ClientID – This will be the Application ID of the App you created in MS Azure.
    • Client Secret – This will be the Secret Value created in the Client Secret step of Azure
    • TenantID – This is the tenant id found in the overview of your app. 
  • Now that you have set up the environment variables – make sure to make that environment your active environment. It should show up here. You can access it from the drop down as well.

Setup Authentication

  • Click on the Delegated Folder in the MS Graph Collection on the left hand side of Postman. This will bring up the Authorization Screen. 
    • Selection OAuth2.0 from the drop down.
    • Select Add Auth data to the Request Headers
    • Access Token → Available Tokens ( this will get filled in later )
    • Header Prefix – Bearer
  • Configure New Token Section
    • Token Name – Fill in something for this
    • Grant Type – Authorization Code
    • Leave the rest defaulted. 

3. It will look much like this after filling out. 

  • Set the Callback URL in MS Azure – Note: This step must be done or your auth will fail. Take the Callback URL above with the oauth URL for Postman and add it to the Callback URLs in your application in MS Azure.
  • Get a New Access Token – Use this to debug whether you can get authorized or not and once authorized use the token.
    • Click on the Get New Access Token at the bottom of this screen in Postman on the Authorization screen.
    • This will kick off the OAuth Flow, which will have you login to MS Azure and Authenticate. I used the Microsoft Authenticator download to my phone for MFA purposes which makes this more automatic. 
    • Note: This is where if you have an incorrect setup, you’ll get a bunch of errors. Check your setup against the above, do you have the right token id?
  • You will go through the screens until you get a very long access token. Click on the Use Token button. 

Create and Run the MS Graph Online Meeting API

Although there are several Teams APIs in the Team Folder, they are chat related versus the Online Meeting. We need to create a new API for the OnlineMeeting to be accessed. In this section we will cover creating the new API, adding parameters, and testing the API. 

  • Navigate to the Teams Folder 
  • Click New at the Top( or right click to add a request ) and add a new Web HTTP Request
    • Switch the type from GET to POST
    • Put in the name of the method in the POST
    • On the Body, use the JSON application type.
    • Add the body in JSON format, Subject was added for the example. 
    • Click Send
  • Results are shown below from the meeting when successful.

From Adwait & Mrityunjoy Chowdhury: Reverse Map Service Appointment to the appropriate shift

Shift is one of the major additions made in Salesforce Scheduler, It provides the user with enormous flexibility and makes the entire process of appointment scheduling less cumbersome. The diagram below shows the interaction between Shift and various other entities involved in scheduling a Service Appointment.

Blank diagram2.jpg

A Service Territory Member can have multiple Shifts associated with it and as a result the time slot selected for the Service Appointment can fall between more than one overlapping shifts (considering both the required and primary Service Resources).

In the following sections we will figure out a way to reverse map all the associated Shifts with corresponding Service Appointment.

Creating a custom object

For completing the purpose of reverse mapping Shift with Service Appointment a custom object(Service appointment shift) is used as a junction object. It comprises of the following custom fields:-

  • ServiceAppointnmentId – Referring to a Service Appointment.
  • ShiftId – Referring to a Shift whose startTime and endTime completely encloses the Service Appointment’s time slot.
  • Modified – A boolean flag to help backtrack all the updates made to Service Appointment.Only the records with the flag value false should be considered as reliable entries.
  • Canceled – A boolean flag which is set to true only when the associated Service Appointment is canceled.
Blank diagram.jpeg

As evident from the design proposed whenever a Service Appointment is scheduled we need to obtain all the Shifts associated with the Service Appointment.For each associated Shift, one record comprising of ServiceAppointmentId,ShiftId

and Modified(initially false) must be made.

For achieving our goal of reverse mapping we will be writing triggers on two entities:

  • Service Appointment
  • Assigned Resource

Creating a Service Appointment

We must do the reverse mapping the moment a Service Appointment is created.Whenever a Service Appointment is created necessary inserts are made in Assigned Resource and this will invoke the trigger that we will be writing for Assigned Resource .

Pseudocode

Let us first discuss the pseudocode for triggers when new Service Appointments are created.

  • Create a trigger which runs after inserts are made in Assigned Resource.
  • Fetch all the linked Service Appointments using the just inserted Assigned Resource records.
  • Check for any previous entries for obtained Service Appointments in the custom object
    and for all those records set the Modified as true.
  • Design a map of Service Appointment and all the required resources for that appointment.
  • Loop through all the Service Appointments in the map and obtain the associated Shifts of the Service Resource.
  • Do the inserts for Service Appointment and Shift mapping records.

Updating a Service Appointment

Whenever a Service Appointment is modified we can make changes in the time slot and the required resources of the Service Appointment.Once a Service Appointment is updated we must set the Modified field for all the records of this Service Appointment in the custom object to true and obtain new associated Shifts. Let’s have a look at the possible scenarios and the way we are handling it.

  • Deletion of required SR from Appointment(will invoke an after delete trigger on Assigned Resource)
  • Addition of required SR to Appointment(will invoke an after insert trigger on Assigned Resource)
  • Moving of SR from required to optional resource(will invoke an after update trigger on Assigned Resource)
  • Moving of SR from optional to required resource(will invoke an after update trigger on Assigned Resource)
  • Change in Appointment start time and/or end time
  • Change in Appointment WTG/WT (Appointment duration)
  • Changing the time slot at the same time altering the Assigned Resources.

For these cases we will be using one update and a delete trigger on Assigned Resource wherein we implement a similar logic as mentioned above.

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 getassociatedshifts on AssignedResource (after insert,after update,after delete) {

   /* fetching all the linked service appointments from the trigger */

   List<ServiceAppointment> AllAppointments = new List<ServiceAppointment>();

   if(Trigger.isDelete) {

       List<String> AffectedAppointments = new List<String>();

       For(AssignedResource a:Trigger.old) {

           AffectedAppointments.add(a.ServiceAppointmentId); 

       }

       AllAppointments = [Select Id,ServiceTerritoryId,SchedStartTime,SchedEndTime,WorkTypeId from ServiceAppointment where Id In :AffectedAppointments];

   } else {

       AllAppointments = [Select Id,ServiceTerritoryId,SchedStartTime,SchedEndTime,WorkTypeId from ServiceAppointment where Id In (Select ServiceAppointmentId from AssignedResource where Id In :Trigger.New)];

   }

   /*

     check for any previous entries for obtained service appointments in the custom object

     and for all those records set the modified flag as true

  */

   List<Service_appointment_shift__c> ServiceAppointmentShiftToUpdate =new List<Service_appointment_shift__c>();

   For(Service_appointment_shift__c all:[Select id,modified__c from Service_appointment_shift__c where Service_Appointment__c In :AllAppointments]) {

       all.Modified__c = true;

       ServiceAppointmentShiftToUpdate.add(all);

   }

   update ServiceAppointmentShiftToUpdate;

   /* Making call to helper class for necessary inserts */

   ServiceAppointmentShiftMapping.InsertNewRecords(AllAppointments);

}

Helper class

public class ServiceAppointmentShiftMapping {

   public static void InsertNewRecords(List<ServiceAppointment> AllAppointments) {

       /*creating a map of ServiceAppointmentId and List<ServiceResourceId> */

       Map<Id,List<Id> > ServiceAppointmentToAllServiceResources = new Map<Id,List<Id> >();

       /*for creating the map we need list of required resource and service appointment */

       List<AssignedResource> ServiceResourcetoServiceAppointment =new List<AssignedResource>();

       ServiceResourceToServiceAppointment = [Select ServiceResourceId,ServiceAppointmentId from AssignedResource where ServiceAppointmentId in:AllAppointments

                AND  IsRequiredResource = True];

       for(AssignedResource temp:ServiceResourceToServiceAppointment) {

           /* check if the key already exists or not*/

           if(ServiceAppointmentToAllServiceResources.containsKey(temp.ServiceAppointmentId)) {

               List<Id> ResourceList = ServiceAppointmentToAllServiceResources.get(temp.ServiceAppointmentId);

               ResourceList.add(temp.ServiceResourceId);

               /*insert the key value pair*/

               ServiceAppointmentToAllServiceResources.put(temp.ServiceAppointmentId,ResourceList);

           } else {

               ServiceAppointmentToAllServiceResources.put(temp.ServiceAppointmentId,new List<Id>{temp.ServiceResourceId});

           }

       }

       /*All the new records for custom object will be stored in this*/ 

       List<Service_appointment_shift__c> AllNewEntries =new List<Service_appointment_shift__c>(); 

       For(ServiceAppointment current:AllAppointments) {

           List<Id> ResourceId = new List<Id>();

           /*obtain the list of resources from the map */

           ResourceId = ServiceAppointmentToAllServiceResources.get(current.Id);

           //fetch associated shits with wtg and without wtg

           List<Shift> probableShifts= new List<Shift>();

           probableShifts =[Select Id from Shift where ServiceResourceId IN :ResourceId

                            AND ServiceTerritoryId = :current.ServiceTerritoryId

                            AND StartTime <= :current.SchedStartTime

                            AND EndTime >= :current.SchedEndTime

                            AND Status =’Confirmed’

                            AND WorkTypeGroupId IN (Select WorkTypeGroupId from WorkTypeGroupMember where WorkTypeId =:current.WorkTypeId)

                          ];

           List<Shift> probableShiftsNoWtg= new List<Shift>();

           probableShiftsNoWtg =[Select Id from Shift where ServiceResourceId IN :ResourceId

                                 AND ServiceTerritoryId = :current.ServiceTerritoryId

                                 AND StartTime <= :current.SchedStartTime

                                 AND EndTime >= :current.SchedEndTime

                                 AND Status =’Confirmed’

                                 AND WorkTypeGroupId =”

                            ]; 

           /* add all the records to be inserted in the custom object in a list*/

           for(Shift a:probableShifts) {

               Service_appointment_shift__c next = new Service_appointment_shift__c(Service_Appointment__c = current.Id,Shift__c = a.Id,Modified__c = false);

               AllNewEntries.add(next);

           }

           for(Shift a:probableShiftsNoWtg) {

               Service_appointment_shift__c next = new Service_appointment_shift__c(Service_Appointment__c = current.Id,Shift__c = a.Id,Modified__c = false);

               AllNewEntries.add(next);

           }

        }

        Insert AllNewEntries;

   }

}

  • Service Appointment is canceled

trigger cancelServiceAppointment on ServiceAppointment (after update)

{

   /*

     This trigger will be called whenever a service appointment is update(time slot is

     changed,assigned resources are changed,status is changed etc)

     but the only purpose of this trigger is to handle the case when the service appointment

     is canceled

   */

   /*

      All the Service Appointments that are canceled.

   */

   List<ServiceAppointment> changed = new List<ServiceAppointment>();

   For(Integer i=0;i<Trigger.New.size();++i) {

       ServiceAppointment justUpdated = Trigger.New[i];

       /*

          checking the status of updated Service Appointment

       */

       if(justUpdated.Status == ‘Canceled’) {

           changed.add(justUpdated);

       }

   }

   /*

       Setting the canceled flag and modified flag of the obtained service Appointment in custom object

   */

   List<Service_appointment_shift__c> ServiceAppointmentShiftToUpdate =new List<Service_appointment_shift__c>();

   For(Service_appointment_shift__c all:[Select id,modified__c from Service_appointment_shift__c where Service_Appointment__c In :changed]) {

       all.Canceled__c =true;

       all.Modified__c = true;

       ServiceAppointmentShiftToUpdate.add(all);

   }

   /* updating the records */

   update ServiceAppointmentShiftToUpdate;

}

From Apeksh Dave: Create Zoom Meetings from Salesforce (with Scheduler)

Introduction

A customer of Salesforce is looking to integrate with Zoom and the Online Meeting capabilities from Salesforce using Portal / Internal screens. The use case is fairly straightforward, as a customer of a bank, I want to schedule an online meeting with my advisor and discuss my accounts. The technology the bank wants to use is Zoom.

The technology usage behind this particular demo is to simply create a meeting in Zoom via an API which returns a URL that parties can click on to launch Zoom( either browser or desktop ) and have a meeting. There is a lot more that can be done, but this integration is to show the possibilities with the connection

Technology Utilized

Listened below are the main technologies and configurations used in the demo preparation.

Main SaaS Tech Stacks

Demo Setup Technology

  • Zoom Development Account
  • Salesforce Org
  • Postman – To easily test the setup and API’s
  • <optional> Visual Code – Used to write and deploy code
  • Apex Classes
    • Invocable Methods setup to make available in Flows
    • Test Classes
  • Auth Provider Setup in Salesforce
  • Named Credentials
  • Salesforce Flows – To provide the screen navigation, call Zoom to get the meeting, and update the appropriate records.

Demo Flow High Level

This section describes the general flow of the demo from start to finish when showing to the customer. This is a specific flow for the customer this demo was prepared for. You can tailor or make your own flow(s) for your customer.

  • Customer goes to the portal to schedule a meeting, using the Salesforce Scheduler.
  • This demo has an authenticated flow, there is a person account associated with the demo.
  • Enter the required information from the screen
    • Click by previously scheduled service appointments or search for a banker.
    • Click Next
    • Click on a Work Type Group → Select the type => Click Next
    • Select Video Call
    • Enter a address → San Francisco → Select a branch
    • Choose a date in scheduler
    • The call to Zoom is done at this point and a URL is returned to the “Additional Information” Section. Note, this could be put anywhere but for the purpose of the meeting we chose here.
    • Click on next and a new Service Appointment is created with the Meeting URL in a custom field that can be accessed to launch a Zoom meeting.

The integration behind the scene created a Zoom meeting valid link that can be clicked on to start a Zoom meeting. This utilized the Zoom API, OAuth/Open ID authentication/authorization, and Apex Callout to create and return the meeting.

Demo Details

This section will describe the setup needed to execute the demo. What is described is the particular flow used to validate and test the integration. The entire section for Postman is completely optional, but it is suggested to do this to validate and debug. In our case, it was invaluable in determining the exact headers to place into the API. In the initial development, Postman was first used outside of Salesforce to ensure the API format was correct and the authentication/auth was setup correctly in Zoom.

Zoom Setup

The first piece that is needed is a Zoom Developer Account.  Please note you cannot use a Salesforce Zoom account as they have disabled permission to use the API.  Best bet create Personal Zoom account with personal ( no salesforce account) or working with your client account that allows to create meeting via API

image.png
  • Click on Create under OAuth.
image.png
  • Name it, choose an account level app and toggle off the publish button like this.
image.png
  • Next screen
    • Screen will show the client key, client secret. Copy & store in a secure place. You will need these in the Salesforce setup.
    • Populate the redirect url and add the allow list.
image.png
  • Enter developer name.
  • Scope is meeting:write:admin
  • Press install.

Setup Salesforce/Postman Application in Zoom

Salesforce Setup

This section describes the Salesforce Setup needed to access Zoom API meetings. It is strongly advised to setup Postman first to validate and test the API as well as get familiar with the API before jumping into Apex coding setup. This section will consist of Authentication setup, the Named Credential, and the Apex code needed to create a Zoom meeting and retrieve a URL. Additionally, this will provide the callback referenced in the Zoom section which will be needed to complete the Authentication section in Zoom.

Auth. Provider Setup

The first step is to create the Authorization Provider configuration to connect to Zoom. You will need the Secret Key(Value) from the Certificate setup in Zoom and the application id.

  1. Enter Salesforce Setup → Auth Providers → New.
  2. Choose Open ID Connect from the Provider Type drop-down.
  3. Name the Auth Provider.
  4. URL Suffix → Can make the same as the name.
  5. Consumer Key → This is the client key in Zoom.
  6. Consumer Secret → This is the client secret in Zoom.
image.png
  1. Authorization Endpoint URL – https://zoom.us/oauth/authorize see screen above.
  2. Token Endpoint URL – https://zoom.us/oauth/token see screen above.
  3. Default Scopes <optional> – meeting:write:admin
  4. Make sure this option is checked: Send client credentials in header.
  5. Use the defaults for the remainder.
  6. Save.
  7. Click on the newly defined Auth Provider to open it up.
  8. Copy the Callback URL that is shown in the Salesforce Configuration. If you have Experience Cloud setup and you are using those domains, then add those callback URLs as well to the Zoom Dev configuration.
  9. Take the callback URL you copied above and paste back in the Zoom redirect url & allow list. (Use the exact same word to word, no space.)
image.png

Named Credential

Creating a named credential will perform the authentication to Zoom and do the OAuth validation. Here is where the Zoom Authenticated App is used to validate the OAuth connection. (I set mine to auto approve.) It will prompt you to login to your instance of Zoom. The login will be the Zoom Developer login/id created during the setup of the developer instance.

  1. In Setup → Named Credentials, click New.
  2. Enter the label and the URL of the API Endpoint you wish to access. In this case, the full API is used for the onLineMeeting for Delegated access. The application access has a different signature
  3. Identify Type will be Named Principal.
  4. Auth Protocol will be OAuth 2.0.
  5. Select the Auth Provider created in the previous step.
  6. Scope: very important! The scope here will match the scope you created in Zoom for the application. It is space delimited.
    • meeting:write:admin
  7. Click on Generate Authorization Header.
  8. Check on Start Auth Flow on Save.
  9. Generate Auth Header is checked.
  10. Click Save. This will trigger the actual connection to Zoom and validate the Authentication.
  11. When you Save, the OAuth flow will initiate.
image.png
  • Zoom Login Screen will popup.
  • Login.
  1. If successful, you will see Authenticated in the Authentication Status in Salesforce. If it fails, you will get a failure screen from Zoom.  Common Issues:
  • Used the wrong secret key or id. Double check the values in the Auth. The Providers are correct.
  • URL Endpoint is invalid. Check your URL Endpoint. (Using Postman first helps avoid this issue.)

Salesforce Application Code

This section will review the Apex application code needed to make the Rest API call to setup a Zoom meeting and retrieve the meeting URL from the Zoom Server. The code is not production quality but instead is a sample to prove the concepts. The current iteration does not accommodate error handling in a meaningful way nor does it do much more than create a meeting. The goal of this good is to demonstrate the basic connection for the customer and proof of concept the meeting invites. The sample code will be in two parts, the first part is a test class that can be used to validate the connection and result. Once that is working, then the 2nd class uses @InvocableMethod so that it can be used in flows. Obviously this can be tailored however desired.

This section does not instruct how to setup command line tools, deploy the source, or other development tasks. It is assumed the reader understands how to deploy, run the developer tools/debugger in Salesforce.

Test Apex Class – Simple Class to Create the Meeting utilizing Named Credentials

public class TestZoomApi {

   /* Test Method for Unit Testing Connection */

   public static String getMeetingUrl()

   {

       HttpRequest req = new HttpRequest();

       Http http = new Http();

       //Setup the Endpoint and append the name of the file

       req.setEndpoint(‘callout:SalesforceZoomPOCNC’);

       req.setMethod(‘POST’);

       req.setHeader(‘Content-Type’,’application/json’);

       req.setHeader(‘Accept’,’*/*’);

       req.setHeader(‘Accept-Encoding’,’gzip, deflate, br’);

       //Setup the JSON Body – in the test just set a subject, can add more through Postman or other tests

       req.setBody(‘{“topic”: “test create meeting”,”type”: “1”}’);       

       //req.setBody(‘{}’);

       System.debug(‘Body: ‘ + req.getBody());

       System.debug(‘Endpoint Value: ‘+ req.getEndpoint());

       System.debug(‘Request: ‘ + req);

       HTTPResponse res = http.send(req);

       System.debug(‘Response Body: ‘+res.getBody());

       /* Parse Response */

       JSONParser parser = JSON.createParser(res.getBody());

       String webLink;

       webLink = ‘ZoomNotSetup’;

       while (parser.nextToken() != null) {

       if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&

       (parser.getText() == ‘join_url’)) {

           parser.nextToken();

           webLink = parser.getText();

           System.debug(‘join_url= ‘ + webLink);

           }

       }

   return webLink;

   }

}

Common Issues/Errors

  1. Zoom will return a 201 in the HTTP Response. This is a success. It is documented in the API.
  2. HTTP Error 500 – Internal Server Error
    1. Notice the headers that are set. The Zoom documentation does not state it explicitly, but in our testing we found that the 2 accept lines need to be in the header when sending the request. You need to add the gzip, etc and the “*/*” accept lines.
    2. 401 – The 401 unauthorized generally means the scope is incorrect in the Named Credential, or in the Named Credential and/or the Zoom setup. Make sure the permissions are correct and the scope is space delimited.
    3. 403 – Forbidden – This occurs when the secret keys are incorrect. You should have fixed this when you saved the name credential so it shouldn’t show up.
  3. Note: When using the Apex Debugger – Look for the debug line to joinWebURL populated with a long string for the meeting. It will start with something like this: 12:40:52:477 USER_DEBUG [37]|DEBUG|joinWebUrl= https://us04web.zoom.us/j/xxxxxxxxxx?pwd=xxxxxxxxxxxxxxxxx

Salesforce InvocableMethod Class Utilizing the Apex Code and Callout

This is the actual code used in the demonstration. This code is accessible in the Flow Builder inside of Salesforce as an Apex Action.

global class GetZoomMeetingURLwithInput {

   @InvocableMethod(label=’Get Zoom Meeting URL with Input’ description=’Returns a meeting URL For Zoom’)

   global static List<String> makeApiCalloutwithInput(List<List<String>> inputZoomsParms)

   {

       HttpRequest req = new HttpRequest();

       Http http = new Http();

       //Setup the Endpoint and append the name of the file

       req.setEndpoint(‘callout:SalesforceZoomPOCNC’);

       req.setMethod(‘POST’);

       req.setHeader(‘Content-Type’,’application/json’);

       req.setHeader(‘Accept’,’*/*’);

       req.setHeader(‘Accept-Encoding’,’gzip, deflate, br’);

        /* Setup the Parameters for Meetings, subject, etc. */

       // Note: The initial demo only utilized the title, further development can use other inputs.

       system.debug(‘Array size  =’ + inputZoomsParms.get(0).size()); 

       String inTitle = ‘”‘ + inputZoomsParms.get(0).get(0) + ‘”‘;

       system.debug(‘inTitle =’ + inTitle);   

       String inAgenda = ‘”‘ + inputZoomsParms.get(0).get(0) + ‘”‘;

       system.debug(‘inAgenda =’ + inAgenda);              

       String inPwd = ‘”‘ + inputZoomsParms.get(0).get(1) + ‘”‘;

       system.debug(‘inPwd =’ + inPwd);               

       String inStart = ‘”‘ + inputZoomsParms.get(0).get(2) + ‘”‘;

       system.debug(‘inStart =’ + inStart);               

       String inEnd = ‘”‘ + inputZoomsParms.get(0).get(3) + ‘”‘;

       system.debug(‘inEnd =’ + inEnd);

       //Setup the JSON Body – in the test just set a subject, can add more through Postman or other tests

       // req.setBody(‘{“topic”:’ + inTitle + ,”type”: “1”}’);       

       req.setBody(‘{“topic”: “test create meeting”,”type”: “1”}’);       

       //req.setBody(‘{}’);

       System.debug(‘Body: ‘ + req.getBody());

       System.debug(‘Endpoint Value: ‘+ req.getEndpoint());

       System.debug(‘Request: ‘ + req);

       HTTPResponse res = http.send(req);

       System.debug(‘Response Body: ‘+res.getBody());

       /* Parse Response */

       JSONParser parser = JSON.createParser(res.getBody());

       String webLink;

       webLink = ‘ZoomNotSetup’;

       while (parser.nextToken() != null) {

       if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&

       (parser.getText() == ‘join_url’)) {

           parser.nextToken();

           webLink = parser.getText();

           System.debug(‘join_url= ‘ + webLink);

           }

       }

   // Apex Actions Return. The method signature for an invocable method requires a List of Strings to be returned.

   return new List<String>{webLink};

   }

}

Salesforce User Experience Setup

This section is optional but describes how the above code can be accessed in the low-code building tools inside of Salesforce.

This section in a flow shows how this action is called to retrieve a URL for the Zoom Meeting and then assigns it to a Record (such as the Service Appointment, and/ or its associated Event) in Salesforce. A larger flow will then do further processing as part of a full scheduling flow. The takeaway is that the Apex Code above is an option to drag and drop into the low code builder and use it when running a flow. It could also be embedded within a Lightning Web Component(LWC) and used in other places. Alternatively, it can be accessed from another Apex Class to get the required information.

image.png

Postman Setup

This section describes how to setup Postman to access Zoom APIs and test the API integration outside of Salesforce in a developer-centric manner. This is completely optional, but oftentimes if it works in Postman and not in another application or Salesforce, you can see what is different in Postman versus the other applications. This tutorial assumes the reader is familiar with Postman and has downloaded the application or is using the web version. This document will use the desktop installation for reference. This section does not require knowledge of the Salesforce Setup. We recommend starting with Postman before the setup in Salesforce.

Postman Collection – use version 2 link below. (Do NOT use v1.)

Download the Zoom Postman Collection

To make life easier, download the Postman Collection already created for Zoom as plenty of examples. To do so:

Setup Postman Authentication

Setting up Authentication is straightforward and will require the client id and secret key from the Zoom setup done previously. Additionally, you will need to add the callback URL to the Authentication section in Zoom. You also need to setup Environment variables as part of this step.

Setup Zoom Dev account with Postman OAuth App

image.png
  • Click on Create under OAuth.
image.png
  • Name it, choose an account level app and toggle off the publish button like this.
image.png
  1. ClientID – will be the Application ID of the App you created in the Zoom dev account.
  2. Client Secret – will be the Client Secret in Zoom dev account.
  3. Make sure the Redirect url and Allow List are exactly this https://app.getpostman.com/oauth2/callback
image.png
  1. Enter developer name.
  2. Scope is
    • meeting:write:admin
  3. Press install.

Setup Postman authentication

  • Setup Zoom environment like this, and:
  • Edit api_key with Client ID from Zoom dev account.
  • Edit api_secret with client secret from Zoom dev account.
image.png
  • Go to Zoom Api. Right click. Edit, and:
    • Edit setup variable with baseurl.
image.png
  • Edit Authorization with appropriate info as shown below.
    • Make sure the callback url is exactly the same in the Zoom Dev account.
image.png
image.png
  • Get a new Access Token.
  • You will have to login in the Zoom account.

Get/Create and Run the Zoom Online Meeting API

image.png

{
“topic”: “test create5 meeting”,
“type”: “2”,
“start_time”: “2022-02-15T10:00:00”,
“duration”: “30”,
“registrants_email_notification”: “true”
}

  • Authentication – inherited from the parent.
  • Set this:
image.png

From Ankit Srivastava: Report available Appointment Slots by Resource

Salesforce Scheduler gives tools needed to simplify appointment scheduling in Salesforce. We can create a personalized experience by scheduling customer appointments in person, or by phone or video; with the right person at the right place and time. One of the strong feature of the product is its ability to manage availability of multiple Service Resources for various Work Types across multiple Service Territories.

Now the biggest question is if there is an out of the box Salesforce Report that can provide a list of service appointment slots available for all or a specific set of service resources. The Answer is NO.

So, how do we solve for it?

Salesforce Scheduler calculates the availability of a service resource at runtime on demand (when a user is scheduling an appointment). Salesforce Scheduler uses multiple entities along with data from external systems to calculate availability data. This information is NOT stored anywhere to report on.

To solve for the reporting Question – we need some complex level of customization. This can be achieved using the power of the Salesforce Scheduler and Salesforce platform to generate reports on Service Resource’s availability information.

Approach we will use in this blog post

Salesforce Scheduler provides the LxScheduler namespace which offers the apex method getAppointmentCandidates which provides the availability information. We will use this method to

  • retrieve the information using Apex
  • dump the retrieved information into a custom object that we will create and
  • report on top of the custom object

Create Custom Object – Appointment Slots

Let us create a custom object, “Appointment Slots” with these custom fields to store availability information

  • Service Resource (lookup)
  • Service Territory (lookup)
  • Work Type Group (lookup)
  • Start Date (Date/Time)
  • End Date (Date/Time)
  • Remaining Appointments (Integer, default value = 1)

Enable reporting for this custom object.

Query and Dump retrieved information into the Custom Object

We can use following batch apex to populate availability information in our custom object.
P.S. Apex governor limits will apply on this code (https://developer.salesforce.com/docs/atlas.en-us.234.0.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_apexgov.htm).

Slot.apex

class Slot {
Datetime startTime;
Datetime endTime;
List<String> resources;
String territoryId;
}

ReportDump.apex

public without sharing class ReportDump implements Database.Batchable<sObject>{
public Database.QueryLocator start(Database.BatchableContext BC){
String query = 'SELECT Id FROM ServiceResource where isActive = true';
return Database.getQueryLocator(query);
}

public void execute(Database.BatchableContext BC, List<ServiceResource> srList){
Integer DUMP_DURATION = 30;
String ACCOUNT_ID = '001xx000003GYQRAA4';
String SCHEDULING_POLICY = '0Vrxx0000004C92';
//Hardcoding WTG for now
List<String> wtgs = new List<String>();
wtgs.add('0VSxx0000004C92GAE');

List<Appointment_Slots__c> slots = new List<Appointment_Slots__c>();

for (ServiceResource sr : srList){
//Find all territories he works in
List<ServiceTerritoryMember> stms = [Select Id, ServiceTerritoryId
from ServiceTerritoryMember
where ServiceResourceId = :sr.Id];
List<String> territories = new List<String>();
for (ServiceTerritoryMember stm : stms){
territories.add(stm.ServiceTerritoryId);
}

for (String wtg : wtgs){
lxscheduler.GetAppointmentCandidatesInput input = new lxscheduler.GetAppointmentCandidatesInputBuilder().setWorkTypeGroupId(wtg).setTerritoryIds(territories).setStartTime(System.now().format('yyyy-MM-dd\'T\'HH:mm:ssZ', 'America/New_York')).setEndTime(System.now().addDays(DUMP_DURATION).format('yyyy-MM-dd\'T\'HH:mm:ssZ', 'America/New_York')).setAccountId(ACCOUNT_ID).setSchedulingPolicyId(SCHEDULING_POLICY).setApiVersion(Double.valueOf('54.0')).build();
String response = lxscheduler.SchedulerResources.getAppointmentCandidates(input);

List<Slot> objs = (List<Slot>)JSON.deserialize(response, List<Slot>.class);
for (Slot obj : objs){
Appointment_Slots__c slot = new Appointment_Slots__c();
slot.Start_Date__c = obj.startTime;
slot.End_Date__c = obj.endTime;
slot.Service_Territory__c = obj.territoryId;
slot.Service_Resource__c = sr.Id;
slot.Work_Type_Group__c = wtg;
slots.add(slot);
}
}
}
insert slots;
}

public void finish(Database.BatchableContext BC){
}

}

Create a Report using the Custom Object

Create a Salesforce report using this custom object. Refer – https://help.salesforce.com/s/articleView?id=sf.rd_reports_overview.htm&type=5 on how to create reports. Here is a sample matrix report we created which reports a Service resources total available hours at a Service location for an appointment topic / template – and this is how it looks.

Truncating records in custom Object

The example quoted above requires the Appointment Slots object to be empty in order to calculate the Service Resources availability. Ensure you truncate the data in the custom object before running the above batch job. Please refer this help article on Truncating Salesforce Obejcts https://help.salesforce.com/s/articleView?id=sf.dev_object_trunc.htm&type=5.

You can also use apex to delete all records from the object as shown at https://developer.salesforce.com/forums/?id=906F0000000BPDTIA4

From Ankit Srivastava: Restraining Shifts within the Service Territory’s Operating Hours

In the Spring ‘22 release, Scheduler introduced Shift Rostering Management which can be used to setup working hours for resources.

Shifts is a paradigm changing update to Salesforce Scheduler product. It eases pain of setting adhoc or non-recurring time slots for Service Resources to work. It also gives flexibility to work outside Service Territory’s operating hours.

One of the advantages of using Shifts is the ability to use Custom reports to report on working hours of a resource but that goes away if we allow resources to setup shifts beyond the working hours of a Territory. So here one would want to restrict the Service Resource to only create Shifts within the working hours of a Service Territory. This way, you could use create a custom report on the Shift record to understand the working hours of previous weeks or months.

Another scenario where this could be a need is when we have two types of Service Resources:

  1. Service Resources who work within the constraints of their Service Territory’s Operating Hour
  2. Service Resources who can go beyond their Service Territory’s Operating Hours. For eg: contractors (independent wealth managers) who can even take important appointments after working hours of the Service Territory (Branch)

Selectively restraining working hours of resources

First we need to define which resources can work beyond their branches working hours.

For this we are going to simply add a custom field to Service Resource of type boolean. Let’s call this field OvertimeEnabled for now.

If OvertimeEnabled is True then the Service Resource can work extra hours else they cannot.

Data Model

Next we need to set our Scheduling Policy such that we don’t restrict Shifts as per Service Territory’s Operating Hour by default. For this we will se up policy to use Shifts but uncheck the box ‘Use service territory’s operating hours with shifts’

Now we can add a trigger on Shift entity which runs before insert and update on the entity. In this trigger we will check validate that shift timings are within Branch’s Operating Hours only for resources who have OvertimeEnabled flag set to false.

Sample Code

https://github.com/anardana/salesforce-scheduler-shifts-validation

trigger ShiftValidation on Shift (before insert, before update) {
List<Shift> shifts = Trigger.new;

Set<String> sr = new Set<String>();
for(Shift shift: shifts) {
sr.add(shift.ServiceResourceId);
}

List<ServiceResource> extendedSr = [SELECT Id FROM ServiceResource WHERE OvertimeEnabled__c = False AND Id IN :sr];

Set<String> srIds = new Set<String>();
for(ServiceResource sr1: extendedSr) {
srIds.add(sr1.Id);
}

Set<Shift> confirmedShifts = new Set<Shift>();
Set<String> territoryIds = new Set<String>();
//We will only run this validation for Shifts with Status Category = "Confirmed" AND Service Resource can work extended hours
for(Shift shift: shifts) {
if(shift.StatusCategory == 'Confirmed' && srIds.contains(shift.ServiceResourceId)) {
confirmedShifts.add(shift);
territoryIds.add(shift.ServiceTerritoryId);
}
}

if(confirmedShifts.size() > 0) {
//Get all Service Territory IDs along with their OperatingHourId
ServiceTerritory[] territoryIdsWithOperatingHours = [SELECT Id, OperatingHoursId FROM ServiceTerritory WHERE Id IN :territoryIds];

Set<String> operatingHourIds = new Set<String>();
for(ServiceTerritory st: territoryIdsWithOperatingHours) {
//Service Territory may not have Operating Hour defined
if(st.OperatingHoursId != null) {
operatingHourIds.add(st.OperatingHoursId);
}
}

//Get Timeslot information for all Operating Hours got in previous step. We disregard all STM level concurrent timeslots (Shifts with MaxAppointments set to 1)
TimeSlot[] timeSlots = [SELECT Id, DayOfWeek, StartTime, EndTime, OperatingHoursId FROM TimeSlot WHERE MaxAppointments = 1 AND OperatingHoursId IN :operatingHourIds];

//Complex data structure to store Working hours for all Service territories. We will store empty inner map in case Service Territory does not have Operating Hour defined
Map<String, Map<String, TimeSlot>> serviceTerritoryWithTimeSlotsPerDay = new Map<String, Map<String, TimeSlot>>();

for(ServiceTerritory territory: territoryIdsWithOperatingHours) {
Map<String, TimeSlot> timeSlotsPerDay = new Map<String, TimeSlot>();

for(TimeSlot timeSlot: timeSlots) {
if(timeslot.OperatingHoursId == territory.OperatingHoursId) {
timeSlotsPerDay.put(TimeSlot.DayOfWeek.substring(0,3), TimeSlot);
}
}
serviceTerritoryWithTimeSlotsPerDay.put(territory.Id, timeSlotsPerDay);
}

//Main validation logic for all confirmed shifts
for(Shift s: confirmedShifts) {
String dayOfShiftStart = ((Datetime) s.StartTime).format('E').substring(0,3);
String dayOfShiftEnd = ((Datetime) s.EndTime).format('E').substring(0,3);

Map<String, TimeSlot> slots = serviceTerritoryWithTimeSlotsPerDay.get(s.ServiceTerritoryId);

if(dayOfShiftStart != dayOfShiftEnd) {
s.addError('Shift should be within Service Territory\'s Operating hours ');
} else if(slots.get(dayOfShiftStart) == null || slots.get(dayOfShiftEnd) == null) {
s.addError('Operating Hours for Service Territory not set up correctly');
}else if(s.StartTime.time() < slots.get(dayOfShiftStart).StartTime || s.EndTime.time() > slots.get(dayOfShiftEnd).EndTime) {
//Eureka
s.addError('Shift should be within Service Territory\'s Operating hours ');
}
}
}
}

P.S. For this code we have considered that Service Territory’s OH are in same timezone as shifts being created.

From Mrityunjoy Chowdhury: Adding Multiple Shift records using a CSV file

In the Spring ‘22 release, Scheduler introduced Shift Rostering Management which can be used to setup working hours for resources. 

Instead of adding shift records one at a time, we can add multiple shifts in one go! Salesforce Scheduler allows adding multiple shifts. In this document we will demonsrate how to upload shifts using Salesforce bulk API capability.

Here I’ve even provided a template to help you create the final CSV which you need to upload!

Quick Help Video on process

Video how to create BulkShift csv data

Step 1.a: Creating Shifts CSV file directly 

If you plan to share this with business users, try the format in 1.b which allows you to create the Id & record mapping and the business user only needs to select the record names.

  1. Create spreadSheet/Excel file 
  2. Copy Paste the first row of the below table
  3. StartTime/EndTime Should be in GMT format (Local Time and GMT time difference should be adjusted)
  4. Status should be Published,Confirmed,Tentative
  5. TimeSlotType should be Normal
  6. File -> Download/Export-> download as CSV file
  1. Open Convert  CSV to JSON and upload the file 
  1. Copy the CSV data and use it as CSV shift data in Upload Shift data step.
StartTimeEndTimeStatusServiceTerritoryIdServiceResourceIdOwnerIdTimeSlotType
2022-03-30T04:30:00.000Z2022-03-30T16:30:00.000ZConfirmed0HhS70000004EkMKAU0HnS700000007xwKAA005S7000000VndRIASNormal
2022-03-29T04:30:00.000Z2022-03-29T16:30:00.000ZConfirmed0HhS70000004EkMKAU0HnS700000007xwKAA005S7000000VndRIASNormal
2022-03-26T04:30:00.000Z2022-03-29T16:30:00.000ZConfirmed0HhS70000004EkMKAU0HnS700000007xwKAA005S7000000VndRIASNormal

Step 1.b: Create bulk Shift CSV from customized spreadsheet (Optional step).

  1. Download this File: BulkShifts
  1. Navigate to the “ShiftsMetadata” and add all the metadata of the Shifts. (Step done by Admin)
  • Service Resource Name,Service Resource Id
  • Service Territory Name,Service Territory Id,
  • Owner Name,Owner Id,
  • WorkTypeGroup Name,WorkTypeGroup Id,
  • GMT Time difference. (GMT time difference is the time difference from your local time to GMT Time).
  1. Navigate to next sheet “DraftShifts” Create your required shifts based on the data you have uploaded in ShiftMetadata sheet,(Note No need to add any data in “GMT Start Time” and “GMT End Time”, it will populate the data automatically on dragging the column. Calculation is already done.Remember that don’t delete the first column because it has all the formulas, just update the first row as per you need.
  1. Navigate to FinalShiftData and you will see the status is already populated.

Drag the rest of the columns to populate all the data from DraftShits to Final Shifts. If there is #NA value present for any cell , remove those values to make value empty.(Note: Do not delete the first row as it has all the formulas. It will populate the value and then drag the column)

  1. Export the FinalShiftData sheet as CSV file

 File -> Download -> Comma Separated Value.

  1. Open Convert  CSV to JSON and upload the file 
  1. Copy the CSV data and use it as CSV shift data in Upload Shift data step.


Step 2: Create a Bulk Job 

Bulk API is rest based so you can use the salesforce workbench to make bulk API requests. 

   A. Open Workbench 

   B. Select Environment and API version (Note: shift for Scheduler is introduced after 54.0)

   C. login to workbench

    D. In the top menu, select utilities | Rest Explorer

We will use /job/ingest resource to create create a Job

URI/services/data/vXX.X/jobs/ingest
Request MethodPOST
Request HeaderContent-Type: application/json; charset=UTF-8Accept: application/json
Request Body{ “object”: “Shift”, “contentType”: “CSV”, “operation”: “insert”, “lineEnding”: “CRLF”}

You should get a response that includes the job ID, with a job state as Open. You’ll use the job ID in Bulk API 2.0 calls in the next steps

Step 3: Upload your CSV data

After creating a job, you’re ready to upload your data. You will provide record data using the CSV formatted data or file you created in steps 1.

URI/services/data/vXX.X/jobs/ingest/jobId/batches
Request MethodPUT
Request HeaderContent-Type: text/csvAccept: application/json
Request BodySample Data:
EndTime,StartTime,Status,ServiceResourceId,ServiceTerritoryId,OwnerId,TimeSlotType,WorkTypeGroupId,WorkTypeId2022-01-14T14:00:00.000Z,2022-01-14T09:30:00.000Z,Tentative,0HnS700000007xwKAA,0HhS70000004EkMKAU,005S7000000VndPIAS,Normal,,2022-01-15T14:00:00.000Z,2022-01-15T09:30:00.000Z,Tentative,0HnS70000004EFyKAM,0HhS70000004EkMKAU,005S7000000VndRIAS,Normal,,

Step 4: Upload complete

Once you’re done submitting data, you can inform Salesforce that the job is ready for processing by closing the job.

URI/services/data/vXX.X/jobs/ingest/jobId
Request MethodPATCH
Request HeaderContent-Type: application/json; charset=UTF-8Accept: application/json
Request Body{    “state” : “UploadComplete”}

Step 5: Check the job status and results.

To get basic status information on a job, such as the overall job state or the number of records processed, use a GET request with the following details:

URI/services/data/vXX.X/jobs/ingest/jobId
Request MethodGET
Request HeaderContent-Type: application/json; charset=UTF-8Accept: application/json

job has been completed and is in the JobComplete state (or Failed state)


Step 6: Check the processed data.

To verify that record was successfully processed or not successfulResults for processed data and failedResults for the records are not processed

URI/services/data/vXX.X/jobs/ingest/jobId/successfulResultsservices/data/vXX.X/jobs/ingest/jobId/failedResults
Request MethodGET
Request HeaderContent-Type: application/json; charset=UTF-8Accept: application/json