The hundred-dollar PDF
Therapy associates pay hundreds a year for software that fills out one state form. I built it for someone I care about with a spreadsheet, a template, and one API call.
Every therapy associate in California carries a quiet bureaucratic burden. To become licensed, they log thousands of supervised hours, and the Board of Behavioral Sciences wants those hours reported on its own form: the BBS 37A-525, the Weekly Log of Experience Hours. Direct clinical time in one column, non-clinical in another, supervision split into individual and group, subtotals per week, totals at the bottom, one page per ten weeks, signed by a supervisor.
An entire cottage industry exists to fill in that form. Subscription apps charge students hundreds of dollars a year for it, and they charge that because they can: the people who need it are unlicensed, underpaid, and busy counting other people’s feelings instead of their own hours.
Someone close to me was staring down that subscription while finishing her master’s. She was already keeping her hours in a spreadsheet, careful and complete. The paid app wanted her to re-enter all of it into their system so they could print it back out.
The data was already right there, in her spreadsheet, in her own handwriting so to speak. The only thing missing was the last inch: getting it onto the state’s form.
So I built the last inch.
The trick
The tool is a Google Apps Script bound to her existing spreadsheet. No new app, no new login, no data migration. She keeps logging hours exactly the way she already does; the only thing that changed is two new buttons in the menu bar, one for each clinic she works with.
The template is where it gets fun. I took the state’s official form and laid invisible text boxes over it, each holding a placeholder. Here’s what it looks like with the curtain pulled back:
The template, redrawn with the curtain pulled back. The state’s form underneath is unchanged; every gold token is an invisible text box waiting for a number.
That grid is ten weeks of slots per page. {D4} is a week’s start date, {A4} its direct clinical hours, {C4} its weekly total, down to {T6}, the grand total the Board actually audits. The double-braced ones hold her name, supervisor, and file number, read from a config tab in the sheet.
When she clicks the button, the script prompts for an optional date range, reads every week from the sheet, sorts each entry into the Board’s categories, chunks the weeks into groups of ten, and then makes a single batchUpdate call to the Google Docs API per chunk. One request replaces all eighty-seven placeholders at once. The filled document is exported to PDF and dropped in a Drive folder, named and dated, ready for a signature.
The accounting
The Board’s categories nest, and the nesting is exactly the kind of thing humans get wrong at 11pm:
| Column | Meaning |
|---|---|
| A | Direct clinical service |
| A1 | Relational sessions, a subset of A |
| B | Everything non-clinical |
| B1 | Individual supervision, a subset of B |
| B2 | Group supervision, a subset of B |
| C | The week’s total, A + B only |
Subsets must never be double-counted into the totals. That rule lives in one pure function, and it’s covered by a Jest suite that runs locally against the same file Apps Script deploys, thanks to a small module guard the Apps Script runtime ignores. Date filtering, week chunking, category math, placeholder building: each is a tested pure function. The only untested code is the thin layer that talks to Google.
What it stands on
Google Sheets as the database, Google Docs as the layout engine, Drive as delivery, Apps Script as the runtime, clasp for deploys from git. Total infrastructure cost: zero dollars, forever. She presses a button and the paperwork that funded someone’s yacht just appears.
That’s the kind of engineering I like best. Not the clever kind, the kind kind: find where the work is already being done, and build only the inch that’s missing.