Competency Management (CMGM)
Bridge between appraisal outcomes and skill profiles — turning KRA scores into career development roadmaps
Vision
CMGM is the integration layer that bridges two existing modules — PERM (KRA appraisal scores) and SkillManagement (employee skill levels) — to produce actionable employee competency profiles and career development plans. After every appraisal cycle finalization, profiles are automatically regenerated via a Dapr event subscription, keeping career data current without manual HR intervention.
Business Value
Career recommendations based on actual skill levels and appraisal scores — not manager opinion alone
Competency profiles regenerate automatically after every finalized appraisal via Dapr subscription
Skill gaps identified by CMGM enrich PERM's TNA requests with specific skill-level training needs
Assessors and employees get concrete behavioral indicators per proficiency level — from CompetencyManager reference
Key Concepts Glossary
| Term | Definition |
|---|---|
| Competency Framework | A named grouping of KRA-to-Skill mappings for a role family (e.g., "Software Engineering Track", "Sales Track"). One framework can serve multiple career paths. |
| KRA-Skill Map | The bridge record: "KRA X maps to Skill Y at proficiency level Z". Links appraisal KRA performance to concrete skill standards. |
| Behavioral Anchor | Descriptive text explaining what "proficiency level 3 of Java" looks like in practice. Includes observable behavioral indicators as JSON array. |
| Proficiency Level (1-5) | Matches MSKILLLEVEL.SLNO from SkillManagement: 1=Beginner, 2=Basic, 3=Intermediate, 4=Advanced, 5=Expert. |
| Career Path | A defined progression from CurrentRole to TargetRole within a framework. Has TypicalMonthsRequired and a list of required skills at minimum proficiency levels. |
| Employee Competency Profile | Snapshot generated on-demand (or auto on appraisal finalization): compares employee's current skills vs. framework requirements. Stores OverallCompetencyScore and SkillGapsJson. |
| OverallCompetencyScore | Percentage of framework requirements met: (metCount / totalCount) × 100. |
| ReadinessPercent | Per-career-path score: what % of path requirements does the employee currently meet. |
| Skill Dev Plan (TSKILLDEVPLAN) | Individual/Team/Dept plan to address a specific skill gap. Has TrainingMethod, EstimatedHours, TrainerId, and completion tracking. |
| KSABO / SFIA / ESCO | Industry competency taxonomy references used to seed anchor text and map job roles. Not tables — reference frameworks used during master data setup. |
Module Boundary — CMGM vs SkillManagement
SkillManagement Owns
- MSKILL master (skill definitions)
- MSKILLLEVEL (1-5 proficiency definitions)
- MEMPLOYEESKILL (current employee skill levels)
- MROLERESOURCE (role skill requirements)
- Skill assessment forms and approvals
- Skill matrix views
CMGM Owns
- MCOMPETENCYFRAMEWORK (grouping of KRA→Skill maps)
- MCOMPETENCYKRAMAP (KRA↔Skill bridge with expected level)
- MCOMPETENCYANCHOR (behavioral indicators per level)
- MCAREERPATH + MCAREERPATHREQUIREMENT
- TEMPLOYEECOMPETENCYPROFILE (snapshots)
- TSKILLDEVPLAN (development plans)
Database Schema — Framework Tables
MCOMPETENCYFRAMEWORK
| Column | Type | Notes |
|---|---|---|
| FrameworkId | int PK | |
| Code / Name / Description | nvarchar | Unique Code per tenant |
| Status | smallint | 0=Inactive, 1=Active |
| TenantId | int | Index: (TenantId, Status). Unique: (Code, TenantId) |
MCOMPETENCYKRAMAP — KRA ↔ Skill Bridge
| Column | Type | Notes |
|---|---|---|
| CompetencyKRAMapId | int PK | |
| FrameworkId | int FK MCOMPETENCYFRAMEWORK | |
| KRAId | int FK MKRA | From PERM master |
| SkillId | int FK MSKILL | From SkillManagement |
| ExpectedProficiencyLevel | tinyint | 1-5; matches MSKILLLEVEL.SLNO |
| IsMandatory | bit | Mandatory gaps trigger immediate dev plan recommendation |
| SortOrder | smallint | |
| TenantId | int | Unique: (FrameworkId, KRAId, SkillId, TenantId) |
MCOMPETENCYANCHOR — Behavioral Anchors
| Column | Type | Notes |
|---|---|---|
| AnchorId | int PK | |
| SkillId | int FK MSKILL | |
| ProficiencyLevel | tinyint | 1-5; Unique: (SkillId, ProficiencyLevel, TenantId) |
| AnchorText | nvarchar(max) | Summary behavioral statement (1-2 sentences) |
| BehaviorIndicatorsJson | nvarchar(max) nullable | JSON array of observable behavioral indicators |
| TenantId | int | Index: (SkillId, TenantId) |
Career Path Tables
MCAREERPATH
| Column | Type | Notes |
|---|---|---|
| CareerPathId, PathCode, PathName | int / nvarchar | Unique PathCode per tenant |
| FrameworkId | int FK | |
| CurrentRoleId / TargetRoleId | int FK MJOBROLE | From → To role progression |
| TypicalMonthsRequired | int nullable | Estimated time to qualify for target role |
| Status | smallint | 0=Inactive, 1=Active |
| TenantId | int | Indexes: (FrameworkId, TenantId, Status), (CurrentRoleId, TenantId) |
MCAREERPATHREQUIREMENT
| Column | Type | Notes |
|---|---|---|
| CareerPathReqId | int PK | |
| CareerPathId | int FK MCAREERPATH | |
| SkillId | int FK MSKILL | Unique: (CareerPathId, SkillId, TenantId) |
| MinProficiencyLevel | tinyint | Minimum level to qualify for this path |
| IsMandatory | bit | Mandatory gaps block career path readiness |
| SortOrder | smallint |
Profile & Development Plan Tables
TEMPLOYEECOMPETENCYPROFILE
| Column | Type | Notes |
|---|---|---|
| CompetencyProfileId | int PK | |
| EmployeeId / FrameworkId | int | Upsert by (EmployeeId, FrameworkId, TenantId) |
| CareerPathId / TargetRoleId | int | -1 = not selected |
| GeneratedOn | datetime | Timestamp of last generation |
| OverallCompetencyScore | decimal(5,2) | (metCount / totalCount) × 100 |
| SkillGapsJson | nvarchar(max) | JSON: [{SkillId, SkillName, CurrentLevel, RequiredLevel, IsMandatory}] |
| Status | smallint | 0=Draft, 1=Published |
| TenantId | int | Indexes: (EmployeeId, TenantId, Status), (FrameworkId, TenantId) |
TSKILLDEVPLAN — Skill Development Plans
| Column | Type | Notes |
|---|---|---|
| DevPlanId | int PK | |
| PlanType | smallint | 0=Individual, 1=Team, 2=Department |
| TargetId | int | EmployeeId / TeamId / DeptId based on PlanType |
| SkillId / CurrentProficiencyLevel / TargetProficiencyLevel | int / tinyint | |
| Priority | smallint | 0=Low 1=Medium 2=High 3=Critical |
| TrainingMethod | smallint | 0=Classroom, 1=OnTheJob, 2=Online, 3=Mentoring, 4=Coaching |
| TargetDate / EstimatedHours / ActualHours | date / int | ActualHours set on CompleteSkillDevPlan |
| TrainerId | int | -1 = Unassigned |
| CompletionNotes | nvarchar nullable | Required on completion |
| Status | smallint | 0=Planned 1=InProgress 2=Completed 3=OnHold |
Framework Setup Flow
with expected levels
per skill per level
Current→Target role
skills + min levels
GenerateCompetencyProfile — Core Algorithm
① Load Employee Skill Levels
SkillManagementDAL → MEMPLOYEESKILL WHERE EmployeeId=@emp AND ProfileStatus=1: returns {SkillId, CurrentLevNo} for all assessed skills
② Load KRA-Skill Mappings
CMGMDAL → MCOMPETENCYKRAMAP WHERE FrameworkId=@fw: returns all {KRAId, SkillId, ExpectedProficiencyLevel, IsMandatory}
③ Load Latest KRA Scores
PERMDAL → GET_LATEST_KRA_SCORES_BY_EMPLOYEE: CTE selects top 1 finalized appraisal → ranks KRA scores preferring Manager eval over Self eval. If no finalized appraisal, all KRA scores = 0.
④ Evaluate Each Mapping (skillMet logic)
skillMet = (empCurrentLevel ≥ expectedLevel) OR (kraScore ≥ 60 AND empCurrentLevel ≥ expectedLevel - 1)
Second condition: a strong KRA score (≥60%) with level-1 gap is considered "developing-to-met" — reduces false negatives for high performers whose skill assessments lag behind their actual performance.
⑤ Compute Score & Build Gaps
OverallCompetencyScore = (metCount / totalCount) × 100
Collect SkillGapsJson array for all !skillMet entries.
⑥ Upsert Profile
INSERT or UPDATE TEMPLOYEECOMPETENCYPROFILE by (EmployeeId, FrameworkId). Publish GENERATECMGMPROFILEEVENTTYPEID event log. Cache invalidation.
⑦ Publish Dapr (if gaps)
If gaps.Any(): publish cmgm.skillgap.identified → PERM subscribes to enrich TTNAREQUEST with skill-specific training detail.
Always publish: cmgm.profile.generated → ESS career dashboard refresh.
Key SQL — GetLatestKRAScoresByEmployee (added to PERMDAL)
-- AppraisalEvalQB.GET_LATEST_KRA_SCORES_BY_EMPLOYEE
WITH LatestAppraisal AS (
SELECT TOP 1 APPRAISALID, FINALPERCENTAGE
FROM TAPPRAISAL
WHERE EMPLOYEEID = @EmployeeId AND STATUS = 4 AND TENANTID = @TenantId
ORDER BY APPRAISALID DESC
),
RankedEvals AS (
SELECT tek.KRAID, tek.KRASCORE, te.EVALTYPE,
ROW_NUMBER() OVER (
PARTITION BY tek.KRAID
ORDER BY CASE te.EVALTYPE WHEN 1 THEN 0 ELSE 1 END -- Manager(1) preferred
) AS Rn
FROM TAPPRAISALEVALKRA tek
INNER JOIN TAPPRAISALEVAL te ON te.APPRAISALEVALID = tek.APPRAISALEVALID
AND te.APPRAISALID = (SELECT APPRAISALID FROM LatestAppraisal)
)
SELECT re.KRAID AS KRAId, k.KRANAME AS KRAName,
re.KRASCORE AS KRAScore,
la.FINALPERCENTAGE AS OverallScore,
la.APPRAISALID AS AppraisalId
FROM RankedEvals re
INNER JOIN MKRA k ON k.KRAID = re.KRAID AND k.TENANTID = @TenantId
CROSS JOIN LatestAppraisal la
WHERE re.Rn = 1
GetCareerPathRecommendations (ESS Algorithm)
// CompetencyProfileBLL.GetCareerPathRecommendationsAsync
// EmployeeId ALWAYS from login.UserId — never from request param
1. Get employee CurrentRoleId from MEMPLOYEE
2. Load all active MCAREERPATH WHERE CurrentRoleId = employee's role AND TenantId = @TenantId
3. For each path:
a. Load MCAREERPATHREQUIREMENT for path
b. metCount = skills where employee.CurrentLevNo >= req.MinProficiencyLevel
c. mandatoryGapCount = mandatory reqs where employee.CurrentLevNo < MinLevel
d. ReadinessPercent = (metCount / totalReqs) * 100
4. Sort by ReadinessPercent DESC (closest-to-qualifying first)
5. Return List<CareerPathMatchDTO> with ReadinessPercent + MandatoryGapCount + TopGaps
Dapr Integration Flow
Subscribe: perm.appraisal.finalized
CMGMSL registers a Dapr MVC subscriber. On receiving this event: auto-triggers ICompetencyProfileBLL.GenerateCompetencyProfileAsync(employeeId, defaultFrameworkId, login, ct)
Route: POST /cmgm/subscribe/appraisal-finalized
Publish: cmgm.skillgap.identified
Published when profile generation finds gaps. Payload: {EmployeeId, FrameworkId, TenantId, Gaps: [{KRAId, SkillId, GapLevel}]}
PERM subscribes → inserts TTNAREQUEST rows with KRAId and skill-specific detail
// PERM subscriber (in PERMSL) — enriches TNA from CMGM skill gaps
[ApiController][Route("subscribe")]
public class CMGMSubscriberController : ControllerBase
{
[HttpPost("skill-gap-identified")]
[Topic("pubsub", "cmgm.skillgap.identified")]
public async Task<IActionResult> OnSkillGapIdentified(
[FromBody] SkillGapIdentifiedEvent evt, CancellationToken ct)
{
foreach (var gap in evt.Gaps.Where(g => g.KRAId > 0))
{
await _tnaBll.CreateTNAFromSkillGapAsync(
evt.EmployeeId, gap.KRAId, gap.SkillId, gap.GapLevel, ct);
}
return Ok();
}
}
Skill Dev Plan Lifecycle
Created from gap analysis
Training underway
ActualHours + Notes required
CompleteSkillDevPlan validates Status = 0 or 1 before completing. ActualHours is required — cannot complete without recording time invested.PERMDAL Additions for CMGM
CMGM reads KRA scores directly from PERMDAL (not via PERMBLL) to avoid a circular BLL reference:
// PERMDAL/DTOs/EmployeeKRAScoreDTO.cs (new DTO added to PERMDAL)
public sealed class EmployeeKRAScoreDTO {
public int KRAId { get; set; }
public string? KRAName { get; set; }
public decimal KRAScore { get; set; } // manager eval preferred; falls back to self eval
public decimal OverallScore { get; set; } // TAPPRAISAL.FinalPercentage from latest finalized
public int AppraisalId { get; set; }
}
// Added to IAppraisalEvalDAL interface:
Task<IEnumerable<EmployeeKRAScoreDTO>> GetLatestKRAScoresByEmployeeAsync(
int employeeId, LoginDTO login, CancellationToken ct);
// CMGMBLL.csproj ProjectReference:
// <ProjectReference Include="..\..\PERM\PERMDAL\PERMDAL.csproj" /> ← DAL only, not BLL
// <ProjectReference Include="..\..\SkillManagement\SkillManagementDAL\SkillManagementDAL.csproj" />
API Endpoints Reference (24 total)
Framework (4)
| Method | Route | Cache |
|---|---|---|
| GET | /Competency/Framework/GetCompetencyFrameworkList | CLIENT_LEVEL 10 min |
| GET | /Competency/Framework/GetCompetencyFramework {FrameworkId} | CLIENT_LEVEL |
| POST | /Competency/Framework/SaveCompetencyFramework | null (invalidates) |
| DELETE | /Competency/Framework/DeleteCompetencyFramework {FrameworkId} | null — guard: ValidationException if active KRAMaps exist |
KRA Map (3)
| Method | Route | Notes |
|---|---|---|
| GET | /Competency/KRAMap/GetKRAMap {FrameworkId} | CLIENT_LEVEL 10 min — returns all mappings with joined KRAName + SkillName + LevelLabel |
| POST | /Competency/KRAMap/SaveKRAMap | Bulk: DELETE existing for framework + INSERT new batch in transaction |
| DELETE | /Competency/KRAMap/DeleteKRAMap {CompetencyKRAMapId} | Single row delete |
Anchors (3)
| Method | Route | Cache |
|---|---|---|
| GET | /Competency/Anchor/GetAnchorsBySkill {SkillId} | CLIENT_LEVEL 30 min — all 5 levels for a skill |
| POST | /Competency/Anchor/SaveAnchor | null — guard: unique (SkillId, ProficiencyLevel) |
| DELETE | /Competency/Anchor/DeleteAnchor {AnchorId} | null |
Career Path (7)
| Method | Route | Cache |
|---|---|---|
| GET | /Competency/CareerPath/GetCareerPathList {FrameworkId?} | CLIENT_LEVEL 10 min |
| POST | /Competency/CareerPath/SaveCareerPath | null |
| DELETE | /Competency/CareerPath/DeleteCareerPath {CareerPathId} | null |
| GET | /Competency/CareerPath/GetCareerPathRequirements {CareerPathId} | CLIENT_LEVEL 10 min |
| POST | /Competency/CareerPath/SaveCareerPathRequirement | null |
| DELETE | /Competency/CareerPath/DeleteCareerPathRequirement {CareerPathReqId} | null |
Profile (4)
| Method | Route | Notes | Cache |
|---|---|---|---|
| GET | /Competency/Profile/GetCompetencyProfile {EmployeeId} | Latest published profile | USER_LEVEL 5 min |
| POST | /Competency/Profile/GenerateCompetencyProfile {EmployeeId} | On-demand generation; triggers full algorithm | null |
| GET | /Competency/Profile/GetSkillGapAnalysis {EmployeeId, CareerPathId} | Live gap vs specific path; includes anchor text per gap | NOT_REQUIRED |
| GET | /Competency/Profile/GetCareerPathRecommendations | ESS: EmployeeId from login.UserId only; sorted by ReadinessPercent DESC | USER_LEVEL 5 min |
Development Plans (5)
| Method | Route | Cache |
|---|---|---|
| GET | /Competency/Development/GetSkillDevPlanList {EmployeeId?, PlanType?, Status?} | USER_LEVEL 3 min |
| POST | /Competency/Development/SaveSkillDevPlan | null |
| POST | /Competency/Development/CompleteSkillDevPlan {DevPlanId, ActualHours, CompletionNotes} | null — validates Status=0 or 1 |
| DELETE | /Competency/Development/DeleteSkillDevPlan {DevPlanId} | null — only Planned/OnHold deletable |
Sample Scenarios
Scenario A — Software Engineering Framework & KRA Map
| KRA (PERM) | Mapped Skill | Expected Level | Mandatory |
|---|---|---|---|
| Technical Excellence (KRAId: 5) | Backend Development | 3 — Intermediate | Yes |
| Technical Excellence (KRAId: 5) | System Design | 3 — Intermediate | Yes |
| Customer Focus (KRAId: 8) | Communication | 2 — Basic | No |
| Delivery Quality (KRAId: 9) | Code Review | 3 — Intermediate | Yes |
| Leadership (KRAId: 12) | Team Leadership | 2 — Basic | No |
Scenario B — Behavioral Anchors for Code Review Skill
| Level | Anchor Text | Sample Indicators |
|---|---|---|
| L1 Beginner | Can identify obvious style issues; understands basic PR process | Reviews formatting conventions; asks for clarification |
| L2 Basic | Reviews for correctness; catches common bugs and edge cases | Identifies off-by-one errors; checks null handling |
| L3 Intermediate | Reviews for design patterns, performance, security; constructive feedback | Questions architectural choices; flags SQL injection risks |
| L4 Advanced | Establishes team review standards; mentors reviewers; architectural feedback | Writes team review guidelines; coaches juniors on review craft |
| L5 Expert | Defines org-wide review culture; recognized externally as authority | Speaks at conferences; authors industry review frameworks |
Scenario C — Employee Competency Profile (John Smith, EmployeeId: 201)
John Smith — FY2026 Appraisal: 78%
Framework: Software Engineering Track · Target: Senior Dev → Tech Lead
| Skill | Current Level | Required | Gap | Mandatory | Status |
|---|---|---|---|---|---|
| Backend Development | L3 | L3 | — | Yes | ✓ Met |
| System Design | L2 | L3 | 1 level | Yes | ✗ Gap |
| Communication | L3 | L2 | — | No | ✓ Met |
| Code Review | L3 | L4 | 1 level | Yes | ✗ Gap |
| Team Leadership | L1 | L2 | 1 level | No | ⚠ Gap |
Scenario D — Career Path Recommendations
| Path | From → To | Readiness | Mandatory Gaps | Months |
|---|---|---|---|---|
| Engineering Track | Senior Dev → Tech Lead | 60% | 2 (Code Review L4, System Design L3) | 18 |
| Architecture Track | Senior Dev → Solutions Architect | 40% | 3 (System Design L4, Cloud L3, Stakeholder Mgmt L2) | 24 |
| Management Track | Senior Dev → Engineering Manager | 35% | 4 (Leadership L3, Hiring L2, P&L L2, Conflict Mgmt L2) | 30 |
Employee is closest to Tech Lead path. Recommended dev plan: Code Review mentoring (40h) + System Design course (20h).
Scenario E — Skill Dev Plan Created from Gap
POST /Competency/Development/SaveSkillDevPlan
{
"PlanType": 0, // Individual
"TargetId": 201, // John Smith
"SkillId": 45, // Code Review
"CurrentProficiencyLevel": 3,
"TargetProficiencyLevel": 4,
"Priority": 2, // High
"TrainingMethod": 3, // Mentoring
"TargetDate": "2026-09-30",
"EstimatedHours": 40,
"TrainerId": 305 // Senior architect as mentor
}
// Later: complete the plan
POST /Competency/Development/CompleteSkillDevPlan
{ "DevPlanId": 501, "ActualHours": 38, "CompletionNotes": "John completed all 8 mentoring sessions. Ready to conduct independent reviews." }
// Status → 2, ActualHours: 38, CompletionNotes populated
Integration Map
| Direction | Mechanism | What Flows |
|---|---|---|
| PERM → CMGM | Dapr: perm.appraisal.finalized | Triggers auto-generation of TEMPLOYEECOMPETENCYPROFILE for the finalized employee |
| CMGM reads PERM | ProjectReference to PERMDAL | IAppraisalEvalDAL.GetLatestKRAScoresByEmployee — KRA scores for profile generation |
| CMGM reads SkillMgmt | ProjectReference to SkillManagementDAL | MEMPLOYEESKILL current levels via IEmployeeSkillProfileBLL injection |
| CMGM → PERM TNA | Dapr: cmgm.skillgap.identified | Gaps payload → PERM inserts TTNAREQUEST rows with skill-specific training context |
| CMGM → ESS | Dapr: cmgm.profile.generated | ESS career dashboard cache invalidation and refresh trigger |
Service Configuration
| Setting | Value |
|---|---|
| Port | 5125 |
| Proxy prefix | /cmgm/ |
| SignalR | None — no real-time push needed for CMGM |
| Assembly scanning | CMGMBLL + CMGMDAL in CMGMSL/Program.cs |
| CMGMBLL.csproj references | CMGMDAL + PERMDAL + SkillManagementDAL (DAL-only cross-module) |
| Dapr subscriber route | POST /cmgm/subscribe/appraisal-finalized |
Caching Strategy
| Data | Level | TTL | Invalidation |
|---|---|---|---|
| MCOMPETENCYFRAMEWORK list | CLIENT_LEVEL | 10 min | Save, Delete |
| MCOMPETENCYKRAMAP per framework | CLIENT_LEVEL | 10 min | SaveKRAMap, DeleteKRAMap |
| MCOMPETENCYANCHOR per skill | CLIENT_LEVEL | 30 min | SaveAnchor, DeleteAnchor |
| MCAREERPATH list | CLIENT_LEVEL | 10 min | Save, Delete |
| MCAREERPATHREQUIREMENT per path | CLIENT_LEVEL | 10 min | Save, Delete |
| TEMPLOYEECOMPETENCYPROFILE | USER_LEVEL | 5 min | GenerateCompetencyProfile |
| GetSkillGapAnalysis | NOT_REQUIRED | — | Always live — gap data changes with skill assessments |
| GetCareerPathRecommendations | USER_LEVEL | 5 min | GenerateCompetencyProfile |
| TSKILLDEVPLAN per employee | USER_LEVEL | 3 min | Save, Complete, Delete |