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

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

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

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) {

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)) {

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) {

//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) {
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.