Semantius Logo

Workforce Planning — Semantic Model

1. Overview

A scenario-based headcount planning system. The system tracks the current org (departments, locations, cost centers, jobs, employees, positions) as the source-of-truth baseline, and lets planners draft alternative future-state plans through scenarios containing staged headcount actions (add, eliminate, transfer). Approved scenarios materialize into real position records, which can then be handed off to recruiting via lightweight hiring requisitions.

2. Entity summary

#Table nameSingular labelPurpose
1departmentsDepartmentOrg units the workforce is grouped into; supports hierarchy via parent_department_id
2locationsLocationOffices, regions, or remote pools where positions sit
3cost_centersCost CenterFinancial buckets used to budget headcount cost
4jobsJobCatalog of role definitions — title, level, family — used to template positions
5employeesEmployeeCurrent workforce members; can occupy a position and report to a manager
6positionsPositionA discrete seat (filled, open, or approved-future), tied to a job, department, location, cost center
7headcount_plansHeadcount PlanA named plan covering a timeframe (e.g. “FY2026”) with status (draft → in-review → approved → active → archived)
8scenariosScenarioA what-if version of a plan (base, optimistic, conservative, custom). Many per plan; one is marked active
9headcount_actionsHeadcount ActionA staged change within a scenario — add, eliminate, or transfer — with effective date and cost impact
10hiring_requisitionsHiring RequisitionA lightweight handoff record marking that a position has been cleared to start recruiting

Entity-relationship diagram

flowchart LR
    departments -->|parent of| departments
    employees -->|heads| departments
    departments -->|primary dept for| cost_centers
    employees -->|owns| cost_centers
    employees -->|manages| employees
    locations -->|home for| employees
    jobs -->|templates| positions
    departments -->|contains| positions
    locations -->|hosts| positions
    cost_centers -->|funds| positions
    employees ---|fills| positions
    positions -->|backfilled by| positions
    headcount_actions -->|materializes as| positions
    employees -->|owns| headcount_plans
    employees -->|approves| headcount_plans
    headcount_plans -->|contains| scenarios
    employees -->|creates| scenarios
    scenarios -->|contains| headcount_actions
    positions -->|targeted by| headcount_actions
    jobs -->|specified in| headcount_actions
    departments -->|target of| headcount_actions
    locations -->|target of| headcount_actions
    cost_centers -->|target of| headcount_actions
    positions -->|opens| hiring_requisitions
    employees -->|recruits| hiring_requisitions
    employees -->|hiring manager for| hiring_requisitions

3. Entities

3.1 departments — Department

Plural label: Departments Label column: department_name Audit log: no Description: An org unit the workforce is grouped into (e.g. Engineering, Sales). Supports hierarchy via parent_department_id so sub-departments can roll up under a parent. Created by HR or planning admins as the org’s structural skeleton.

Fields

Field nameFormatRequiredLabelReference / Notes
department_namestringyesNameunique
department_codestringnoCodeunique; short code, e.g. ENG, SLS
parent_department_idreferencenoParent Departmentdepartments (N:1, self)
head_employee_idreferencenoDepartment Heademployees (N:1)
descriptiontextnoDescription
is_activebooleanyesActive

Relationships

  • A department may have a parent department (N:1, optional, self-referential, clear on delete).
  • A department may have one head employee (N:1, optional, clear on delete).
  • A department may have many cost_centers for which it is the primary department (1:N, via cost_centers.primary_department_id).
  • A department may host many positions (1:N, via positions.department_id).
  • A department may be the target of many headcount_actions (1:N, via headcount_actions.department_id).

3.2 locations — Location

Plural label: Locations Label column: location_name Audit log: no Description: An office, regional hub, remote pool, or field location where positions can sit. Used to plan headcount geography and assign employees a home base.

Fields

Field nameFormatRequiredLabelReference / Notes
location_namestringyesName
location_typeenumyesTypevalues: office, remote_pool, hybrid_hub, field
citystringnoCity
regionstringnoRegion / State
countrystringnoCountryISO-2 or full name
timezonestringnoTime ZoneIANA, e.g. Europe/Berlin
is_activebooleanyesActive

Relationships

  • A location may be the home location for many employees (1:N, via employees.home_location_id).
  • A location may host many positions (1:N, via positions.location_id).
  • A location may be the target of many headcount_actions (1:N, via headcount_actions.location_id).

3.3 cost_centers — Cost Center

Plural label: Cost Centers Label column: cost_center_code Audit log: no Description: A financial bucket against which headcount cost is budgeted and reported. Often (but not always) maps 1:1 with a department. Carries a currency for multi-currency budgeting support.

Fields

Field nameFormatRequiredLabelReference / Notes
cost_center_codestringyesCodeunique
cost_center_namestringyesName
primary_department_idreferencenoPrimary Departmentdepartments (N:1)
owner_employee_idreferencenoOwneremployees (N:1)
currency_codestringyesCurrencyISO-4217, e.g. USD, EUR
is_activebooleanyesActive

Relationships

  • A cost_center may have a primary department (N:1, optional, clear on delete).
  • A cost_center may have one owner employee (N:1, optional, clear on delete).
  • A cost_center may fund many positions (1:N, via positions.cost_center_id).
  • A cost_center may be the target of many headcount_actions (1:N, via headcount_actions.cost_center_id).

3.4 jobs — Job

Plural label: Jobs Label column: job_name Audit log: no Description: A reusable role definition (title + level + family) that templates positions. A position is “an instance of a job” placed in a department/location/cost center. Carries an optional comp band for budgeting reference.

Fields

Field nameFormatRequiredLabelReference / Notes
job_namestringyesNamee.g. “Senior Software Engineer”
job_codestringnoCodeunique
job_familystringnoJob Familye.g. Engineering, Sales
job_levelstringnoLevele.g. L4, Manager, Director
job_typeenumyesTypevalues: individual_contributor, people_manager, executive
descriptiontextnoDescription
min_annual_compensationfloatnoMin Annual Compensationcomp band low
max_annual_compensationfloatnoMax Annual Compensationcomp band high
compensation_currencystringnoCompensation CurrencyISO-4217
is_activebooleanyesActive

Relationships

  • A job may template many positions (1:N, via positions.job_id).
  • A job may be specified in many headcount_actions (1:N, via headcount_actions.job_id).

3.5 employees — Employee

Plural label: Employees Label column: employee_full_name Audit log: yes Description: A current workforce member (full-time, part-time, contractor, or intern). Each employee may occupy a position and reports to a manager. Lifecycle covers pending_startactiveon_leaveterminated.

Fields

Field nameFormatRequiredLabelReference / Notes
employee_full_namestringyesFull Name
employee_numberstringnoEmployee Numberunique
work_emailemailnoWork Emailunique
employment_typeenumyesEmployment Typevalues: full_time, part_time, contractor, intern
employment_statusenumyesEmployment Statusvalues: pending_start, active, on_leave, terminated
hire_datedatenoHire Date
termination_datedatenoTermination Date
manager_employee_idreferencenoManageremployees (N:1, self)
home_location_idreferencenoHome Locationlocations (N:1)

Relationships

  • An employee may have a manager employee (N:1, optional, self-referential, clear on delete).
  • An employee may have a home location (N:1, optional, clear on delete).
  • An employee may fill exactly one position (1:1, via positions.current_employee_id with uniqueness).
  • An employee may head many departments (1:N, via departments.head_employee_id).
  • An employee may own many cost_centers (1:N, via cost_centers.owner_employee_id).
  • An employee may own and approve many headcount_plans (1:N each, via headcount_plans.owner_employee_id and .approved_by_employee_id).
  • An employee may create many scenarios (1:N, via scenarios.created_by_employee_id).
  • An employee may serve as recruiter or hiring manager on many hiring_requisitions (1:N each).

3.6 positions — Position

Plural label: Positions Label column: position_code Audit log: yes Description: A discrete seat in the org. Captures both reality (filled or open today) and approved-future seats (committed from an approved scenario, with a target start date). Uncommitted what-if seats live as headcount_actions, not as positions. Status drives lifecycle: filledopen, approved_futureopen once the start date arrives, eliminated for closed seats.

Fields

Field nameFormatRequiredLabelReference / Notes
position_codestringyesPosition Codeunique, e.g. POS-00123
position_statusenumyesStatusvalues: filled, open, approved_future, on_hold, eliminated
job_idreferenceyesJobjobs (N:1)
department_idreferenceyesDepartmentdepartments (N:1)
location_idreferenceyesLocationlocations (N:1)
cost_center_idreferenceyesCost Centercost_centers (N:1)
current_employee_idreferencenoCurrent Employeeemployees (N:1); unique — at most one position per employee
ftefloatyesFTE1.0 = full-time, 0.5 = half-time
target_start_datedatenoTarget Start Datefor approved_future and open
actual_start_datedatenoActual Start Datewhen the seat became filled
end_datedatenoEnd Datewhen the seat was eliminated
budgeted_annual_costfloatnoBudgeted Annual Cost
budget_currencystringnoBudget CurrencyISO-4217
is_backfillbooleannoIs Backfill
backfill_for_position_idreferencenoBackfill Forpositions (N:1, self)
originated_from_action_idreferencenoOriginated From Actionheadcount_actions (N:1); set when committed from a scenario
notestextnoNotes

Relationships

  • A position belongs to one job, one department, one location, one cost_center (each N:1, required, restrict on delete).
  • A position may be filled by exactly one employee (1:1, via current_employee_id with uniqueness, clear on delete).
  • A position may be a backfill of another position (N:1, optional, self-referential, clear on delete).
  • A position may have originated from one headcount_action (N:1, optional, clear on delete).
  • A position may be targeted by many headcount_actions (1:N, via headcount_actions.target_position_id).
  • A position may have many hiring_requisitions over time (1:N, via hiring_requisitions.position_id, restrict on delete).

3.7 headcount_plans — Headcount Plan

Plural label: Headcount Plans Label column: plan_name Audit log: yes Description: A named plan covering a fiscal timeframe. Acts as a container for one or more scenarios. Lifecycle: draftin_reviewapprovedactivearchived.

Fields

Field nameFormatRequiredLabelReference / Notes
plan_namestringyesNamee.g. “FY2026 Headcount Plan”
plan_statusenumyesStatusvalues: draft, in_review, approved, active, archived
fiscal_year_labelstringnoFiscal Yeare.g. FY2026
start_datedateyesStart Date
end_datedateyesEnd Date
owner_employee_idreferencenoPlan Owneremployees (N:1)
descriptiontextnoDescription
approved_atdate-timenoApproved At
approved_by_employee_idreferencenoApproved Byemployees (N:1)

Relationships

  • A headcount_plan may have one owner employee and one approver employee (each N:1, optional, clear on delete).
  • A headcount_plan has many scenarios (1:N, parent, cascade on delete — scenarios live and die with their plan).

3.8 scenarios — Scenario

Plural label: Scenarios Label column: scenario_name Audit log: yes Description: An alternative version of a plan (base case, aggressive growth, conservative). Each plan has many scenarios; exactly one per plan is marked is_active_for_plan = true. The active scenario’s actions are what gets committed when the plan is approved.

Fields

Field nameFormatRequiredLabelReference / Notes
scenario_namestringyesNamee.g. “Base Case”, “Aggressive Growth”
headcount_plan_idparentyesPlanheadcount_plans (N:1, cascade)
scenario_typeenumyesTypevalues: base, optimistic, conservative, custom
scenario_statusenumyesStatusvalues: draft, in_review, approved, archived
is_active_for_planbooleanyesActive for Planexactly one per plan should be true
descriptiontextnoDescription
committed_atdate-timenoCommitted Atwhen actions were materialized into positions
created_by_employee_idreferencenoCreated Byemployees (N:1)

Relationships

  • A scenario belongs to one headcount_plan (N:1, parent, cascade on delete).
  • A scenario may have a creator employee (N:1, optional, clear on delete).
  • A scenario has many headcount_actions (1:N, parent, cascade on delete).

3.9 headcount_actions — Headcount Action

Plural label: Headcount Actions Label column: action_label Audit log: yes Description: A single staged change within a scenario. Three action types: add (create a new seat), eliminate (close an existing seat), transfer (move an existing seat between department/location/cost center). The caller populates action_label (e.g. “Add Sr SWE / Eng / Berlin / 2026-Q1”) to give the action a human-readable handle.

Fields

Field nameFormatRequiredLabelReference / Notes
action_labelstringyesLabelcaller populates
scenario_idparentyesScenarioscenarios (N:1, cascade)
action_typeenumyesAction Typevalues: add, eliminate, transfer
action_statusenumyesStatusvalues: proposed, in_review, approved, committed, rejected
target_position_idreferencenoTarget Positionpositions (N:1); required for eliminate/transfer, null for add
job_idreferencenoJobjobs (N:1); required for add
department_idreferencenoDepartmentdepartments (N:1); required for add, target dept for transfer
location_idreferencenoLocationlocations (N:1); required for add, target loc for transfer
cost_center_idreferencenoCost Centercost_centers (N:1); required for add, target cc for transfer
effective_datedateyesEffective Date
ftefloatnoFTEfor add
budgeted_annual_costfloatnoBudgeted Annual Costfor add
budget_currencystringnoBudget CurrencyISO-4217
justificationtextnoJustification

Relationships

  • A headcount_action belongs to one scenario (N:1, parent, cascade on delete).
  • A headcount_action may target one existing position (N:1, optional, clear on delete) — required for eliminate/transfer.
  • A headcount_action may specify one job, department, location, cost_center (each N:1, optional, clear on delete) — populated for add/transfer per the action type.
  • A headcount_action may materialize as many positions once committed (1:N, via positions.originated_from_action_id).

3.10 hiring_requisitions — Hiring Requisition

Plural label: Hiring Requisitions Label column: requisition_number Audit log: yes Description: A lightweight handoff record marking that an open position has been cleared to start recruiting. Tracks recruiter, hiring manager, target/actual fill dates, and an optional URL into the external ATS where the candidate pipeline lives. A future ATS module would replace this with a richer requisition + candidate model.

Fields

Field nameFormatRequiredLabelReference / Notes
requisition_numberstringyesRequisition Numberunique, e.g. REQ-2026-0123
position_idreferenceyesPositionpositions (N:1, restrict)
requisition_statusenumyesStatusvalues: open, on_hold, filled, cancelled
recruiter_employee_idreferencenoRecruiteremployees (N:1)
hiring_manager_employee_idreferencenoHiring Manageremployees (N:1)
opened_datedateyesOpened Date
target_fill_datedatenoTarget Fill Date
filled_datedatenoFilled Date
external_ats_urlurlnoExternal ATS URLhandoff link to the recruiting tool
notestextnoNotes

Relationships

  • A hiring_requisition is for exactly one position (N:1, required, restrict on delete).
  • A hiring_requisition may have a recruiter employee and a hiring manager employee (each N:1, optional, clear on delete).

4. Relationship summary

FromFieldToCardinalityKindDelete behavior
departmentsparent_department_iddepartmentsN:1referenceclear
departmentshead_employee_idemployeesN:1referenceclear
cost_centersprimary_department_iddepartmentsN:1referenceclear
cost_centersowner_employee_idemployeesN:1referenceclear
employeesmanager_employee_idemployeesN:1referenceclear
employeeshome_location_idlocationsN:1referenceclear
positionsjob_idjobsN:1referencerestrict
positionsdepartment_iddepartmentsN:1referencerestrict
positionslocation_idlocationsN:1referencerestrict
positionscost_center_idcost_centersN:1referencerestrict
positionscurrent_employee_idemployees1:1referenceclear
positionsbackfill_for_position_idpositionsN:1referenceclear
positionsoriginated_from_action_idheadcount_actionsN:1referenceclear
headcount_plansowner_employee_idemployeesN:1referenceclear
headcount_plansapproved_by_employee_idemployeesN:1referenceclear
scenariosheadcount_plan_idheadcount_plansN:1parentcascade
scenarioscreated_by_employee_idemployeesN:1referenceclear
headcount_actionsscenario_idscenariosN:1parentcascade
headcount_actionstarget_position_idpositionsN:1referenceclear
headcount_actionsjob_idjobsN:1referenceclear
headcount_actionsdepartment_iddepartmentsN:1referenceclear
headcount_actionslocation_idlocationsN:1referenceclear
headcount_actionscost_center_idcost_centersN:1referenceclear
hiring_requisitionsposition_idpositionsN:1referencerestrict
hiring_requisitionsrecruiter_employee_idemployeesN:1referenceclear
hiring_requisitionshiring_manager_employee_idemployeesN:1referenceclear

5. Enumerations

5.1 locations.location_type

  • office
  • remote_pool
  • hybrid_hub
  • field

5.2 jobs.job_type

  • individual_contributor
  • people_manager
  • executive

5.3 employees.employment_type

  • full_time
  • part_time
  • contractor
  • intern

5.4 employees.employment_status

  • pending_start
  • active
  • on_leave
  • terminated

5.5 positions.position_status

  • filled
  • open
  • approved_future
  • on_hold
  • eliminated

5.6 headcount_plans.plan_status

  • draft
  • in_review
  • approved
  • active
  • archived

5.7 scenarios.scenario_type

  • base
  • optimistic
  • conservative
  • custom

5.8 scenarios.scenario_status

  • draft
  • in_review
  • approved
  • archived

5.9 headcount_actions.action_type

  • add
  • eliminate
  • transfer

5.10 headcount_actions.action_status

  • proposed
  • in_review
  • approved
  • committed
  • rejected

5.11 hiring_requisitions.requisition_status

  • open
  • on_hold
  • filled
  • cancelled

6. Open questions

6.1 🔴 Decisions needed (blockers)

None.

6.2 🟡 Future considerations (deferred scope)

  • Should position_assignments be added as a history entity to record every employee who has held a position over time, rather than only the current occupant on positions.current_employee_id?
  • Should a skills (or competencies) catalog be introduced, with M:N junctions to jobs (required skills) and employees (held skills) to support skills-based planning?
  • Should headcount_actions.action_type be extended with promote, reclassify, and comp_change to cover non-seat changes within a scenario, or are those better modeled as a separate compensation_actions entity?
  • Should a legal_entities entity be added to support multi-subsidiary planning (with cost_centers and employees rolling up to a legal entity)?
  • Should attrition assumptions (e.g. expected voluntary attrition % per department per quarter) be modeled as planning inputs, possibly as an attrition_assumptions entity attached to scenarios?
  • Should scenarios be able to stage org-structure changes (new departments, splits, merges) in addition to position changes, or are department changes always made directly on the live org?
  • Should the lightweight hiring_requisitions be replaced or extended by a full ATS module (candidates, applications, interview_stages, offers) when recruiting moves into this system?
  • Should employees carry an optional user_id link to the platform’s built-in users for SSO/login integration, with deduplication handled at deploy time?
  • Should cost_centersdepartments become an M:N junction to support orgs where a department is funded by multiple cost centers, rather than the current single primary_department_id reference?
  • Should compensation_currency on jobs and budget_currency on positions/headcount_actions be promoted from a string field to a currencies lookup entity once the active currency list stabilizes?

7. Implementation notes for the downstream agent

  1. Create one module named workforce_planning and two baseline permissions (workforce_planning:read, workforce_planning:manage) before any entity.
  2. Create entities in this order so that referenced entities exist first: locations, departments, jobs, cost_centers, employees, positions, headcount_plans, scenarios, headcount_actions, hiring_requisitions. Note that departments, employees, cost_centers, and positions all contain forward references to entities created later in the list — create the entity first with all non-FK fields, then add the FK fields in a second pass once the target tables exist.
  3. For each entity: set label_column to the snake_case field marked as label in §3, pass module_id, view_permission (workforce_planning:read), edit_permission (workforce_planning:manage). Set audit_log: true on employees, positions, headcount_plans, scenarios, headcount_actions, and hiring_requisitions (per §3); leave the others at the default false. Do not manually create id, created_at, updated_at, or the auto-label field.
  4. For each field in §3: pass table_name, field_name, format, title (the Label column), and for reference/parent fields also reference_table and a reference_delete_mode consistent with §4. For positions.current_employee_id, set unique_value: true to enforce the 1:1 business rule (an employee fills at most one position). The Required column is analyst intent; the platform manages nullability internally.
  5. Fix up each entity’s auto-created label-column field title. create_entity auto-creates a field whose field_name equals the entity’s label_column, and its title defaults to singular_label (e.g. entity departments with singular_label: "Department" and label_column: "department_name" yields an auto-field departments.department_name with title "Department"). For every entity in this model, the §3 Label for the label-column row differs from singular_label, so update_field must be called for each. Use the composite string id "{table_name}.{field_name}" (passed as a string, not an integer):
    • "departments.department_name" → title "Name"
    • "locations.location_name" → title "Name"
    • "cost_centers.cost_center_code" → title "Code"
    • "jobs.job_name" → title "Name"
    • "employees.employee_full_name" → title "Full Name"
    • "positions.position_code" → title "Position Code"
    • "headcount_plans.plan_name" → title "Name"
    • "scenarios.scenario_name" → title "Name"
    • "headcount_actions.action_label" → title "Label"
    • "hiring_requisitions.requisition_number" → title "Requisition Number"
  6. Deduplicate against Semantius built-in tables. This model is self-contained but does not currently declare any entity that overlaps with the Semantius built-ins (users, roles, permissions, etc.). No deduplication action is required for this model. If a future extension declares any built-in (e.g. linking employees.user_id to users), read Semantius first and reuse the built-in as the reference_table target rather than recreating it.
  7. After creation, spot-check: every label_column resolves to a real field; every reference_table target exists; the is_active_for_plan boolean on scenarios has a uniqueness expectation per headcount_plan_id (enforce via application logic if the platform does not support partial unique indexes); positions.current_employee_id has unique_value: true set.