Skip to content

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.

  • 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: escalate walk). 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.
  • party (00009): kind includes employee; every organization_member has a party_id. Claims, advances and payouts bind to the party, not to the login. organization_member also carries employee_no, cost_centre, active.
  • Position tree (00018, platform/orgstruct): org_unit, position (seats that outlive their holder; reports_to_position_id, materialised path), member_position (dated assignments, is_primary, exclusion constraint, withdrawal 00023). 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, with on_vacancy (escalate or refuse) and max_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, decision authority_via = sole_owner_exception.
  • The committed semantic (“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 for effect).
  • 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_id unique, kind employee): 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_id already 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_no moves to employee. 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_override row and an audit entry.
  • On the last day: seat assignments end (the seat becomes vacant and on_vacancy: escalate takes 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 hired event; the employee number is kept, not reissued.
  • Import: an employees kind 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_salary NUMERIC(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_override row and an audit entry.
  • vacancy: position, kind (replacement, new), quantity, needed-by, reason; its own vacancy workflow, approver from grants, never chosen by the requester. Approval ends in committed. Approving a new vacancy raises the position’s approved headcount by its quantity; a replacement changes 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.

  • 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 after effective_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).

  • 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).
  • leave_request: employee, type, from, to, half-day flags, days calculated, reason (en/ms), optional attachment (documents store). Workflow leave_request, approver the employee’s superior, resolved by the system. Approval ends in committed and posts take to the ledger; cancelling after approval posts reverse.
  • Working days exclude non-working weekdays and public holidays: work_calendar (Monday–Friday by default) and public_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.
  • 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: workflow may not import hr. workflow defines Unavailable(ctx, tx, member, date) bool; hr registers its implementation at boot, the seam accounting uses as the ledger poster.
  • 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) and leave (H3, depends on hr) 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 new vacancy’s approval raises approved headcount once; a replacement’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_ledger for every employee and type (H3).
  • 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.