Builds · Finance automation

Production Labor Report

A payroll PDF goes in. A live finance workbook comes out, with every formula still intact.

The problem

What it replaced

Twenty minutes of retyping, twenty-six times a year.

Every pay period the payroll system produces an Employee Labor Detail report: a PDF, dozens of lines, several locations. The production kitchen's labor has to end up in a workbook that tracks hours and dollars per person per period, so we can watch what it actually costs to make the product.

By hand that means reading the PDF, finding the production lines, adding up each person's regular and overtime, locating their row in four separate sections of the workbook, and typing four numbers per person into the right column. About twenty minutes when nothing is strange.

Now: drop the PDF in a folder, run one command, read what it says it did, and check that the total ties to the payroll report. The download and the tie-out are still manual, on purpose. That's where the judgment is.

The whiteboard

How the workflow runs

Four stages. The first one and the last one are mine.

01

I hand it the report

Nobody automates the login, and choosing the period is a decision.
Employee Labor Detail / PDF, one pay period
Dropped in a folder
One command

Everything it knows comes from that one file and the workbook's own layout. No database, no history, no network.

02

It reads the PDF

Text layer in, numbers out. Most of the work is deciding what to ignore.
Employee_Labor_Detail_2026-01-25.pdf Code

What it keeps

1Location 3, account 66000The production kitchen's labor, and nothing else on the report.
2The name between the twoWhatever sits between the GL code and the category, so suffixes and commas survive.
3Regular, overtime, PTO, holidaySummed separately, because they land in different sections.
4The lines with no GL code at allA second role the payroll system never mapped. Easy to miss entirely.

What it throws away

5Every other locationStores have their own report and their own workbook.
6The production managerOverhead, not production cost. Named in config, not hard-coded.
7Subtotal rowsThey look exactly like people to a parser. They aren't.
03

It writes the workbook

Four cells per person, in one column, and nothing else touched.
rows 5–19Hours

Regular plus overtime.

rows 23–37Dollars

Wages for the period.

rows 41–73PTO & holiday

Hours and dollars, kept apart from worked time.

Rows 77–91 are never written to. Total dollars is an Excel formula that adds the two sections above it. Writing a number there would replace the formula and quietly break the file for every future period, so the automation stops one section short, on purpose.
04

I tie it out

The control point. Nothing is accepted until this passes.

Does the workbook total match the payroll report?

If it does, the period is done. If it doesn't, something new showed up, usually a person the automation has never seen. That's the check that catches the whole class of quietly-missing errors, and it's why I haven't put this on a schedule.

The stages at both ends are still me. That isn't a gap waiting to be closed. Stopping it is trivial: don't run it. The workbook is untouched until the script saves, and it only ever writes one column, so a bad run is undone by clearing that column and going again.
The demo

Watch it in action

A payroll PDF, an empty column, one command, a filled workbook.

Animated walkthrough  ·  1:27 Watch the workflow, start to finish The payroll report, the empty column, the script running, the workbook filling in, and the formula surviving. Narrated, with a scrubber and chapters. Open in a new tab →

It opens on its own page because it needs the whole window. An illustration rather than a screen recording: every figure in it comes from the real run, and the software is drawn.

Below is the actual terminal output from that run, on generated data. Same numbers every time, nothing is random.

$ py run_demo.py 3. reading the PDF ------------------ reading Employee_Labor_Detail_2026-01-25.pdf excluded: Vance, Gregory Sr. split role: Okafor, Danielle (Ice Cream Maker) +21.4h / $385.20 (no GL code) split role: Okafor, Danielle (Ice Cream Maker) +3.15h / $85.05 (no GL code) pay period 01/12/2026 to 01/25/2026, 12 people 4. writing into the workbook ---------------------------- mapped 14 workbook rows pay period matches column E Alder, Marcus J 64.53h $ 1,411 Brandt, Simone 71.60h $ 1,274 Castellano, Nico 53.08h $ 908 Halloran, Beth 12.75h $ 191 -> All Other Okafor, Danielle 40.25h $ 704 Okafor, Danielle (Ice Cream Maker) 24.55h $ 470 Whitfield, Erin 80.40h $ 1,453 … 5 more wrote 11 matched + 1 into All Other saved 2026 Production Labor (SAMPLE).xlsx formulas preserved ------------------ row 20 TOTAL HOURS formula =SUM(E5:E19) row 77 TOTAL DOLLARS formula =E23+E59 row 3 period end date formula =E2+13

The three amber lines are the whole point. A manager deliberately excluded. An employee whose second role has no GL code, caught from a line the main parser structurally cannot see. And somebody with no row of their own, rolled into a catch-all instead of vanishing.

The demo builds its own inputs. It invents the payroll PDF and the workbook before it runs, so there's nothing real anywhere in the chain. pip install -r requirements.txt then py run_demo.py.

The result

What landed in the workbook

Read back out of the saved file: column E, the period the PDF covered.

EmployeeHoursDollarsPTO hrs
Alder, Marcus J64.531,4110.00
Brandt, Simone71.601,2748.00
Okafor, Danielle40.257040.00
Okafor, Danielle (Ice Cream Maker)24.554700.00split role
Reyes, Tomas61.501,1330.00
Whitfield, Erin80.401,4530.00
Castellano, Nico53.089088.00
Park, Jae-Won68.381,1900.00
Sandoval, Ruth A71.631,2320.00
Novak, Priya61.481,0280.00
Ferreira, Luz58.139530.00
All Other12.751910.00no row of their own
Total (ties to the payroll report)668.2811,94716.00
The honest version

Where the AI is additive

Nothing at runtime. Everything in the building.

At runtime: nowhere. No model reads the PDF. No agent decides anything. It's deterministic Python and it should be. The layout is fixed and the work is arithmetic. Putting a language model in this loop would add cost, latency, and a way to be creatively wrong about somebody's wages.

In building it: entirely. I don't otherwise write Python. This exists because I could describe what I wanted, watch code appear, run it, watch it fail on a real edge case, and describe the fix. The AI's contribution isn't that the workflow is smart. It's that the workflow exists, built by the person who actually understood the accounting.

That's the pattern I'd point at for most finance work: the model helps you build the tool, the tool does the job.

Controls

Where the risks are

Five components, five dials. Where this one actually sits.

ComponentThis buildWhy
LLM
none
Fixed layout, arithmetic output. Nothing to reason about.
Tools
one file
Reads one PDF, writes one workbook. No network, no credentials, no database.
Context
one period
Knows the report in front of it and the workbook's own layout. No history, no access to anything else.
Autonomy
I approve
A human triggers it, a human accepts the result. It could run on a schedule. It doesn't, because the tie-out is the control.
Skills
prescribed
Fixed procedure, named exceptions in config. It doesn't adapt. It says so instead of improvising.

The dangerous failure here was never a crash. It was a silent undercount. An employee's second role wasn't GL-mapped, so those hours appeared on a different kind of line entirely and the parser skipped them without a word. The report was just light, and it looked fine.

That's why the tie-out isn't ceremony, and why unmatched people roll into a catch-all instead of being dropped: the total has to keep tying out even when the automation doesn't recognize somebody. Blast radius is one column of one workbook. Clear it and run again.

Transferable

What's worth stealing

Two decisions that matter more than the libraries.

Write cells, don't generate files. The workbook is alive. People open it, filter it, and have trusted its totals for years. So the automation writes individual cells and leaves every formula, format and other column exactly as it found them. Four of the five sections get written; the fifth is derived by Excel formulas and is deliberately never touched. Generating a fresh export would have been far easier to build, and nobody would use the output.

Match by name, not by position. The sections looked parallel: third row of hours, third row of dollars, same person. Then two people were reordered in one section only. The automation wrote each of their dollars onto the other's row and every total still came out right, which is the worst kind of wrong. Now each section is searched by name, and a name that can't be found falls back to position and warns. Fail loudly.

In my words

My notes

Not written yet

This is where I'll write about actually building it:

  • What the first version looked like, and how long it took to work.
  • The moment I realized hours were missing, and what I thought it was at first.
  • What it feels like shipping something that touches a file other people rely on.
  • What I'd tell a finance person building their first one.