TOURWB-MOD-002 · Architecture ratified 2026-04-14 · Tour shipped 2026-04-14

Payroll

Same employees. Same substrate. No reconciliation.

Every legacy HR + Payroll combination drifts because they're two databases with an integration layer. WorkBench Payroll has no employee table. It reads HR's. The math always works because the data always matches. Here's how.

← All modulesStart the tour →
Section 2

The Sibling Mandate

Payroll doesn't have an employees table. When Payroll needs to know who someone is, it reads HR's Dim_Employee. When HR terminates someone, Payroll's queries see the new status immediately — not after a sync job runs at midnight.

WorkBench
One substrate. Two module lenses.
HR & PEOPLEEmployee status,tenure, tokens, rolesPAYROLL & COMPComp events,pay runs, withholdingsreadsreadsDIM_EMPLOYEE · FACT_STATUSCHANGE · FACT_COMPCHANGE · FACT_PAYROLLRUNThe substrate. One set of facts. No duplication.EMP-001MayaEMP-003PriyaEMP-007HanaEMP-009TheoEMP-012Noor
Payroll doesn't have an employees table. When Payroll needs to know who Priya is, it reads HR's Dim_Employee. When HR terminates someone, Payroll's queries see the new status immediately — not after a sync job runs at midnight.
What most platforms replace it with
HR DATABASEemployees_hr(owns employee_id)PAYROLL DATABASEemployees_payroll(its own employee_id)nightly syncreconciliation job
Two databases. Integration layer between them. Drift by 5 p.m. on any given Friday. This is what the substrate replaces.
“Reconcile HR to Payroll becomes a category error — there is nothing to reconcile because there is only one set of facts.”
CR-WB-PAYROLL-001 · Sibling Mandate (ratified 2026-04-14)
Same employee, two tours
This is Priya Shankar (EMP-003) — the same employee you saw in the HR & People tour. Not a copy. Not a sync. The same record, read through a different module's lens.
Section 3

Compensation Events

Every change to an employee's comp is an immutable Fact_CompChange event. The current rate you see is a derived view computed from the fact history. Pay rates, raises, promotions — all events, never mutations.

Fact timeline · newest first
Priya Shankar · EMP-003
Fact_CompChange2026-03-01
RAISE · $148,000/yr
8% merit raise — entered after prior pay period cutoff; triggers retroAdjustment next run.
Recorded 2026-03-14
Fact_CompChange2024-01-01
PROMOTION · $137,037/yr
Promotion — engineering lane lead.
Recorded 2023-12-15
Fact_CompChange2022-09-12
HIRE · $120,000/yr
Senior Engineer hire.
Recorded 2022-08-30
All events preserved forever. The employee record is a derived view; pay rate, status, and role are computed from the fact history.
Notice the two dates. effectiveDate is when the raise happened. recordedDate is when we knew about it. They're independent. That's bitemporality — see Section 7.
JSON · one toggle per event
FCC-003-03 · RAISE · 2026-03-01
{
  "factId": "FCC-003-03",
  "factType": "Fact_CompChange",
  "employeeId": "EMP-003",
  "effectiveDate": "2026-03-01",
  "recordedDate": "2026-03-14T16:30:00Z",
  "changeType": "RAISE",
  "compensation": {
    "baseAmount": 148000,
    "baseCurrency": "USD",
    "baseCadence": "ANNUAL",
    "variableStructure": {
      "type": "NONE",
      "details": null
    },
    "allowances": []
  },
  "previousFactId": "FCC-003-02",
  "reason": "8% merit raise — entered after prior pay period cutoff; triggers retroAdjustment next run.",
  "authoredBy": "EMP-001",
  "correction": {
    "isCorrection": false,
    "correctsFactId": null,
    "reason": null
  }
}
FCC-003-02 · PROMOTION · 2024-01-01
{
  "factId": "FCC-003-02",
  "factType": "Fact_CompChange",
  "employeeId": "EMP-003",
  "effectiveDate": "2024-01-01",
  "recordedDate": "2023-12-15T16:00:00Z",
  "changeType": "PROMOTION",
  "compensation": {
    "baseAmount": 137037,
    "baseCurrency": "USD",
    "baseCadence": "ANNUAL",
    "variableStructure": {
      "type": "NONE",
      "details": null
    },
    "allowances": []
  },
  "previousFactId": "FCC-003-01",
  "reason": "Promotion — engineering lane lead.",
  "authoredBy": "EMP-001",
  "correction": {
    "isCorrection": false,
    "correctsFactId": null,
    "reason": null
  }
}
FCC-003-01 · HIRE · 2022-09-12
{
  "factId": "FCC-003-01",
  "factType": "Fact_CompChange",
  "employeeId": "EMP-003",
  "effectiveDate": "2022-09-12",
  "recordedDate": "2022-08-30T10:00:00Z",
  "changeType": "HIRE",
  "compensation": {
    "baseAmount": 120000,
    "baseCurrency": "USD",
    "baseCadence": "ANNUAL",
    "variableStructure": {
      "type": "NONE",
      "details": null
    },
    "allowances": []
  },
  "previousFactId": null,
  "reason": "Senior Engineer hire.",
  "authoredBy": "EMP-001",
  "correction": {
    "isCorrection": false,
    "correctsFactId": null,
    "reason": null
  }
}
Deferred
Equity grants — coming with CR-WB-EQUITY-001. The schema supports variableStructure.type = "EQUITY" today; the UI, vesting model, and tax handling ship with that council review.
Section 4

Pay Run Anatomy

Every pay run cites the facts that drove it. If we ever ask 'why was this paycheck this amount?' the substrate answers with named events, not a guess. The math is legible, start to finish.

Current pay run · Fact_PayrollRun
Priya Shankar · EMP-003
Biweekly · regular run
Pay period 2026-03-152026-03-28 · payDate 2026-03-28
Gross
$6,113.96
includes retroAdjustment $421.65 — late-recorded raise catch-up
Minus withholdings
FICA · Social Security· US-FEDERAL
$379.07
FICA · Medicare· US-FEDERAL
$88.65
Federal income tax· US-FEDERAL
$917.09
State income tax· US-STATE
$305.70
Benefits
$150.00
Total withholdings: $1,840.51
Net deposited
$4,273.45
acct-****-003
Source facts cited by this run
This run cites the Fact_CompChange that determines Priya's rate, plus a retroAdjustment line for the two weeks her raise was effective but not yet recorded. Click through to see the source event.
See full pay run — all 12 employees
EmployeeemployeeIdGrossWithholdingsNetCited facts
Maya OkaforEMP-001$6346.15−$1904.71$4441.44FCC-001-03
Daniel ReyesEMP-002$5192.31−$1585.68$3606.63FCC-002-03
Priya ShankarEMP-003$6113.96−$1840.51$4273.45FCC-003-03
Jordan WebbEMP-004$4230.77−$1319.82$2910.95FCC-004-02
Clara NilssonEMP-005$4923.08−$1511.22$3411.86FCC-005-02
Marcus BellEMP-006$3769.23−$1192.18$2577.05FCC-006-02
Hana TakedaEMP-007$3000.00−$979.50$2020.50FCC-007-02
Elena DuarteEMP-008$4153.85−$1298.54$2855.31FCC-008-02
Theo GrantEMP-009$4423.08−$1372.97$3050.11FCC-009-03
Amara JohnsonEMP-010$3800.00−$0.00$3800.00FCC-010-01
Luca FerraraEMP-011$1885.71−$0.00$1885.71FCC-011-03
Noor Al-SayedEMP-012$2027.48−$710.59$1316.89FCC-012-01
Pay period 2026-03-152026-03-28. Cited facts preserved per row.
Section 5

Live Burn Rate

Headcount from HR. Comp from Payroll. One query. No sync.

Annualized payroll cost — as of 2026-04-14
$1,265,800
HR
Active headcount
11
from ROSTER (active + on-leave)
Payroll
Annualized base comp
$1,265,800
latest Fact_CompChange per employee
Payroll
Annualized variable
$0
bonus / commission / equity (none in V1)
See the SQL
-- One query. Two substrates. No sync. No nightly job.
SELECT
  COUNT(DISTINCT hr.employee_id)                    AS active_headcount,
  SUM(comp.annualized_base)                         AS annualized_base_comp,
  SUM(comp.annualized_variable)                     AS annualized_variable
FROM   hr.dim_employee                              AS hr
JOIN   payroll.dim_current_comp                     AS comp
  ON   comp.employee_id = hr.employee_id            -- same identity, no join key translation
WHERE  hr.status_parent IN ('Active', 'On Leave')   -- HR is authoritative for status
  AND  comp.as_of_date  =  CURRENT_DATE;            -- Payroll's latest fact per employee
The interesting line is the JOIN. There is no employee_id_payroll, no mapping table, no reconciliation step. hr.employee_id is comp.employee_id. Identity is shared, not synced.
This number is computed live from HR and Payroll facts in the same query. There is no sync. There is no nightly job. There is one substrate.
Section 6

The Variance Explainer

Every non-zero delta between two pay runs accounted for by cited facts. I3 invariant: unexplained residuals are surfaced, not hidden.

Prior period · 2026-03-01 → 2026-03-14
$47,793.73
Current period · 2026-03-15 → 2026-03-28
$49,865.62
Variance
+$2,071.89
Unexplained residual: $0.00
I3 invariant · zero residual in steady state
Section 7

Bitemporal in the Wild

Priya's 8% raise effective March 1 was recorded March 14 — two weeks late. Two dates, two questions. The substrate answers all of them.

Scenario · as of March 14, 2026
Priya's 8% raise — effective Mar 1, recorded Mar 14
The raise took effect March 1, but the comp event wasn't entered into the substrate until March 14 — two weeks after the prior pay-period cutoff. Three questions, three different right answers, all from the same facts.
Substrate answer
$137,037 base
Asked on March 1 itself: the only Fact_CompChange the substrate had on file was FCC-003-02 (promotion, effective 2024-01-01). The raise existed in real life but had not yet been recorded.
Cited fact: FCC-003-02 — last comp event with recordedDate ≤ 2026-03-01
Two dates, two questions. The substrate answers all of them without rewriting history.
Section 8

Build Receipts

Every decision that shaped this module is on the record. The architecture didn't appear — it was ratified.

Predecessor chain — what had to be ratified first
FactlayerModule CharterModule BuildHR & PeoplePayroll & Comp (you are here)
Substrate ratified. Tour shipped. Module build queued.
Next tour · WB-MOD-003 · Time & Attendance
The third side of the triangle
Time reads from the same substrate — the hours that turn compensation into actual gross. Three modules, one identity layer, no reconciliation.
Continue →