Payroll does not match the accounting system
Payroll runs person by person. The accounting system receives totals. When someone asks why one department went over, the answer takes a spreadsheet and a morning. This page shows where the detail is lost, and the one query that drills through it.
◆ What goes wrongthe plain version, before any technical detail.
Payroll calculates one line per person for every kind of pay: base salary, overtime, a bonus, each deduction, each tax. Thousands of lines a period in a mid-size company.
The accounting system never sees them. The lines are summed by account and posted as a single journal entry. Correct totals, nothing underneath.
Every departmental question after that point is answered by hand, and the working is thrown away each month.
◆ Why the two numbers driftfour causes, each one a wrong answer rather than an error message.
| Cause | What actually happens |
|---|---|
| Net pay is not the cost | The employer also pays its own payroll taxes, its benefit share, and its retirement match. Those sit in the journal and never appear in net pay, so reconciling on it compares two different things. |
| Cost centers that were never assigned | Payroll accounting maps each earning and deduction to an account and a cost center. Where the mapping is missing the amount still posts, so the company total stays right while department numbers go quietly wrong. The Cost Center & Company page carries the code map. |
| Two different dates | A period ending 31 May can post in June. Filter payroll by pay period and the ledger by posting date, and the totals differ by exactly one run. |
| Corrections after the fact | Payments outside the normal cycle come in four kinds: one already made elsewhere, an urgent extra, a reversal, a reissue. The wrong kind duplicates or drops accounting entries, and the ledger takes whatever was chosen. |
All four share a shape. Nothing errors. The report runs, looks finished, and is wrong. That is the expensive kind, because it surfaces only when somebody who knows the business says that cannot be right.
◆ What the delivered reports give youand the two places they stop.
| Delivered report | What it does, and where it stops |
|---|---|
| Gross to Net | Walks earnings down through deductions and taxes to net pay. Correct, and the wrong figure for a ledger tie, per the first cause above. |
| Payroll Results Summary | Period totals per pay group. Good for a run check, one period at a time, so trend and variance work stays manual. |
| Pay Results by Organization | Result detail by organization, with a costing variant adding cost center and fund. The closest delivered report to what is needed, and it lives inside Workday, so a ledger held elsewhere cannot be joined to it. |
| Journal preview | Shows the entry before it is committed. A control worth using, and a one time check, not a record you can query next quarter. |
None of these is poor. A tie needs both sides in one place, and one side is always somewhere else.
◆ What we buildevery line kept, and the total proved.
One row per person, per pay component, per period, landed on the customer's own cloud. The ledger lands beside it. The totals are compared on a schedule and the result is kept, so last quarter's check can still be produced.
Grain, the level of detail one row represents, is the whole decision. A row at person and pay component adds up to any question: by department, by pay type, by month, by legal entity. A row that arrives summarised cannot be taken apart again.
◆ The drill, in one querythe account tie and the department detail returned together.
Two grains in one pass. Grouping sets tell the database to aggregate at more than one level in a single query, so the account subtotal and the cost center breakdown come back together rather than as two reports somebody has to line up.
-- one query, three levels: grand total, account, cost center within account
WITH lines AS (
SELECT c.ledger_account,
COALESCE(cc.cost_center, 'none') AS cost_center,
f.amount
FROM fct_payroll_result_line f
JOIN dim_date d ON f.accounting_date_key = d.date_key
JOIN dim_pay_component c ON f.pay_component_key = c.pay_component_key
LEFT JOIN dim_cost_center cc ON f.cost_center_key = cc.cost_center_key
WHERE d.fiscal_period = '2026-05'
AND c.affects_ledger
),
payroll AS (
SELECT ledger_account, cost_center, SUM(amount) AS payroll_amount
FROM lines
GROUP BY GROUPING SETS ((), (ledger_account), (ledger_account, cost_center))
),
ledger AS (
SELECT a.ledger_account, SUM(j.amount) AS ledger_amount
FROM fct_gl_journal_line j
JOIN dim_date d ON j.accounting_date_key = d.date_key
JOIN dim_account a ON j.account_key = a.account_key
WHERE d.fiscal_period = '2026-05'
AND j.source_system = 'Payroll'
GROUP BY GROUPING SETS ((), (a.ledger_account))
)
SELECT COALESCE(p.ledger_account, l.ledger_account, 'total') AS ledger_account,
COALESCE(p.cost_center, 'all') AS cost_center,
p.payroll_amount,
CASE WHEN p.cost_center IS NULL THEN l.ledger_amount END AS ledger_amount,
CASE WHEN p.cost_center IS NULL
THEN COALESCE(p.payroll_amount, 0) - COALESCE(l.ledger_amount, 0)
END AS difference
FROM payroll p
FULL OUTER JOIN ledger l
ON p.ledger_account IS NOT DISTINCT FROM l.ledger_account
AND p.cost_center IS NULL
ORDER BY CASE WHEN p.ledger_account IS NULL THEN 0 ELSE 1 END,
1,
CASE WHEN p.cost_center IS NULL THEN 0 ELSE 1 END,
2
Three things carry the weight. Grouping sets produce a null cost center on subtotal rows, so unmapped lines are coalesced to none before grouping: none is a real gap in the data, null is a subtotal, and conflating them hides the gap inside its own total. The full outer join keeps an account that exists on one side only, which is the failure an inner join silently swallows. And both sides filter on the accounting date, which removes the two-dates cause above.
Sample output, May 2026. The account rows open and close; 6000 is shown open:
| ledger_account | cost_center | payroll_amount | ledger_amount | difference |
|---|---|---|---|---|
| total | all | 4,663,142.08 | 4,663,142.08 | 0.00 |
| −6000 · Salaries and wages | all | 3,997,550.00 | 3,997,550.00 | 0.00 |
| 6100 · Sales | 6100 | 1,842,500.00 | ||
| 6120 · Technology Delivery | 6120 | 1,208,750.00 | ||
| 6300 · Support | 6300 | 937,880.00 | ||
| none · Unassigned | none | 8,420.00 | ||
| +6010 · Employer payroll taxes | all | 305,812.58 | 305,812.58 | 0.00 |
| 6100 · Sales | 6100 | 140,932.31 | ||
| 6120 · Technology Delivery | 6120 | 92,469.38 | ||
| 6300 · Support | 6300 | 71,746.76 | ||
| none · Unassigned | none | 664.13 | ||
| +6020 · Employer benefit cost | all | 359,779.50 | 359,779.50 | 0.00 |
| 6100 · Sales | 6100 | 165,825.00 | ||
| 6120 · Technology Delivery | 6120 | 108,787.50 | ||
| 6300 · Support | 6300 | 84,409.20 | ||
| none · Unassigned | none | 757.80 |
Every level rolls up. Each account's cost center rows add to its own subtotal, the three account rows add to the grand total, and the same four departments appear under every account, so one department's full cost is three clicks, not three reports.
The empty cells are the point. The ledger holds no cost center, so those columns can only ever populate at the account level. The 8,420.00 on the none row belongs to a department nobody can name, and the total still ties. Sample values are illustrative, never client data.
- pay component
- One kind of pay or deduction: base salary, overtime, a tax, a benefit premium.
- gross pay
- Total earnings before any deductions or taxes are taken out.
- net pay
- What reaches the employee's bank account, after deductions and taxes.
- employer cost
- Gross pay plus the employer's own taxes and benefit share. What the ledger carries.
- journal entry
- The accounting record of a transaction, posted to the ledger.
- ledger account
- The bucket a cost is recorded in, such as salaries or employer taxes.
- cost center
- The department or team a cost is charged to.
- accounting date
- The date a transaction is recognised in the books. Not always the pay period end.
- off cycle
- A payment made outside the normal pay run: prior payment, urgent payment, reversal, reissue.
- grain
- The level of detail one row represents. One row per person per pay component per period, here.
- fact
- A table of measured amounts, such as pay, joined to descriptive tables.
- dimension
- A descriptive table: who, which department, which date, which pay type.
- full outer join
- A join that keeps rows present on either side, so nothing is silently dropped.
- grouping sets
- An instruction to aggregate at several levels in one query, giving subtotals and detail together.
- subtotal row
- A rolled-up line. Here it carries the word all, and is not the same as a missing value.
- tie
- A check that two independently built totals agree.