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.

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.

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

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.

Request MethodPUT
Request HeaderContent-Type: text/csvAccept: application/json
Request BodySample Data:

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.

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:

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

Request MethodGET
Request HeaderContent-Type: application/json; charset=UTF-8Accept: application/json