I started with the first feature in this project: Project Staffing Assistant.
Project Staffing Assistant helps managers decide which candidates are suitable for a project based on actual project requirements.
I began with the backend, building the intelligence layer in Apex.
The Core Service – SkillEvaluatorService
public with sharing class SkillEvaluatorService
Two important design decisions:
public → Required because LWC will call this Apex class
with sharing → Ensures record-level security is respected
I had previously configured roles, OWD, and sharing rules (Refer here).
Using with sharing ensures this evaluation logic follows those configurations.
Apex Sharing Behavior:
Apex runs in system context by default. Object-level and field-level permissions are not automatically enforced.
with sharing enforces record-level sharing rules only, ensuring queries and DML respect the current user’s access.
with sharing does not enforce object or field permissions. You must explicitly handle CRUD/FLS (e.g., WITH SECURITY_ENFORCED or Security.stripInaccessible()).
If no sharing keyword is defined, the class inherits sharing from its caller, so behavior may vary depending on depending on how it is invoked.
Triggers run in system context. Even if a helper class is marked with sharing, the trigger executes in system mode.
Designing Data Transfer Objects
Instead of returning raw Employee__c or Employee_Skill__c records, I created Data Transfer Objects or DTOs.
DTOs define the structured connection between backend and UI. They wrap only the fields required by the frontend, preventing unnecessary exposure of internal data.
For this feature, the UI needed:
Detailed skill gap information (for manager-level decision making)
Candidate-level summary information
Note: @AuraEnabled is required for LWC(UI) to access Apex properties and methods.
Skill-Level DTO
public class SkillGapDetail {
@AuraEnabled public String skillName;
@AuraEnabled public Integer requiredLevel;
@AuraEnabled public Integer impact;
}
Represents a single skill gap for a candidate.
Advantages:
All evaluation logic runs in Apex, so UI performs no calculations
Business logic stays in the backend
UI remains lightweight
Future logic changes don’t affect frontend code
Candidate-Level DTO
public class CandidateResult {
@AuraEnabled public String employeeName;
@AuraEnabled public Decimal gapScore;
@AuraEnabled public Boolean isProjectReady;
@AuraEnabled public SkillGapDetail detail;
}
For each evaluated employee, the UI receives:
Employee name
Final gap score
Ready / Not Ready flag
Skill Gap Detail
This keeps the response clean and structured.
Entry Point – evaluateProject()
@AuraEnabled
public static List<CandidateResult> evaluateProject(Id projectId, Integer topN)
Responsibilities:
Accept a Project Id
Evaluate unallocated employees
Rank them
Return top N candidates
Persist evaluation results
Guard Clause
- Guard clauses help prevent unnecessary processing and avoid unexpected or confusing UI behavior.
if (projectId == null) return new List<CandidateResult>();
If no project is provided, evaluation stops.
Prevents:
Null pointer exceptions
Unexpected UI errors
Wasted governor limits
Load Project Requirements
List<Project_Skill_Requirement__c> reqs = [
SELECT Skill__c, Required_Level__c,
Importance__c, Weight__c
FROM Project_Skill_Requirement__c
WHERE Project__c = :projectId
];
Each requirement contains:
Skill
Required Level
Importance (Required / Nice-to-have)
Weight
After fetching, I converted them into Maps for fast access.
Why Maps?
Governor limits restrict queries per transaction. Querying inside loops risks hitting limits. By storing data in Maps:
Avoid repeated SOQL calls
Ensure constant-time lookups (O(1))
Keep code bulk-safe
Maps are essential in Apex for this reason.
Weighted Impact Formula
This is the heart of the evaluation engine.
I first compute the deficit to rank candidates:
deficit = requiredLevel - employeeLevel;
By itself, this treats all skills equally. To make evaluations more realistic, I introduced weighted scoring:
Integer impact = deficit * importanceMultiplier * weight;
Where:
Required skill → multiplier = 2
Nice-to-have → multiplier = 1
Weight → configurable per skill
From this I ensured,
Missing a critical skill has higher impact
Minor skills don’t disproportionately penalize a candidate
The result is a system that is realistic and flexible rather than rigid.
Effective Level – Making It Smarter
Raw skill levels aren’t always reliable. To improve accuracy, I introduced two adjustments:
- Confidence adjustment
- Staleness adjustment
1. Confidence Adjustment
Boolean isTrusted = (src == 'Manager-assessed');
Integer confidenceAdjust = isTrusted ? 0 : 1;
Integer afterConfidence = rawLevel - confidenceAdjust;
- Self-assessed → reduce slightly
- Manager-assessed → keep unchanged
2. Staleness Adjustment
Date staleCutoff = Date.today().addMonths(-12);
if (lastVerified == null) {
stalenessAdjust = 2;
} else if (lastVerified <= staleCutoff) {
stalenessAdjust = 1;
}
Never verified → larger reduction
Verified >12 months ago → slight reduction
Finally, the effective level is computed as:
Integer effectiveLevel = afterConfidence - stalenessAdjust;
if (effectiveLevel < 0) effectiveLevel = 0;
This makes the evaluation time and credibility aware, preventing outdated or inflated skill ratings from misleading staffing decisions.
Ranking Candidates
results.sort(new CandidateComparator());
Custom comparator:
private class CandidateComparator implements System.Comparator<CandidateResult> {
public Integer compare(CandidateResult x, CandidateResult y) {
if (x.gapScore != y.gapScore) {
return (x.gapScore < y.gapScore) ? -1 : 1;
}
return x.employeeName.toLowerCase()
.compareTo(y.employeeName.toLowerCase());
}
}
Sorting priority :
Lowest gap score
Alphabetical order as tie-breaker
Using this comparator ensures deterministic sorting, providing consistent results across repeated evaluations.
Project Ready Logic
cr.isProjectReady = (requiredImpact == 0);
If all required skills have zero impact, the candidate is ready.
Nice-to-have gaps don’t block readiness, preventing unnecessary hiring when existing employees are suitable.
Persisting Recommendations
The evaluation results are stored in the Project_Candidate__c object.
A composite key is used to uniquely identify each candidate for a project:
pc.Project_Employee_Key__c =
String.valueOf(projectId) + '|' + String.valueOf(employeeId);
Note: - The Project_Employee_Key__c is a Text field marked Unique and Required.
The records are then saved using:
upsert candidates Project_Employee_Key__c;
upsert ensures:
Insert if record doesn’t exist
Updates the record if it already exists
Prevents duplicate records
-
Allows re-evaluation to update previous scores
Top comments (2)
Great to see the progress on Skill Align! Project Staffing Assistant sounds like a really useful feature excited to see how you've implemented it. Salesforce devs like you inspire me to start my own side project!
Thank you so much! I’m really happy the Project Staffing Assistant caught your attention
Building backend logic in Salesforce has been challenging but super rewarding.
You should absolutely start your own side project — that’s where real growth happens.