Workforce Capacity Planning
Workforce Capacity Planning
The Workforce Capacity Planning module allows HR and operations teams to define headcount targets by department and planning period, model hiring plans against budget constraints, and monitor skill gaps and staffing risks in real time.
Accessing the Dashboard
Navigate to Dashboard → Capacity Planning in the main sidebar (listed after Recruitment). The URL is /dashboard/capacity.
Authentication is required. The page uses a server-side auth guard before rendering.
Dashboard Overview
The capacity planning dashboard is divided into four sections:
1. Summary KPI Cards
Four cards at the top of the page provide an org-wide snapshot:
| Card | Description |
|---|---|
| Planned Headcount | Total headcount targeted across all active plans |
| Current Headcount | Live headcount pulled from the employees table |
| Net Gap | Difference between planned and current (negative = understaffed) |
| Risk Distribution | Count of plans by risk level (critical / understaffed / healthy / overstaffed) |
2. Headcount Heatmap
A colour-coded grid showing headcount risk at the intersection of planning period and department.
| Colour | Risk Level | Gap % Threshold |
|---|---|---|
| 🔴 Red | Critical | Gap > 25% understaffed |
| 🟡 Amber | Understaffed | Gap 10–25% understaffed |
| 🟢 Green | Healthy | Gap within −10% to +20% |
| 🔵 Blue | Overstaffed | Gap > 20% overstaffed |
3. Plans Table
A paginated table of all capacity plans. Each row is expandable and shows:
- Planned vs. current headcount with gap badge
- Planning period (formatted as
Mon YYYY – Mon YYYY) - Budget allocated and budget per head
- Skill requirements with priority levels
4. High-Priority Skill Gap Alert Panel
Surfaces skill gaps flagged as high priority across all active plans, grouped by skill and level. Use this panel to identify critical hiring needs that cut across multiple departments.
Creating a Capacity Plan
Click the + New Plan button on the dashboard to open the create plan modal. Fill in the following fields:
| Field | Description |
|---|---|
| Department | Select from the organisation's department list |
| Period Start / End | Planning window (e.g. a quarter) |
| Planned Headcount | Target number of employees for this period |
| Budget Allocated | Budget in your base currency |
| Status | One of: draft, active, closed, archived |
On creation, the plan's current_headcount is automatically populated from the live employees table.
Risk Classification
Gap percentage is calculated as:
gap = planned_headcount − current_headcount
gap_pct = (gap / planned_headcount) × 100
Risk level is then assigned:
| Gap % | Risk Level | Meaning |
|---|---|---|
| > +25% | Critical | Severely understaffed |
| +10% to +25% | Understaffed | Moderate shortfall |
| −10% to +10% | Healthy | Within acceptable range |
| < −10% (surplus) | Overstaffed | Headcount exceeds plan by more than 20% |
The gap sign convention is: a positive gap means you need more people; a negative gap means you have more people than planned.
tRPC API Reference
All capacity planning operations are exposed under the capacityPlanning router.
capacityPlanning.list
Returns a paginated list of capacity plans with department names resolved.
Input:
{
orgId: string;
page?: number; // default 1
pageSize?: number; // default 20
}
capacityPlanning.get
Returns full details for a single plan.
Input:
{ id: string; orgId: string }
capacityPlanning.create
Creates a new plan. current_headcount is auto-populated from the employees table at creation time.
Input:
{
orgId: string;
departmentId?: string;
periodStart: string; // ISO date, e.g. "2025-01-01"
periodEnd: string; // ISO date, e.g. "2025-03-31"
plannedHeadcount: number;
skillRequirements?: Array<{
skill: string;
level: string;
count: number;
priority: string; // e.g. "high", "medium", "low"
}>;
budgetAllocated?: number;
status?: "draft" | "active" | "closed" | "archived";
}
All mutations are audit-logged via logAudit().
capacityPlanning.update
Updates an existing plan's headcount, skills, budget, or status.
Input:
{
id: string;
orgId: string;
plannedHeadcount?: number;
currentHeadcount?: number;
skillRequirements?: SkillRequirement[];
budgetAllocated?: number;
status?: "draft" | "active" | "closed" | "archived";
}
capacityPlanning.delete
Soft-deletes a capacity plan.
Input:
{ id: string; orgId: string }
capacityPlanning.gapAnalysis
Returns computed gap metrics for all active plans within an org.
Response per plan:
{
id: string;
departmentName: string;
periodStart: string;
periodEnd: string;
plannedHeadcount: number;
currentHeadcount: number;
gap: number;
gapPct: number;
riskLevel: "critical" | "warning" | "healthy" | "overstaffed";
budgetAllocated: number;
budgetPerHead: number;
skillRequirements: SkillRequirement[];
highPrioritySkillGaps: SkillRequirement[];
status: string;
}
capacityPlanning.summary
Org-level aggregates for dashboard KPI cards.
Response:
{
totalPlanned: number;
totalCurrent: number;
netGap: number;
riskDistribution: {
critical: number;
warning: number;
healthy: number;
overstaffed: number;
};
}
capacityPlanning.departments
Returns the list of departments for use in plan-creation dropdowns.
Input:
{ orgId: string }
Automated Weekly Refresh (Cron)
The capacity-gap-analysis Inngest function runs automatically every Monday at 06:00 UTC.
What it does:
- Fetches all active capacity plans for each organisation
- Queries the live employees table to get the latest headcount per department
- Updates
current_headcounton each plan with the refreshed value - Tracks execution via
registerBatch/trackBatchRun
Concurrency: The function is scoped to a concurrency limit of 1 — it will not execute twice simultaneously for the same organisation.
You do not need to manually trigger this job; it runs on schedule. To force a manual refresh of the dashboard data, use the refresh button on the capacity planning page.
Database Schema
Capacity plans are stored in the capacity_plans table (defined in src/db/schema-planning.ts).
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
org_id | uuid | Tenant identifier |
department_id | uuid | null | Links to the departments table |
period_start | date | Start of the planning window |
period_end | date | End of the planning window |
planned_headcount | integer | Target number of employees |
current_headcount | integer | Live employee count (auto-refreshed) |
skill_requirements | jsonb | Array of skill requirement objects |
budget_allocated | numeric | Budget for this plan |
status | enum | draft | active | closed | archived |
The skill_requirements JSONB field holds an array of objects with the shape:
[
{
"skill": "TypeScript",
"level": "senior",
"count": 3,
"priority": "high"
}
]
Audit Logging
All mutating operations (create, update, delete) on capacity plans are recorded via the platform's logAudit() function. Audit records include the acting user, the org, the operation type, and a snapshot of the changed data.