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.
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.
Dim_Employee. When HR terminates someone, Payroll's queries see the new status immediately — not after a sync job runs at midnight.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.
effectiveDate is when the raise happened. recordedDate is when we knew about it. They're independent. That's bitemporality — see Section 7.{
"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
}
}{
"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
}
}{
"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
}
}variableStructure.type = "EQUITY" today; the UI, vesting model, and tax handling ship with that council review.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.
| Employee | employeeId | Gross | Withholdings | Net | Cited facts |
|---|---|---|---|---|---|
| Maya Okafor | EMP-001 | $6346.15 | −$1904.71 | $4441.44 | FCC-001-03 |
| Daniel Reyes | EMP-002 | $5192.31 | −$1585.68 | $3606.63 | FCC-002-03 |
| Priya Shankar | EMP-003 | $6113.96 | −$1840.51 | $4273.45 | FCC-003-03 |
| Jordan Webb | EMP-004 | $4230.77 | −$1319.82 | $2910.95 | FCC-004-02 |
| Clara Nilsson | EMP-005 | $4923.08 | −$1511.22 | $3411.86 | FCC-005-02 |
| Marcus Bell | EMP-006 | $3769.23 | −$1192.18 | $2577.05 | FCC-006-02 |
| Hana Takeda | EMP-007 | $3000.00 | −$979.50 | $2020.50 | FCC-007-02 |
| Elena Duarte | EMP-008 | $4153.85 | −$1298.54 | $2855.31 | FCC-008-02 |
| Theo Grant | EMP-009 | $4423.08 | −$1372.97 | $3050.11 | FCC-009-03 |
| Amara Johnson | EMP-010 | $3800.00 | −$0.00 | $3800.00 | FCC-010-01 |
| Luca Ferrara | EMP-011 | $1885.71 | −$0.00 | $1885.71 | FCC-011-03 |
| Noor Al-Sayed | EMP-012 | $2027.48 | −$710.59 | $1316.89 | FCC-012-01 |
Headcount from HR. Comp from Payroll. One query. No sync.
-- 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 employeeemployee_id_payroll, no mapping table, no reconciliation step. hr.employee_id is comp.employee_id. Identity is shared, not synced.Every non-zero delta between two pay runs accounted for by cited facts. I3 invariant: unexplained residuals are surfaced, not hidden.
Priya's 8% raise effective March 1 was recorded March 14 — two weeks late. Two dates, two questions. The substrate answers all of them.
Every decision that shaped this module is on the record. The architecture didn't appear — it was ratified.