Phase 5: HR layer, leave, and lifecycle effects on approval routing
Status: design, approved section by section by the owner 2026-09-30; not built. Branch
phase-5-hr-design. Roadmap: Phase 5 — “HR layer deferred from Phase 1: job grades, salary bands,
headcount, vacancies” and “Leave and claims interplay; lifecycle effects on approval routing”.
Architecture spec §4.7 (“Out of scope for Phase 1 … job grades, salary bands, headcount planning,
vacancy and requisition workflows. That is the HR layer”) and §19 Phase 5 (“HR adjacency”).
Payroll (the third Phase 5 item) is not in this spec. It waits on its statutory tables (EPF, SOCSO, EIS, PCB), which need an outside source.
Three lanes, one plan each. They run concurrently with Phase 4 (00700–00789) and take
00800 upward:
| Lane | What | Migrations | Depends on |
|---|---|---|---|
| H1 | Employee record and lifecycle | 00800–00819 |
— |
| H2 | Job grades, salary bands, headcount, vacancies | 00820–00839 |
H1 |
| H3 | Leave | 00840–00859 |
H1 |
H1 first; H2 and H3 in parallel after it.
Owner decisions (2026-09-30)
Section titled “Owner decisions (2026-09-30)”- HD-1 Employee record: separate from the login member; keyed to the existing employee
party. - HD-2 Approver on leave: the seat is treated as unavailable for the dates and routing
escalates up (the existing
on_vacancy: escalatewalk). No delegation is created, so no authority moves unless a person chooses it; the decision log records “approver on leave”. - HD-3 Leave entitlements: the Employment Act minimums are a statutory floor; an organization may raise them and the database refuses less.
- HD-4 Headcount: hiring beyond approved headcount is blocked; an Owner who is not the requester may override with a reason, audited (the BR-11 shape).
- HD-5 Salary: payroll owns individual salaries; the HR layer keeps bands only.
- HD-6 Leaving: recording a last day is blocked while the person holds an open advance or a petty cash float; an Owner who is not the requester may override with a reason, audited.
- HD-7 Adjustments: a manual leave adjustment cannot take a statutory type below the floor for that leave year.
- HD-8 Rule changes: leave rules are effective-dated and apply only to grants made on or after their date; past grants are never rewritten.
What exists (surveyed 2026-09-30)
Section titled “What exists (surveyed 2026-09-30)”party(00009):kindincludesemployee; everyorganization_memberhas aparty_id. Claims, advances and payouts bind to the party, not to the login.organization_memberalso carriesemployee_no,cost_centre,active.- Position tree (
00018,platform/orgstruct):org_unit,position(seats that outlive their holder;reports_to_position_id, materialisedpath),member_position(dated assignments,is_primary, exclusion constraint, withdrawal00023). Superior resolution follows the seat, then the unit head, then refuses with a named reason (superior.go). - Workflow grants resolve
superior,position,unit_head,levels_up, withon_vacancy(escalateor refuse) andmax_escalation_levels(workflow/resolve.go,seed.go). workflow_delegation(00032): dated, manual, revoked and never deleted; the delegate acts on the delegator’s thresholds and the decision log names both.- Sole-owner exception (Phase 2):
workflow.LockSoleOwner,SoleOwnerException, reason mandatory, decisionauthority_via = sole_owner_exception. - The
committedsemantic (“approved to act on; nothing posts”) is added by Phase 4 O1 Task 1 (00700). H2 and H3 need it; their first task adds it only if Phase 4 has not merged it (the droppable pattern Phase 4 O4 uses foreffect). - Phase 3:
moduleinstall, custom fields, the import framework, the documents store.
H1. Employee record and lifecycle (platform/hr, 00800–00819)
Section titled “H1. Employee record and lifecycle (platform/hr, 00800–00819)”employee, one row per employee party (party_idunique, kindemployee): employee number, identity document kind (nric,passport,other) and number, hire date, employment type (permanent,contract,probation,part_time), last day (nullable), status.- Link to login:
organization_member.party_idalready points at the party. An employee with no login has no member row; a member with no employee record (an outside approver) stays valid. employee_nomoves toemployee. The migration copies each member’s value across; the member column stays, read-only, so nothing that reads it breaks.- Personal data: the identity document number is shown only to holders of
hr.employee.sensitive(PDPA; the operator console’s approach). No salary, no bank details (the payout profile holds those). employee_event, append-only:hired,confirmed,transferred,notice_given,left,rehired. Status is derived from the events; nothing is edited in place.- Setting a last day is refused while the person holds an open advance or a petty cash float
(HD-6). An Owner who is not the requester may override with a reason: an append-only
lifecycle_overriderow and an audit entry. - On the last day: seat assignments end (the seat becomes vacant and
on_vacancy: escalatetakes over), login access ends after that day, open claims of theirs may still be settled, and their pending approvals move to the next filled seat. - Rehire reuses the party and the employee row with a new
hiredevent; the employee number is kept, not reissued. - Import: an
employeeskind on the Phase 3 framework.
Out of H1: payroll data, emergency contacts, contracts and other documents, appraisals.
H2. Grades, salary bands, headcount, vacancies (00820–00839)
Section titled “H2. Grades, salary bands, headcount, vacancies (00820–00839)”job_grade: code, name (translation rows), numeric rank, active. Retired, never deleted.grade_band:min_salary,max_salaryNUMERIC(19,4)MYR, effective-dated; the database refuses overlapping periods for one grade and a minimum above the maximum.- The grade belongs to the seat:
position.grade_id(nullable). An employee’s grade derives from their primary seat — one source of truth, as the spec derives the reporting line from the tree. Nothing checks a salary against a band yet; payroll will. - Headcount:
position.approved_headcount(integer ≥ 0, default 1). A unit’s approved headcount is the sum of its positions’, derived. Filled = people currently holding the seat as their primary assignment; acting and secondary assignments do not count. - Over-headcount is blocked (HD-4) by a trigger on assignment that locks the position row; the
Owner-not-requester override writes an append-only
headcount_overriderow and an audit entry. vacancy: position,kind(replacement,new), quantity, needed-by, reason; its ownvacancyworkflow, approver from grants, never chosen by the requester. Approval ends incommitted. Approving anewvacancy raises the position’s approved headcount by its quantity; areplacementchanges nothing.- Filling: a hire links to a vacancy; the vacancy tracks how many it still needs and closes when the count is met or by hand with a reason (audited).
- Reports (JSON): approved, filled and vacant per unit and position; open vacancies and their age.
- Permissions:
hr.grade.manage,hr.headcount.manage,hr.vacancy.*,hr.report.read.
Out of H2: applicant tracking, offer letters, succession planning, a headcount budget in RM, a promotion workflow.
H3. Leave (00840–00859)
Section titled “H3. Leave (00840–00859)”H3.1 Types and the statutory floor
Section titled “H3.1 Types and the statutory floor”-
leave_type: code, name (translation rows), kind (annual,sick,hospitalisation,maternity,paternity,unpaid,other), paid, half-days allowed, attachment required (sick and hospitalisation: a medical certificate). -
leave_rule: days per year for a service band (from month, to month) of a type; effective-dated (HD-8), applying only to grants made on or aftereffective_from. -
leave_statutory_floor: seeded, global, changed only by migration. Seeded figures — unverified, recalled from the 2022 Employment Act amendments:Kind Under 2 years 2–5 years Over 5 years Annual 8 12 16 Sick 14 18 22 Hospitalisation 60 days; maternity 98 consecutive days; paternity 7 consecutive days.
-
The floor is enforced in the database (HD-3): a rule for a statutory type is refused if any service month in its band has fewer days than the floor, checked against the highest floor inside the band (a 0–36 month band at 10 days breaks the 12-day floor from month 24).
H3.2 Balances
Section titled “H3.2 Balances”leave_ledger, append-only:grant,take,reverse,carry_forward,expire,adjust. A balance is the sum of entries; it is never stored or edited.- Grants: a River job at each leave year start (calendar year by default), idempotent per employee, type and year (unique). A first-year hire is prorated by whole months remaining.
- Carry-forward: an organization setting (maximum, expiry); default none — the Act does not require it, so it has no floor.
- Manual adjustment: reason mandatory,
hr.leave.adjust, audited. It cannot take a statutory type below the floor for that leave year (HD-7).
H3.3 Requests and calendar
Section titled “H3.3 Requests and calendar”leave_request: employee, type, from, to, half-day flags, days calculated, reason (en/ms), optional attachment (documents store). Workflowleave_request, approver the employee’s superior, resolved by the system. Approval ends incommittedand poststaketo the ledger; cancelling after approval postsreverse.- Working days exclude non-working weekdays and public holidays:
work_calendar(Monday–Friday by default) andpublic_holiday(date, name as translation rows, optional state). Holidays are not seeded — gazetted dates change every year and vary by state; the Owner enters them, with a warning when a leave year has none. - Balance check: paid annual leave cannot go negative; unpaid leave is unlimited.
- Sole-owner exception: a one-member organization’s Owner may approve their own leave with a reason, audited, like every other Phase 4 and 5 approval.
H3.4 Leave and approval routing (HD-2)
Section titled “H3.4 Leave and approval routing (HD-2)”- Unavailable seat: when a step is entered, a seat whose holder is on approved leave that day is unavailable; routing takes the existing on-vacancy walk to the next filled seat above. The decision log records “approver on leave”. No delegation is created.
- Approver goes on leave after the step was entered: a daily River job re-checks pending steps and moves those whose approver is on leave that day, logging each move.
- Nobody above is available: the step stays with the person on leave and is flagged “approver on leave, no alternate”. It is never refused — a claimant is not blocked by someone else’s holiday.
- Layering:
workflowmay not importhr.workflowdefinesUnavailable(ctx, tx, member, date) bool;hrregisters its implementation at boot, the seam accounting uses as the ledger poster.
H3.5 Privacy, retention, surface
Section titled “H3.5 Privacy, retention, surface”- Medical certificates are health data: the attachment is visible only to the approver, HR and
holders of
hr.leave.medical.read; the leave type is visible to the approver. - Leave tables are retention-bearing: no DELETE grant, like the other statutory records.
- Reports: balances per employee, leave taken by type and period, a team calendar.
- Permissions:
hr.leave.manage,hr.leave.read_all,hr.leave.calendar,hr.leave.adjust,hr.leave.medical.read; employees request under a self-service permission. - Module install:
hr(H1, H2) andleave(H3, depends onhr) join the Phase 3 registry.
Out of H3: leave encashment on termination and accrual of unused leave (payroll), shift rosters, a delegate named on the request, half-year leave years, overtime and time off in lieu.
Drift tests these lanes add (presence first, each seen failing once)
Section titled “Drift tests these lanes add (presence first, each seen failing once)”- Two employee rows for one party, or an employee row on a non-employee party, are refused (H1).
- A last day is refused while an open advance or float exists, and accepted with an Owner override who is not the requester (H1).
- An assignment beyond approved headcount is refused and accepted with the override; an acting assignment does not count (H2).
- A
newvacancy’s approval raises approved headcount once; areplacement’s changes nothing (H2). - A rule below the statutory floor anywhere in its band is refused, including a band that spans two floor bands (H3).
- The grant job run twice grants once; a rule change does not alter a past grant (H3).
- A leave request beyond the paid annual balance is refused; cancelling an approved request restores it (H3).
- A step whose approver is on leave escalates and is logged; with nobody above, it stays and is flagged (H3).
- Leave balance equals the sum of
leave_ledgerfor every employee and type (H3).
Roadmap and outside-authority additions
Section titled “Roadmap and outside-authority additions”- Tick the Phase 5 HR layer and leave items as each lane lands; payroll stays open.
- Needs an outside authority (an HR or legal adviser): the seeded Employment Act leave figures and which employees they cover (some provisions apply only below a wage threshold); the retention period for employment and leave records; whether unused annual leave carried over or lost on leaving has statutory treatment.