I own a home-services company in Michigan. A crew, trucks, real customers who call and complain. I am also its only engineer — every piece of software that runs it is mine, and it has invoiced $216,291.
One Monday morning, a cron job I wrote quietly deleted two men's pay.
The Monday morning cron
Week 29 — the week of 13 July. Two of my guys cut seven yards between them. Each was owed $87.50.
Mowing work orders in my system are generated ahead of the season and cleaned up afterwards. A scheduled job runs Monday at 6am and deletes work orders whose scheduled date has passed. They are checklist items, stale ones clutter the board, and I wrote it that way on purpose.
What I had not thought about is that somewhere between generation and cleanup, a work order stops being a checklist item. The moment a crew member's name and their pay get recorded against it, that row is a pay record. My cleanup job could not tell the difference, because I had never told it there was one.
It deleted the week. Seven yards, two men, and the only surviving trace of what they were owed was a note in a file I keep for the AI agents that work on this codebase. I rebuilt the records by hand from that note — and the Monday 6am cron would have deleted them a second time if I had not also fixed the job first.
The fix is four lines: keep any work order carrying a payout or a paid-contractor field, regardless of date. The lesson is not four lines. The lesson is that a data model has lifecycle states you did not declare, and a deletion job is where you find out.
What this actually is
Haslett Handyman is a real home-services business. It is not a side project, a demo, or a SaaS built for imaginary users.
I am the owner and I am also the only engineer. Everything that runs the business — lead intake, quoting, scheduling, work orders, invoicing, crew payouts, bookkeeping — is software I wrote and operate.
| The machine | Count |
|---|---|
| Invoiced | $216,291 |
| Collected | $191,983 |
| Bank transactions auto-categorized | 3,235 |
| Admin hours removed | ~140 |
| Lines of code (application + automation) | 55,035 |
| Python automation modules | 83 |
| Scheduled jobs running unattended | ~35 |
| Engineers | 1 |
The application is AWS-native: Lambda behind API Gateway, DynamoDB single-table, Cognito for auth, Terraform for all of it. A React web app for the office and a React Native app the crew use in the field. That part is unremarkable — it is the boring, correct choice and I would make it again.
The interesting half is the automation layer: 83 Python modules that run on a schedule on a Mac Mini in my house. They read bank transactions, generate invoices in QuickBooks, chase quotes that went quiet, reconcile work orders against payments, calculate crew payouts, and send real email to real customers.
Nearly every module refuses to do anything until you pass it --commit. That convention was not foresight. It is scar tissue.
The watcher that ran into the void
A job that watches for new bid opportunities had been loaded and firing every twelve hours for months. Working perfectly. Finding bids.
It sent every one of them to a hard-coded Gmail address. The CRM reads a different mailbox entirely. Every bid it ever found landed somewhere nobody was looking.
Nothing errored. Logs were clean. The job reported success every twelve hours for months, because it had succeeded — at the thing I told it to do, which was not the thing I wanted. A cron job that runs and does nothing useful is indistinguishable from one that works, unless you go and check the far end. I now check the far end.
The page footer that ate $66,438.97
My bookkeeping importer reads PDF bank statements. A function called is_cash_withdrawal() decided a line was a cash withdrawal if the text "statement of account" appeared anywhere in it.
The importer appended the statement's own page footer — "…Statement of Account Jan 1 2026 to Jan 31 2026" — to every single line it imported.
Fifty deposits worth $66,438.97 were stored as the literal string "Deposit." The payer was right there in the PDF and got thrown away. For months my books reported a business that collected fine and could not say what any individual job cost.
I had built an operating score to tell me how healthy the business was. Its cash pillar read 58.4 out of 100. When I found this bug and re-imported, the score dropped — and the drop was the finding, not a regression. The number had been reporting on data it could not see.
The review gate I nearly shipped
This is the one I am least comfortable writing down.
I wrote a module to ask happy customers for Google reviews. It filtered the ask list: anyone with a callback, a go-back, a dispute, or a write-off was dropped before the message went out. The message itself said "if anything wasn't right, tell me instead."
That is review gating. It violates Google's policy in both of the forms Google names it, and for a business whose Google listing is the top of the funnel, the penalty is removal of the entire profile.
It never sent a single message — there was no scheduled job wired to it. But it was one cron entry away from running. I found it, removed the gate, and rewrote eligibility to turn only on whose customer it is and whether the work is actually finished. Everyone gets asked or nobody does.
I did not build it to cheat. I built it because filtering felt like politeness, and I did not think about it hard enough. That is how most of this happens.
What actually transfers
- Silence is not success. Three of the four failures here produced no error, no alert, and clean logs. The expensive bugs in automation are not crashes — they are jobs cheerfully doing the wrong thing on schedule.
- Deletion is where undeclared state surfaces. Every row in your system has a lifecycle. You have declared some of it. A cleanup job finds the rest, in production, on a Monday.
- Dry-run by default, commit by flag. Every module that touches money or sends email does nothing until explicitly told to. This costs one keystroke and has saved me more than everything else combined.
- Instrument the far end, not the job. "Did it run" is the wrong question. "Did anything arrive where a human will see it" is the right one.
- Your metrics can be confidently wrong. A score computed from data with a silent gap will report a healthy business. When a fix makes a number worse, that is usually the number becoming honest.
- Ethical bugs look like features while you are writing them. The review gate was the most dangerous thing in this codebase and it was also the most reasonable-seeming.
Why I think this is worth reading
Most software is written for users the author has never met, to solve problems the author has never had. I get to do the opposite. When the invoicing is wrong, a customer calls me. When payroll is wrong, I hand cash to someone who is standing in my driveway and knows exactly what he is owed.
That loop is brutally short and it does not accept excuses. There is no product manager between me and the consequence, no sprint boundary to hide behind, no "we'll pick it up next quarter." The bug that deleted payroll was found by the men whose pay it deleted.
I do not think this makes me a better engineer than anyone reading this. I think it means I have been taught a narrower and more specific set of lessons than most people get, and the ones above are the ones I would want if I were starting over.
The industry is currently arguing about whether AI agents can do real work. I do not have an opinion worth publishing on that. What I have is a Mac Mini in my house running thirty-five scheduled jobs that bill real customers and pay real people, and a list of everything that went wrong while I got there.




