Matching and Compliance Engine
Intro
The 3B Compliance engine is a set of tools that allow system admins to define rules, conditions and restrictions that will apply on shifts that are booked as well as used as part of matching for relevant candidates
Rule Types
When creating a Compliance Rule metadata, you can define the Type (b3s__Type__c) field to either "Scheduling" or "Clock In / Out Component" to designate the compliance rule for use either in the mobile app or in the scheduler. This way, you can have different compliance rules for internal users vs app users.
Look Back and Look Forward Extension
The lookBackExtendDays and lookForwardExtendDays allows us to load define a few days before/and after the start and end field paths. This is useful when creating compliance rules that need to evaluate multi-day hour calculations. Usually these default to the value “1”
Compliance Rules
Compliance rules are Javascript based code rules that evaluate shifts and return evaluation results.
Compliance rules are created as records against the Scheduling Compliance Rule custom metadata.
A compliance rule has the following footprint:
async ({ event, events, contactId, loaders, EvaluationResult }) => {
let results = [];
...
for (const missingCert of missingCerts) {
results.push(
new EvaluationResult({
event: event,
detail: `👎 Missing ${missingCert.b3s__Certificate_Type__r.Name}`,
category: 'Certificates',
points: -50,
isBlocking: true
}),
);
}
return results;
}
As you can see, there are a number of parameters made available to your compliance rule:
- event - this is the contextual event that is being evaluated. Usually a shift, but it can also be an employee request
- events - these are all of the loaded events in memory. This is where the lookBackExtendDays and lookForwardExtendDays come in handy, as you can load more data than displayed on the scheduler
- contactId - this is the contact’s id for evaluation. So, if we are trying to match a shift to a number of candidates, the rule will be re-ran for each candidate and the contactId will be the candidate’s id
- loaders - this will be an array of loader objects that allow you to access additional data. The matchingLoaders property on the shift schedulable is an array where you can define the names of additional loaders
- EvaluationResult - is a class that you must use to return the matching results from the result
Always return an array of EvaluationResult.
As of version 4.0, you can add "isBlocking" property to the EvaluationResult to indicate that this alert should block the scheduler from assigning/inviting the matched candidate or shift.
Compliance Rule Loaders
You can create custom loaders and pass the loader data to compliance rules by specifying the loader name in the matchingLoaders array property on the shift schedulable definition (both for the Scheduler and the Mobile App definition).
Example loader that loads certificates.
{
name: "certificates",
objectName: "b3o__Certificate__c",
filterRecordsBy: function () {
return "b3o__Contact__c IN (SELECT b3o__Candidate__c FROM b3o__Placement__c WHERE b3o__Job__r.b3o__Client_Account__c = {0})"
},
groupRecordsBy: "b3o__Contact__c",
fieldToLoad: ['b3o__Certificate_Type__c', 'b3o__Contact__c', 'b3o__Start_Date__c', 'b3o__End_Date__c', 'b3o__Revoke_Certificate__c'],
filteringItems: function () {
return [
`'${contextRecordId}'`,
]
}
}
Note that for shift search in the mobile app, we recommend that you define a "candidateShifts" loader where you can pull the candidate's other shifts, so you can evaluate worktime regulations. E.g.:
{
name: "candidateShifts",
objectName: "b3s__Shift__c",
filterRecordsBy: function () {
return "b3s__Contact__c = {1}"
},
groupRecordsBy: "Id",
fieldToLoad: ['b3s__Scheduled_Start_Time__c', 'b3s__Scheduled_End_Time__c', 'b3s__Contact__c'],
filteringItems: function () {
return [
`'${contextRecordId}'`,
`'${contactUserId}'`
]
}
}
Compliance Rule EvaluationResult
EvaluationResult is an object that you can use to return structured compliance/evaluation results. The following properties need to be passed:
- event/events - an array or a single instance of a calendar event. We will create the Event Alert records against the event/events passed in this property
- detail - longer description
- category - grouping category
- points - positive/negative whole integer points assignment for evaluation result
return [
new EvaluationResult({
events: allWorkerShifts,
detail: `⚠️ Too many booked hours today (${totalDailyHours})`,
category: 'Working Time',
points: -30,
isBlocker: true
}),
];
Version 2.3 of the WFM package adds the "isBlocker" property that can be used to indicate if the Evaluation Result should have a blocking behaviour, preventing schedulers from booking/inviting the matched candidate.
Complete Examples
async ({ event, events, contactId, loaders, EvaluationResult }) => {
if (!contactId) {
console.warn('No contact id for evaluation');
return [];
}
const certificatesLoader = loaders.find((l) => l.name === 'certificates');
const contactCertificates = certificatesLoader?.loader?.grouped[contactId];
const certRequirementsLoader = loaders.find((l) => l.name === 'certRequirements');
const certRequirements = certRequirementsLoader?.loader.records;
const eventAccountId = event.extendedProps.record?.b3s__Job__r?.b3o__Client_Account__c;
const eventJobTypeId = event.extendedProps.record?.b3s__Job__r?.b3o__Job_Type__c;
const eventSiteId = event.extendedProps.record?.b3s__Site__c;
const accountCertRequirements = certRequirements.filter(
(req) => req.b3s__Account__c != null && req.b3s__Account__c === eventAccountId,
);
const jobTypeCertRequirements = certRequirements.filter(
(req) => req.b3s__Job_Type__c != null && req.b3s__Job_Type__c === eventJobTypeId,
);
const siteCertRequirements = certRequirements.filter(
(req) => req.b3s__Site__c != null && req.b3s__Site__c === eventSiteId,
);
const getMissingCertificates = function (certificates = [], certRequirements = []) {
const contactCertificateTypes = new Set(certificates.map((item) => item.b3o__Certificate_Type__c));
const missingCerts = certRequirements.filter(
(certReq) => !contactCertificateTypes.has(certReq.b3s__Certificate_Type__c),
);
return missingCerts;
};
const getValidCertificates = function (certificates = [], certRequirements = []) {
const contactCertificateTypes = new Set(certificates.map((item) => item.b3o__Certificate_Type__c));
const missingCerts = certRequirements.filter((certReq) =>
contactCertificateTypes.has(certReq.b3s__Certificate_Type__c),
);
return missingCerts;
};
const missingCerts = getMissingCertificates(contactCertificates, [...accountCertRequirements, ...jobTypeCertRequirements, ...siteCertRequirements]);
let results = [];
for (const missingCert of missingCerts) {
results.push(
new EvaluationResult({
event: event,
detail: `👎 Missing ${missingCert.b3s__Certificate_Type__r.Name}`,
category: 'Certificates',
points: -50,
isBlocking: true
}),
);
}
const validCerts = getValidCertificates(contactCertificates, [...accountCertRequirements, ...jobTypeCertRequirements, ...siteCertRequirements]);
for (const validCert of validCerts) {
results.push(
new EvaluationResult({
event: event,
detail: `👍 Has ${validCert.b3s__Certificate_Type__r.Name}`,
category: 'Certificates',
points: 25,
}),
);
}
return results;
}]
Invitations
The invitations schema is used to determine how Shift Invites are interpreted within the scheduler. The below code sample is the default state that can be overridden, by adding an “invitations” object to the “shift” schedulable within the Scheduler Definition. The schema allows admins to define the object that will be used for managing invitations as well as the following special properties:
- canBook - this is a selector which determines when the “Book” button appears against an active invitation. By default, the Book button is visible only for Accepted invitations. You can modify this behaviour as necessary
{
"schedulables": {
//...
"shift":{
//...
"invitations": {
"objectName": "b3s__Invitation__c",
"contactField": "b3s__Contact__c",
"contactRecordReference": "b3s__Contact__r",
"shiftField": "b3s__Shift__c",
"expiresField": "b3s__Expires__c",
"statusField": "b3s__Status__c",
"createdDateField": "CreatedDate",
"canBook": {
"condition": {
"allOrSome": "some",
"selectors": [
{
"field": "b3s__Status__c",
"operator": "equals",
"value": "Accepted",
},
],
},
},
"title": {
"contactName": "Name",
"contactTitle": "Title",
"contactDescription": "Description",
},
"recordHistoryData": [
{
"dateFieldPath":"CreatedDate",
"label":"Invited",
"isComplete":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"CreatedDate",
"operator":"isnull",
"value":false
}
]
}
}
},
{
"dateFieldPath":"LastModifiedDate",
"label":"Accepted",
"isSuccess":true,
"isComplete":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Accepted"
}
]
}
},
"isVisble":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Accepted"
}
]
}
}
},
{
"dateFieldPath":"LastModifiedDate",
"label":"Revoked",
"isError":true,
"isComplete":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Revoked"
}
]
}
},
"isVisble":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Revoked"
}
]
}
}
},
{
"dateFieldPath":"LastModifiedDate",
"label":"Rejected",
"isError":true,
"isComplete":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Rejected"
}
]
}
},
"isVisble":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Rejected"
}
]
}
}
},
{
"dateFieldPath":"LastModifiedDate",
"label":"Booked",
"isSuccess":true,
"isComplete":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Booked"
}
]
}
},
"isVisble":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"Booked"
}
]
}
}
},
{
"dateFieldPath":"b3s__Expires__c",
"label":"Expires",
"isComplete":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Expires__c",
"operator":"isnull",
"value":false
}
]
}
},
"isVisble":{
"condition":{
"allOrSome":"all",
"selectors":[
{
"field":"b3s__Expires__c",
"operator":"isnull",
"value":false
},
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"New"
}
]
}
}
}
]
},
}
}
}
When Compliance Runs?
The compliance engine executes whenever a shift/request change is made on the scheduler. Users can also manually re-calculate compliance by using the button on the right side pop-down bar.
Search and Match Shifts
Users can initiate shift matching from the “Match” context menu option. This menu option is conditionally rendered based on the canMatch conditional property.
Once the user selects on the “Match” option, they will see a pop-up modal with a list of results of potential candidate matches.
Each candidate is ordered based on merit and a Relative % score is provided. Please note that the relative % score doesn’t mean that someone is fully qualified, but it measures the candidate’s score compared to all other candidates.
Each candidate will be presented with their Name, Description and a list of EvaluationResults, grouped by Category with total points per category. Any positive points will be in green and any negative points will be in red.
A user can then invite one or a number of candidates by clicking on the top right-hand corner of the candidate card.
The candidate will then be able to see any invitations in the mobile app, and interact with them to accept or reject the invitations.
The selectionClause on the invitation schedulable in the Clock In/Out definition controls which invitations are visible, so you can for example hide invitations that are expired, revoked, invalid.
When the candidate accepts the invitation (Status = Accepted), the book button will be made available on the invitation card.
You can control when the Book button is displayed based on the canBook property (search this document).
If the user chooses to Book the candidate, the shift will be assigned to the selected candidate, completing the flow.
Tips:
- Use 3B Push Notifications to push shift alerts to candidates using Flows in addition to Emails.
- If you need to implement a F3 process (fastest finger first), you can use a Flow to auto-assign the shift to the Invitation’s Contact, completing the workflow, without requiring that a user approves the acceptance
- Note, you might need to also develop a Flow that will block anyone else from accepting an invitation to a shift that is already booked
Scheduling Context Provider Invitations Schedulable
Version 4.0 of the app introduces a new requirement to define a new "invitation" schedulable within the Scheduling Context Provider. The content of the scheduler is defaulted with these values:
{
"objectType":"b3s__Invitation__c",
"contactFieldPath":"b3s__Contact__c",
"contactRecordReference":"b3s__Contact__r",
"shiftFieldPath":"b3s__Shift__c",
"statusFieldPath":"b3s__Status__c",
"contactsLoader":"contacts",
"allowSelectingThreshold":25,
"allowBookingThreshold":25,
"contactTitle":[
{
"label":"",
"fieldPath":"Name",
"targetRecordIdPath":"Id",
"formatter":"string",
"hideIfNull":true
},
{
"label":"Title",
"fieldPath":"Title",
"formatter":"string",
"hideIfNull":true
}
],
"invitationTitle":[
{
"label":"",
"fieldPath":"Name",
"targetRecordIdPath":"Id",
"formatter":"string",
"hideIfNull":false
},
{
"label":"Status",
"fieldPath":"b3s__Status__c",
"formatter":"string",
"hideIfNull":false
},
{
"label":"Contact",
"fieldPath":"b3s__Contact__r.Name",
"targetRecordIdPath":"b3s__Contact__c",
"formatter":"string",
"hideIfNull":false
},
{
"label":"Shift",
"fieldPath":"b3s__Shift__r.Name",
"targetRecordIdPath":"b3s__Shift__c",
"formatter":"string",
"hideIfNull":false
},
{
"label":"Seen",
"fieldPath":"b3s__Seen_Timestamp__c",
"formatter":"dateTime",
"hideIfNull":false
},
{
"label":"Created",
"fieldPath":"CreatedDate",
"formatter":"dateTime",
"hideIfNull":false
}
],
"shiftTitle":[
{
"label":"",
"fieldPath":"Name",
"targetRecordIdPath":"Id",
"formatter":"string",
"hideIfNull":false
},
{
"label":"Start",
"fieldPath":"b3s__Absolute_Start_Time__c",
"formatter":"time",
"hideIfNull":false
},
{
"label":"End",
"fieldPath":"b3s__Absolute_En_Time__c",
"formatter":"time",
"hideIfNull":false
},
{
"label":"Status",
"fieldPath":"b3s__Status__c",
"formatter":"string",
"hideIfNull":true
},
{
"label":"Job",
"fieldPath":"b3s__Job__r.Name",
"formatter":"string",
"hideIfNull":true
},
{
"label":"Site",
"fieldPath":"b3s__Site__r.Name",
"formatter":"string",
"hideIfNull":true
}
],
"contactFilters":[
{
"label":"Contact",
"type":"reference",
"value":{
"labelField":"Name",
"valueField":"Id"
}
}
],
"shiftFilters":[
{
"label":"Status",
"type":"select",
"value":{
"valueField":"b3s__Status__c",
"options":[
{
"label":"New",
"value":"New"
},
{
"label":"Confirmed",
"value":"Confirmed"
},
{
"label":"Cancelled",
"value":"Cancelled"
}
]
}
}
],
"canAssign":{
"condition":{
"allOrSome":"some",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"New"
}
]
}
},
"canSelect":{
"condition":{
"allOrSome":"some",
"selectors":[
{
"field":"b3s__Status__c",
"operator":"equals",
"value":"New"
}
]
}
},
"canCancelInvite":null,
"canAssignInvite":null
}
The important attributes are as follows:
- contactsLoader - the name of a scheduling loader responsible for loading contacts for matching
- allowSelectingThreshold - set this to an integer between 0 and 100 to limit which match results can be selected by the user for invitation. We will compare this value against the relative match %
- allowBookingThreshold - set this to an integer between 0 and 100 to limit which match results can be assigned.We will compare this value against the relative match %
- contactTitle - an array of fields starting from the Contact object. We will use this when displaying the matched contact. The fields are loaded through the records retreived from the defined contactsLoader
- invitationTitle - an array of fields starting from the Invitation object. We will use this to render the fields on the "already invited" section. The fields are automatically loaded (extracted from the definition itself)
- shiftTitle - an array of fields starting from the Shift object. The fields will be used for rendering shift match result.
- contactFilters - an array of filters that will be used for showing a filter on the matched results when doing candidate search
- shiftFilters - an array of filters that will be used for showing a filter on the matched results when doing shift search
- canAssign - a conditional with the context of the Shift. You can prevent users from selecting a match result using this. E.g. you can prevent shift match result from being selected if the shift's Status is "New"
- canSelect - a conditional with the context of the Shift. You can prevent users from selecting a match result using this.
- canCancelInvite - a conditional with the context of the Invitation. You can prevent users from cancelling an invite using this.
- canAssignInvite - a conditional with the context of the Invitation. You can prevent users from assigning an invite using this.
Compliance on Shifts
Compliance on shifts is displayed as an indicator on top of the shift. If a user places their mouse over the indicator, they can see the full list of results. The counter on the shift is only for the negative point evaluation results.
Suitabilities
Candidate suitabilities are used to link Candidates to Account, Site or Job Type in order to teach the system if that candidate has a positive or negative preference to the specific Account, Site or Job Type. So, for example, if a client has a positive impression of a candidate, system users can create a Suitability record that links the candidate and the client account with an Excellent rating. This will have a positive impact on the score the candidate will get when we search for a suitable candidate for a shift.
Likewise, a negative rating will have a negative impact on the candidate in any search and match executions.
Developers can extend this behaviour by adding custom lookups and modifying the custom rule that evaluates the candidate.
Certificate Requirements
Certificate Requirements link Certificates to Account, Job Type or Site in order to instruct the system which certificates are required for a shift. So, if a Shift is linked to a Job that is for a “Java Developer” in the site “IBM Office”, you can create two Certificate Requirement records that link the “Java Development Experience” certificate to the Job Type “Java Developer” and the “IBM Office Induction” to the Site IBM Office.
This setup will ensure that when we have a shift for evaluation/compliance, we will get consistent results of potential candidates / compliance results that are contextual and users don’t need to manually specify what certificates are required, as the system will use the implicit relationship between the certificates and the Job Type/Account/Site.
Similar to Suitabilities, developers can extend this behaviour by adding custom lookups and modifying the underlying code rules to link to an ATS, custom object model or even modify points assignment.
Matching Shifts to a Candidate
Version 2.3 of the WFM package introduces the ability to match Shifts to a Candidate. Simply right click on a cell against a candidate's row in swimlane view and the system will do a reverse match. No additional configuration is required.