Server-side
var AssignmentRuleEngine = Class.create();
AssignmentRuleEngine.prototype = {
initialize: function() {
this.MAX_WORKLOAD = 10; // Maximum active assignments per user
this.ASSIGNMENT_TIMEOUT = 30; // Minutes before escalation
},
assignRecord: function(recordGr, assignmentType) {
// Try assignment strategies in order of preference
var assignedTo = this._trySkillsBasedAssignment(recordGr) ||
this._tryRoundRobinAssignment(recordGr) ||
this._tryFallbackAssignment(recordGr);
if (assignedTo) {
recordGr.setValue('assigned_to', assignedTo);
recordGr.setValue('assignment_group', this._getUserGroup(assignedTo));
recordGr.setValue('u_assignment_method', assignmentType);
recordGr.setValue('u_assigned_date', new GlideDateTime());
this._logAssignment(recordGr, assignedTo, assignmentType);
return true;
}
// No assignment possible, escalate
this._escalateUnassigned(recordGr);
return false;
},
_trySkillsBasedAssignment: function(recordGr) {
var category = recordGr.getValue('category');
var subcategory = recordGr.getValue('subcategory');
var priority = recordGr.getValue('priority');
// Query for users with required skills and availability
var userGr = new GlideRecord('sys_user');
userGr.addActiveQuery();
userGr.addQuery('u_available_for_assignment', true);
// Join to skills table to find matching expertise
userGr.addJoinQuery('u_user_skills', 'sys_id', 'user')
.addQuery('skill.name', category);
userGr.query();
var candidates = [];
while (userGr.next()) {
var workload = this._getCurrentWorkload(userGr.getUniqueValue());
if (workload < this.MAX_WORKLOAD) {
candidates.push({
userId: userGr.getUniqueValue(),
workload: workload,
skillLevel: this._getSkillLevel(userGr.getUniqueValue(), category)
});
}
}
if (candidates.length > 0) {
// Sort by skill level descending, then workload ascending
candidates.sort(function(a, b) {
if (a.skillLevel !== b.skillLevel) {
return b.skillLevel - a.skillLevel;
}
return a.workload - b.workload;
});
return candidates[0].userId;
}
return null;
},
_tryRoundRobinAssignment: function(recordGr) {
var groupId = this._getTargetGroup(recordGr);
if (!groupId) return null;
// Get group members ordered by last assignment date
var userGr = new GlideRecord('sys_user_grmember');
userGr.addQuery('group', groupId);
userGr.addQuery('user.active', true);
userGr.addQuery('user.u_available_for_assignment', true);
userGr.query();
var candidates = [];
while (userGr.next()) {
var userId = userGr.user.sys_id.toString();
var workload = this._getCurrentWorkload(userId);
var lastAssigned = this._getLastAssignmentDate(userId);
if (workload < this.MAX_WORKLOAD) {
candidates.push({
userId: userId,
lastAssigned: lastAssigned,
workload: workload
});
}
}
if (candidates.length > 0) {
// Sort by last assignment date (oldest first), then by workload
candidates.sort(function(a, b) {
var dateCompare = new GlideDateTime(a.lastAssigned).compareTo(new GlideDateTime(b.lastAssigned));
if (dateCompare !== 0) return dateCompare;
return a.workload - b.workload;
});
return candidates[0].userId;
}
return null;
},
_tryFallbackAssignment: function(recordGr) {
// Assign to group manager or designated backup
var groupId = this._getTargetGroup(recordGr);
if (!groupId) return null;
var groupGr = new GlideRecord('sys_user_group');
if (groupGr.get(groupId) && groupGr.manager) {
var managerId = groupGr.manager.toString();
if (this._isUserAvailable(managerId)) {
return managerId;
}
}
return null;
},
_getCurrentWorkload: function(userId) {
var incidentGr = new GlideAggregate('incident');
incidentGr.addQuery('assigned_to', userId);
incidentGr.addQuery('state', 'NOT IN', '6,7,8'); // Not resolved/closed/cancelled
incidentGr.addAggregate('COUNT');
incidentGr.query();
if (incidentGr.next()) {
return parseInt(incidentGr.getAggregate('COUNT')) || 0;
}
return 0;
},
_escalateUnassigned: function(recordGr) {
recordGr.setValue('u_assignment_failed', true);
recordGr.setValue('u_escalation_date', new GlideDateTime());
// Create assignment failure event for notifications
gs.eventQueue('assignment.failed', recordGr, recordGr.getUniqueValue(), 'Assignment rules could not find suitable assignee');
gs.log('Assignment failed for record: ' + recordGr.getUniqueValue(), 'AssignmentRuleEngine');
},
_getTargetGroup: function(recordGr) {
// Logic to determine target group based on record attributes
var category = recordGr.getValue('category');
var mappingGr = new GlideRecord('u_category_group_mapping');
mappingGr.addQuery('category', category);
mappingGr.query();
if (mappingGr.next()) {
return mappingGr.group.toString();
}
return null;
},
_logAssignment: function(recordGr, assignedTo, method) {
var logGr = new GlideRecord('u_assignment_log');
logGr.initialize();
logGr.setValue('record_id', recordGr.getUniqueValue());
logGr.setValue('record_table', recordGr.getTableName());
logGr.setValue('assigned_to', assignedTo);
logGr.setValue('assignment_method', method);
logGr.setValue('assigned_date', new GlideDateTime());
logGr.insert();
},
type: 'AssignmentRuleEngine'
};
// Business Rule implementation (onChange, after, Incident table)
(function executeRule(current, previous) {
// Only run on insert or when assignment_group changes
if (!current.isNewRecord() && !current.assignment_group.changes()) {
return;
}
// Don't auto-assign if already assigned
if (!gs.nil(current.assigned_to)) {
return;
}
var assignmentEngine = new AssignmentRuleEngine();
assignmentEngine.assignRecord(current, 'automatic');
})(current, previous);
The code implements a hierarchical assignment engine that tries skills-based matching first, then round-robin distribution, and finally fallback assignment to managers. The engine tracks workload limits, maintains assignment history in a custom log table, and includes escalation handling for unassigned records. The Business Rule triggers the assignment automatically when records are created or when assignment groups change.