- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
‎06-17-2025 12:43 AM - edited ‎06-19-2025 12:16 AM
Introduction
Have you (or your customers) ever find the need to understand why dynamic scheduling behaved in a certain way? Why did it assign agent A to this work order task? Why did it not assign agent B? Or why does it refuse to assign ANY agent at all, what are my agents failing on? Have you built specific rules for scheduling, like matching skills, mandatory parts, rejected technicians or excluded technicians, that must be honored, even when doing manual scheduling?
This article explains how we can leverage dynamic scheduling to implement these scheduling criteria, make sure they are satisfied for manual attempts too, and post all the relevant insights to work notes for full transparency.
Turning on the dynamic scheduling logs
First of all, we will need to ensure the dynamic scheduling logs are being written at all. Please navigate to Field Service > Dynamic Scheduling Administration > Properties and set "Show advanced agent recommendation logs to user" to true (com.snc.dynamic.scheduling.showlogs), then proceed with the following sections.
Extending the dynamic scheduling processor
In order to perform our logic, we need the output of the DynamicSchedulingProcessor's processing function. In order to achieve this, we will create a scripted extension point for the DynamicSchedulingProcessor that extends the `
/* Retrieves the next extension point in line. */
getExtensionPoint: function(record) {
if (typeof GlideScriptedExtensionPoint == WMConfigurationConstants.UNDEFINED) {
gs.error(gs.getMessage("DynamicSchedulingProcessorExtensionPoint: Scripted extension points are not enabled"));
return null;
}
try {
var ep = new GlideScriptedExtensionPoint().getExtensions(DynamicSchedulingConstants.DYNAMIC_SCHEDULING_PROCESSOR_EXTENSION_POINT);
for (var e = 0; e < ep.length; e++) {
// Skip this one.
if(ep[e].type === this.type) {
continue;
}
if (ep[e].handles == undefined || ep[e].handles(record)) {
ep[e].initialize();
return ep[e];
}
}
} catch (ex) {
gs.error(gs.getMessage('DynamicSchedulingProcessorExtensionPoint: Error fetching extension point ' + ex));
}
return null;
},
This is just a copy of the standard DynamicSchedulingProcessorHelper's getExtensionPoint, but with a few lines added to skip this particular EP in its detection, so that it grabs the next one in line. runDynamicSchedulingProcessor: function(task_ids, task_table, unAssign, configObj, performAssignment, centralDispatch, appointmentBooking, appointmentRangeStart, appointmentRangeEnd, mustAssignedTasks, inputTaskGR, mode) {
var response = this.getExtensionPoint(task_table).runDynamicSchedulingProcessor(task_ids, task_table, unAssign, configObj, performAssignment, centralDispatch, appointmentBooking, appointmentRangeStart, appointmentRangeEnd, mustAssignedTasks, inputTaskGR, mode);
try {
(new FSMUtils()).createDynamicSchedulingWorkNote(response, performAssignment);
}
catch(e) {
gs.error("FSM exception in dynamic scheduling work note creation: " + e.getMessage());
}
return response;
},
{
"success": true,
"data": {
"recommendationResponseMap": {
"ef1a8b34df113100dca6a5f59bf26327": {
"success": false,
"current": "WOT0008018",
"message": "No resource is available at specified time(s)",
"msgType": "error",
"logs": [
"Task : WOT0008018",
"Assignment groups:[WM Agents]",
"Groupusers: Chuck Farley,Colin Altonen,Cristina Sharper,WM Agent",
"Matching rules: [Assignment : Assign Pending Dispatch Work Order Task]",
"Resources match dimension \"Ignore Rejected Technician \":Chuck Farley,Colin Altonen,Cristina Sharper,WM Agent",
"Resources match dimension \"Matching Skills For Dynamic Scheduling\":Chuck Farley",
"Resources match dimension \"Current Distance From Task\":Chuck Farley",
"Resources match dimension \"Consistent Assignment for SM tasks\":Chuck Farley",
"Candidates from Matching Rule : Chuck Farley",
"Window start: 2025-06-05 15:58:35: Window end:2025-06-19 15:58:35",
"Window start after dependency check: 2025-06-05 15:58:35: Window end after dependency check:2025-06-19 15:58:35",
"Qualifying agents: Chuck Farley",
"Workblocks for Chuck Farley",
"Sorted work blocks:",
"Task : WOT0008018: Error: No resource is available at specified time(s)"
],
"latLongInvalid": false,
"foundQualifiedAgents": true,
"calculatedData": {
"candidatesFromMatchingRule": [
"46c9e158a9fe198101d44d0d22cb640d"
]
},
"task_filter": "satisfied"
}
},
"unassignedTaskMap": {},
"taskInfoMap": {
// ....
},
"isReverted": false,
"latLongInvalid": false
},
"errorCode": "",
"msgType": "success"
}
From this we can derive a number of useful pieces of info:
- Was the attempt successful or was there some error? Keep in mind that "success" does not necessarily mean successfully scheduled, it just means the algorithm ran without any errors, whether it resulted in a schedule or not
- For each of the tasks included (recommendation response map), what was the outcome?
- Was it successful? In this case it does mean whether we were able to identify some resource
- What was the human-readable message?
- If we have the system property com.snc.dynamic.scheduling.showlogs enabled, we get a list of strings that make up the logs showing the output of the matching criteria, the window calculations, and the work blocks calculations. This is where we extract most of the useful information.
- Did we unassign anything to make place for this? (unassigned task map)
Based on this info, we can now create some logic that allows us to expose it, for instance as a work note. It is of course possible to do anything else with this such as storing it in a more structured way, but for the purpose of this article we will write to work notes.
Working with the output
In a new script include called FSMUtils I have built some logic that allows us to work with the response object as seen above. The example simply writes the execution log to work notes, which allows us exactly to see which agents sequentially "survive" the validation rules.
We can also see whether unassignment of a lower priority task has happened, and we can see in the `performAssignment` param whether assignment has actually been performed and completed (e.g. because this was a fully automated scheduling and the system told it to commit) or whether it was just a check.
This gives us the 3 functions below:
createDynamicSchedulingWorkNote: function(schedulingOutputObj, performAssignment) {
for (var wotId in schedulingOutputObj.data.recommendationResponseMap) {
var result = schedulingOutputObj.data.recommendationResponseMap[wotId];
var grTask = new GlideRecord('wm_task');
grTask.get(wotId);
grTask.work_notes = this.getDynamicSchedulingWorkNoteContents(result, performAssignment);
grTask.update();
// Create work note for unassigned tasks
var unassignmentMap = schedulingOutputObj.data.unassignedTaskMap;
if (Object.keys(unassignmentMap).length > 0) {
this.createUnassignedTaskWorknote(unassignmentMap, grTask);
}
}
},
createUnassignedTaskWorknote: function(unassignmentMap, grAssignedTask) {
Object.keys(unassignmentMap).forEach(function(wotId) {
// Sometimes it considers itself for unassignment, in which case the message doesn't make sense.
if (wotId === grAssignedTask.getValue('sys_id')) {
return;
}
// Retrieve the unassigned task and add the note.
var grWOT = new GlideRecord('wm_task');
if (!grWOT.get(wotId)) {
return;
}
grWOT.work_notes = gs.getMessage("Dynamic scheduling: this task was automatically unassigned in favour of higher priority task {0}.", [
grAssignedTask.getValue('number')
]);
grWOT.update();
});
},
getDynamicSchedulingWorkNoteContents: function(result, performAssignment) {
// Successful
if (result.success) {
var baseText = performAssignment ?
"Scheduling assigning to {0} for start date {1}\n\n\n{2}" :
"Scheduling proposed/allowed for {0} at start date {1}\n\n\n{2}";
return gs.getMessage(baseText, [
result.suggestedResources[0].agentName,
result.suggestedResources[0].taskUpdates.expected_start,
result.logs.join("\n")
]);
}
// Unsuccessful
return gs.getMessage("Scheduling unsuccessful: {0} {1}\n\n\n{2}", [
result.current,
result.message,
result.logs.join("\n")
]);
},
Running dynamic scheduling on every assignment
At this point, our custom logic is called when dynamic scheduling is incurred. However, we may want to also run this logic for manual scheduling attempts. The use case is for us to validate that the assigned agent satisfies all dynamic scheduling criteria (e.g. has the required skills etc).
In order to run the validation on manual assignment attempts, I create a new business rule "Before assign, validate scheduling rules" that runs on the wm_task table, `before` Update, with a condition of:
[Assigned to] [Changes] AND
[Assigned to] [is not empty] AND
[Assigned to] [is not] [(blank)] AND
[Window end] [at or after] [Current minute]
With the following script:
(function executeRule(current, previous /*null when async*/ ) {
/* This is a workaround for the "Unassign task" right click refusing to work in some cases */
if (!current.assigned_to.user_name) {
return;
}
/* Check if we have validate_rules_before_assign property turned on and are even using dynamic scheduling. */
var dispatchMethod = new sn_sm.SMConfiguration().getDispatchMethod(current);
var enableRuleValidation = gs.getProperty('work.management.validate_rules_before_assign', 'true') === 'true';
if (dispatchMethod != 'dynamic' || !enableRuleValidation) {
return;
}
/* Check if agent is in the list of auto-assign rules allowed agents */
var allowedAgents = (new sn_fsm_disp_wrkspc.DynamicSchedulingUtil()).getAllowedAgentIdsForTask(current.getValue('sys_id'));
/* Agent validates criteria and is allowed for scheduling: happy path, proceed with scheduling. */
if (allowedAgents.indexOf(current.getValue('assigned_to')) >= 0) {
return;
}
/* Failure scenario: post dynamic scheduling logs as a work note, add warning message and post to logs. */
postLogsAsWorknote();
current.setAbortAction(true);
gs.addErrorMessage(gs.getMessage("Agent {0} does not satisfy the scheduling criteria. Please see work notes for more detailed information.", current.getDisplayValue('assigned_to')));
function postLogsAsWorknote() {
var noShowObj = {
"alwaysShow": false,
"showField": false,
};
var configObj = {
"Location": noShowObj,
"Schedule Status": noShowObj,
"Agent Status": noShowObj,
"Available Parts": noShowObj,
"Matching Skills": noShowObj,
"Distance to Task": noShowObj,
"Groups": noShowObj,
};
(new global.DynamicSchedulingProcessor()).process([current.sys_id], 'wm_task', false, configObj, false);
}
})(current, previous);
This script also implements a system property allowing us to control the behaviour: work.management.validate_rules_before_assign that should be set to true (in case of absence, defaults to true).
We can see that calling the dynamic scheduling engine for posting the work notes is actually NOT the part that checks whether the agent satisfies. For that, we need one more script in the scope FSM Configurable Dispatcher Workspace.
The full reason is that we want to call the OOTB function
var DynamicSchedulingUtil = Class.create();
DynamicSchedulingUtil.prototype = {
initialize: function() {},
getAllowedAgentIdsForTask: function(wotId) {
var noShowObj = {
"alwaysShow": false,
"showField": false,
};
var result = (new sn_fsm_disp_wrkspc.DispatcherWorkspaceAgentRecommendationUtil()).getRecommendedAgentListData(wotId, {
"Location": noShowObj,
"Schedule Status": noShowObj,
"Agent Status": noShowObj,
"Available Parts": noShowObj,
"Matching Skills": noShowObj,
"Distance to Task": noShowObj,
"Groups": noShowObj,
});
if(!result || result.type !== "success") {
return [];
}
return Object.keys(result.recommendedAgents);
},
type: 'DynamicSchedulingUtil'
};
We can now simply pass in a WOT ID and have the system identify the allowed agents using all the correct parameters. Unfortunately we can not use our earlier response object: it returns whether our agent satisfies all matching rules, but not whether they are also available from a working hours perspective. This call, however, does actually return that. If you only want to check whether all dynamic scheduling matching rules validate and do not want to check for work blocks, you can also skip this step and use the response object's information directly.
Putting it all together
In summary, we now have a number of components working together to provide insights in scheduling attempts, both manual and automatic. We have done this without modifying a single OOTB artifact, which means our tech debt is minimal, aside from having a "high code" solution for this.
- An extension of the OOTB DynamicSchedulingProcessor that allows us to intercept the dynamic scheduling response object and do something with it
- A script include FSMUtils that has all the logic for understanding the response object and posting the work notes
-
A script include DynamicSchedulingUtil that wraps around the OOTB
DispatcherWorkspaceAgentRecommendationUtil to identify which agents satisfy all dynamic scheduling criteria and assignment logic - A business rule "Before assign, validate scheduling rules" that is triggered on every scheduling attempt (manual or automatic) and validates the dynamic scheduling criteria before allowing scheduling
 
- 773 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
this is great @Xander H , will this be part of a future product release, or this is the custom solution for now?
do you have something a solution to expose the schedule optimization logs into work notes too?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi @Joshua Chen FX , thanks for your response. We are investigating whether this is something that can be incorporated into the product but for now is custom. If this ends up being roadmapped at some point, I will update this article.
For now I do not have a similar solution for SO, but it is on our list to investigate.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi @Xander H,
I've tried above setup and am running into issues regarding the extension point. When performing an auto assignment I get the feedback: "Invalid Dynamic Scheduling Configuration". While pinpointing the issue, I see that the SI DynamicSchedulingProcessorHelper calls the EP DynamicSchedulingProcessorWithWorkNotes for functions which have not been defined, therefore resulting in a failed configuration. My EP has both the
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi @williamkos, as you have found, the EP needs to be able to expose all other functions of the initial implementation. Therefore you can add all other functions with a call to the next-in-line implementation, something like:
var DynamicSchedulingProcessorWithWorkNotes = Class.create();
DynamicSchedulingProcessorWithWorkNotes.prototype = {
initialize: function() {
},
handles: function(record) {
return true;
},
runDynamicSchedulingProcessor: function(task_ids, task_table, unAssign, configObj, performAssignment, centralDispatch, appointmentBooking, appointmentRangeStart, appointmentRangeEnd, mustAssignedTasks, inputTaskGR, mode) {
var response = (new global.DynamicSchedulingProcessor()).runDynamicSchedulingProcessor(task_ids, task_table, unAssign, configObj, performAssignment, centralDispatch, appointmentBooking, appointmentRangeStart, appointmentRangeEnd, mustAssignedTasks, inputTaskGR, mode);
try {
(new FSMUtils()).createDynamicSchedulingWorkNote(response, performAssignment);
}
catch(e) {
gs.error("FSM exception in dynamic scheduling work note creation: " + e.getMessage());
}
return response;
},
getDynamicSchedulingConfig: function(task, territory){
var extensionPoint = this.getExtensionPoint(task);
return extensionPoint.getDynamicSchedulingConfig(task, territory);
},
getAutoAssignTaskFilters: function(task) {
var extensionPoint = this.getExtensionPoint(task);
return extensionPoint.getAutoAssignTaskFilters(task);
},
getTaskIDsMatchingTaskFilter: function(taskFilterGR, configGR) {
var extensionPoint = this.getExtensionPoint(taskFilterGR.getValue(DynamicSchedulingConstants.TABLE));
return extensionPoint.getTaskIDsMatchingTaskFilter(taskFilterGR, configGR);
},
getSMConfigByDynamicSchedulingConfigRecord: function (tableName, dynamicSchedulingConfig) {
var extensionPoint = this.getExtensionPoint(tableName);
return extensionPoint.getSMConfigByDynamicSchedulingConfigRecord(tableName, dynamicSchedulingConfig);
},
getTaskTable: function(tableName, getBaseTable) {
var extensionPoint = this.getExtensionPoint(tableName);
return extensionPoint.getTaskTable(tableName, getBaseTable);
},
getTaskRecordFromSysClassName: function(taskGR) {
var extensionPoint = this.getExtensionPoint(taskGR);
return extensionPoint.getTaskRecordFromSysClassName(taskGR);
},
getDynamicSchedulingConfigIDForUnassignment: function(taskGR, configID) {
var extensionPoint = this.getExtensionPoint(taskGR);
return extensionPoint.getDynamicSchedulingConfigIDForUnassignment(taskGR, configID);
},
allowOptimizationObjectInitialization: function(task_table) {
var extensionPoint = this.getExtensionPoint(task_table);
return extensionPoint.allowOptimizationObjectInitialization(task_table);
},
/* Retrieves the next extension point in line. */
getExtensionPoint: function(record) {
if (typeof GlideScriptedExtensionPoint == WMConfigurationConstants.UNDEFINED) {
gs.error(gs.getMessage("DynamicSchedulingProcessorExtensionPoint: Scripted extension points are not enabled"));
return null;
}
try {
var ep = new GlideScriptedExtensionPoint().getExtensions(DynamicSchedulingConstants.DYNAMIC_SCHEDULING_PROCESSOR_EXTENSION_POINT);
for (var e = 0; e < ep.length; e++) {
// Skip this one.
if(ep[e].type === this.type) {
continue;
}
if (ep[e].handles == undefined || ep[e].handles(record)) {
ep[e].initialize();
return ep[e];
}
}
} catch (ex) {
gs.error(gs.getMessage('DynamicSchedulingProcessorExtensionPoint: Error fetching extension point ', ex));
}
return null;
},
type: 'DynamicSchedulingProcessorWithWorkNotes'
};
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Hi @Xander H,
Thanks so much! I was already wondering if I had to overwrite every single function. I've had this issue before regarding the CSM/ITSM integration, where the extension point got overwritten for just a single function and on go-live date everything broke down (yes poor testing, but still). This neat trick with the getExtensionPoint function will help me at that customer as well, since for a quick and dirty hotfix I just copied every OOTB function, but now I can resolve it without risking upgrade issues.
The handles function in here is kinda funny, since it always results in true. I guess the addition of the record to the getExtensionPoint function adds little value here, but still good to just stay close to OOTB.
Again, thanks for your help, it works like a charm!