WFM Object Structure Introduced in 2GP
Implementor guide to the objects introduced when the WFM scheduling app was decoupled from the 3B Onboarding package, and how they drive scheduling and compliance.
Compatibility. Existing clients on the 3B Onboarding objects may keep using them indefinitely. All new implementations must use the native objects documented here. The two models are wired in identically — only the loader
objectName/filterRecordsByand the field paths in context providers, loaders and compliance rules differ. Swapping models is a configuration change in Custom Metadata, not a code change.
All API names are shown without the b3s__ namespace prefix for readability; they all carry it in the org.
1. The objects
| Object | Role | Key fields | Parents |
|---|---|---|---|
| Cost_Center__c | Cost grouping under an account | — | Account__c (Master-Detail)
|
| Job_Group__c | Category / family of jobs (e.g. "Nursing", "Security") | Name only
|
— |
| Job__c | A role to be staffed | — | Account__c, Job_Group__c
|
| Site__c | Physical workplace; carries the address, geocode and timezone | Street/City/Province_State/Postal_Code/Country, Location__c (geo), Timezone__c, Site_Instructions__c
|
Account__c
|
| Site_Location__c | Sub-area within a site (ward, gate, floor) | Name only
|
Site__c (Master-Detail)
|
| Ticket_Type__c | Kind of credential / competency (e.g. "First Aid", "SIA Licence") | Name only
|
— |
| Ticket__c | A credential held by a worker, with a validity window | Valid_From__c, Valid_Until__c (Date)
|
Contact__c (MD), Ticket_Type__c (MD)
|
| Certificate_Requirement__c | Declares which Ticket_Type is needed for work matching a set of criteria, and the scoring weight
|
Priority__c (1–5), Max_Award__c, Max_Penalty__c, Ticket_Type__c
|
criteria lookups: Account__c, Cost_Center__c, Job_Group__c, Job__c, Work_Site__c, Work_Location__c
|
| Working_Pattern_Type__c | Reusable shift-pattern template (e.g. "4-on-4-off", "Mon–Fri 9-5") | — | criteria lookups: Work_Job__c, Work_Site__c, Work_Site_Location__c
|
| Working_Pattern_Day__c | One day-slot inside a template | Week_Number__c, Day_Number__c, Start_Time__c, End_Time__c (Time)
|
Working_Pattern_Type__c (MD)
|
| Working_Pattern__c | Assigns a pattern to a worker for a date range ("placement") | Start__c, End__c (Date)
|
Contact__c (MD), Job__c, Working_Pattern_Type__c
|
| Shift_Task__c | A sub-task within a shift | Start__c, End__c (DateTime)
|
Shift__c (MD), Work_Job__c, Work_Site__c
|
How they relate to a Shift
The Shift__c record is the spine. The new model adds these "Work_" links to it (replacing the old Onboarding lookups):
Work_Job__c→Job__c(and through it,Job_Group__candAccount__c)Work_Site__c→Site__c(and through it,Timezone__c+ geocode)Work_Site_Location__c→Site_Location__cContact__c→ the assigned worker (null = open shift)Shift_Demand__c+Required_Staff__c→ demand parent + headcount
A shift therefore answers: who (Contact), what role (Work_Job → Job_Group), where (Work_Site → Site_Location), and for whom (Account via the job). Those five dimensions — Account · Job Group · Job · Site · Site Location — are the universal matching key used by Certificate Requirements, Suitabilities and Working Pattern Types.
2. The two-axis design: requirements vs. holdings
The model deliberately separates what work needs from what a worker has, on both the credential and preference axes:
| Axis | "Requirement" side (attached to work criteria) | "Holding" side (attached to a Contact) |
|---|---|---|
| Credentials | Certificate_Requirement__c → Ticket_Type__c
|
Ticket__c → Ticket_Type__c (+ validity dates)
|
| Preference / skill | Suitability__c (rating per criteria)
|
— (Suitability is contact-scoped already) |
| Schedule shape | Working_Pattern_Type__c + _Day__c (template)
|
Working_Pattern__c (worker × date range)
|
Compliance matches the two sides at evaluation time by the shared Account/Job Group/Job/Site/Location key.
Criteria matching semantics (important)
Certificate_Requirement__c and Suitability__c both use the same "populated criteria must all match" rule (see the compliance code in Scheduler_Tickets / Scheduler_Suitabilities):
- Only the lookup fields that are populated on the requirement are tested.
- Every populated field must equal the shift's corresponding value, or the requirement is skipped (
return false). - At least one criterion must be populated and matched (
evaluatedCriteriaCount > 0 && matches).
So a Certificate Requirement with only Job_Group__c set applies to all shifts in that job group; one with Job_Group__c + Work_Site__c applies only where both match. This is how you scope a requirement from broad (account-wide) to narrow (one site, one job).
3. How the data reaches the scheduler
Three Custom Metadata types wire these objects into the running app. They are unmanaged and live in force-app/unpackaged/customMetadata/.
3.1 Scheduling_Loader__mdt — fetch & shape the data
Each loader is a JSON spec telling the dataLoadingEngine what to query and how to index it. The Object_Name__c field scopes the loader to a context (Account, Contact, Global, or staged sets Load_1000 / Load_2000).
{
"name": "certificates", // referenced by compliance rules via loaders.find(l => l.name === ...)
"objectName": "b3s__Ticket__c",
"filterRecordsBy": ["b3s__Contact__c =:contextId"],
"groupRecordsBy": "b3s__Contact__c", // builds loader.grouped[key] = [records]
"fieldToLoad": ["Name","b3s__Ticket_Type__c","b3s__Valid_From__c","b3s__Valid_Until__c", ...]
}
The loaders that feed the new model (named by their name key):
name
|
Object | Grouped by | Consumed by |
|---|---|---|---|
sites
|
Site__c
|
Id
|
Distance rule (geocode), shift timezone |
jobs
|
Job__c
|
Id
|
Job/Job-Group resolution |
certificates
|
Ticket__c
|
Contact__c
|
Tickets rule (worker holdings) |
certRequirements
|
Certificate_Requirement__c
|
(records)
|
Tickets rule (requirements) |
suitability
|
Suitability__c
|
Contact__c
|
Suitabilities rule |
workingPatterns
|
Working_Pattern__c
|
Id
|
Placement rendering |
contacts
|
Contact
|
— | Distance rule (MailingLatitude/Longitude), matching
|
Each loader exists in
Account_*,Contact_*,Global_*and stagedLoad_1000_*/Load_2000_*variants. They differ only infilterRecordsBy(=:contextIdagainst Account vs Contact vs unfiltered) — pick the set that matches your context provider's sharing scope.
3.2 Scheduling_Context_Provider__mdt — app definition
The big JSON (JSON__c) per object/sharing context. Defines the schedulables map — how each source object becomes a calendar entity:
| Schedulable | objectType | Notes |
|---|---|---|
shift
|
Shift__c
|
the assignable event; declares matchingLoaders, demand fields, Work_* field paths, timezone via Work_Site__r.Timezone__c
|
placement
|
Working_Pattern__c
|
renders a worker's assigned pattern as a background span |
task
|
Shift_Task__c
|
sub-tasks on a shift |
request
|
Employee_Request__c
|
availability (Available / Unavailable / Flexible) |
break
|
Break__c
|
|
invitation
|
Invitation__c
|
The shift schedulable's matchingLoaders lists exactly the loaders the matching/compliance engine hydrates before scoring: ["contacts","certificates","certRequirements","suitability","sites"].
Group__c / Group_Assignment__c (Sharing Groups) decide which context provider(s) apply for a given record/user — see CLAUDE.md.
3.3 Scheduling_Filter__mdt — UI facets
Client-side filter definitions over the loaded events. The new-model filters key off the Work_* paths:
Shift_Job_Filter→b3s__Work_Job__cShift_Job_Group_Filter→b3s__Work_Job__r.b3s__Job_Group__cShift_Site_Filter→b3s__Work_Site__cResource_Filter→ resource (worker) id, plus an "Unassigned" bucket
4. Compliance & matching (Scheduling_Compliance_Rule__mdt)
Each rule is an async ({ event, events, contactId, loaders, matchingModifiers, EvaluationResult }) => EvaluationResult[] function. Rules read worker data out of loaders.find(l => l.name === ...).loader.grouped[contactId] and the shift's attributes out of event.extendedProps.record. Each returned EvaluationResult carries points (positive = award, negative = penalty), a category, a detail string, and optional isBlocking: true for hard stops. Scores are summed to rank candidates; blocking results prevent assignment.
The six shipped rules and the objects they use:
| Rule | Uses | Logic summary |
|---|---|---|
| Scheduler_Tickets | Certificate_Requirement__c (req) × Ticket__c (held)
|
For each requirement matching the shift's criteria, check the worker holds a Ticket of that Ticket_Type whose Valid_From/Until covers the shift. Missing/expired → penalty scaled by Priority (1=100%…5=20%) and Max_Penalty; valid → award scaled by Max_Award. Null validity date = indefinite.
|
| Scheduler_Suitabilities | Suitability__c
|
For suitabilities matching the shift criteria, award/penalise by Rating__c (Excellent/Good/Bad/Terrible) bounded by Max_Award/Max_Penalty.
|
| Scheduler_Distance | Site__c geocode + Contact mailing geocode
|
Haversine distance worker↔site. Linear score +50 (≤5 km) → −50 (≥100 km); estimates travel time. Max Distance search modifier hard-blocks beyond N miles.
|
| Scheduler_Breaks | Shift__c (other shifts)
|
Requires ≥8 h rest before/after each of the worker's other shifts; <8 h → blocking penalty, else +15 award. |
| Scheduler_DoubleBooking | Shift__c (other shifts)
|
Interval-overlap against the worker's other confirmed shifts → blocking conflict. |
| Scheduler_Availability | Employee_Request__c
|
Overlap with worker availability: Unavailable → blocking; Available → +25; Flexible → notice. |
Only Scheduler_Tickets, Scheduler_Suitabilities and Scheduler_Distance depend on the new objects; the other three are object-agnostic (shift-vs-shift and request-vs-shift) and work unchanged in either model.
Demands are not compliance-checked. A demand creates child shifts on assignment (
Shift_Demand__c+Required_Staff__c); only the resulting single-contact shifts run through the rules above. (CLAUDE.md, scheduler.)
5. Working patterns in detail
Templates are normalised so one shape serves many workers:
Working_Pattern_Type__c ──< Working_Pattern_Day__c (Week_Number, Day_Number, Start_Time, End_Time)
│ (Work_Job / Work_Site / Work_Site_Location defaults)
└──< Working_Pattern__c (Contact + Job + Start..End date range) → rendered as "placement"
- A Type = the recurring shape (which week, which weekday, what hours), plus default Work Job/Site/Location.
- A Working_Pattern instance binds that Type to one
Contact+Jobover aStart__c–End__cdate range, and shows on the scheduler as aplacementbackground.
Weekday/DST note. Internal weekday indexing is Mon=0…Sun=6, and re-anchoring a pattern's hours onto calendar dates must be done midnight-up per target day (never
+n*86400000) to stay DST-safe. See the "Weekday indexing" and "Re-anchoring" sections of CLAUDE.md before writing any pattern-expansion code.
6. Implementor checklist for a new (native-model) deployment
- Reference data: create
Job_Group__c→Job__c,Site__c(+Site_Location__c),Ticket_Type__c, optionalCost_Center__c. - Workers' holdings:
Ticket__cper worker/credential with validity dates;Suitability__cratings;Working_Pattern__cassignments. - Requirements:
Certificate_Requirement__crecords scoped via the Account/Job Group/Job/Site/Location lookups, withPriority,Max_Award,Max_Penalty. - Shifts: populate
Work_Job__c,Work_Site__c,Work_Site_Location__c(andContact__cwhen assigned). The job'sAccount+Job_Groupderive automatically. - Config: use the native
Scheduling_Loader__mdtset (*_Sites,*_Jobs,*_Certificates/*_CertificateRequirements,*_Suitabilities,*_Working_Patterns) and a context provider whoseschedulablespoint atb3s__Shift__c/b3s__Working_Pattern__cetc. Attach viaGroup__c/Group_Assignment__c. - Rules: enable the six
Scheduling_Compliance_Rule__mdtrecords; they read the loaders above byname.
To migrate an Onboarding-model org, you only re-point the loaders' objectName/filterRecordsBy/fieldToLoad and the context-provider / compliance field paths at these objects — no app code changes.