003 · AUTOMATION- Python · Telethon · Vision API · Google Sheets

Image to spreadsheet capture

March 2026 to present

The problem

A team was posting information into a private channel as screenshots, and someone was reading each one and typing it into a spreadsheet by hand. Not a huge job on its own, but a few minutes each, several times a day, forever.

Work like this rarely gets automated, because the cost of building the automation looks larger than the cost of continuing. The calculation only changes if the build is genuinely small, so the constraint wasn't technical. It was that this had to be finished in a day or it wasn't worth doing at all.

What we built

A single Python process holding an open connection to the channel, reacting to messages as they arrive rather than polling for them. Text fields are pulled locally with pattern matching. Anything only visible in the image goes to a vision model with a constrained prompt that forces structured output.

Rows are written to the live spreadsheet through the API, addressed to specific cells rather than appended, so the tool works with an existing sheet layout instead of demanding a new one. Later edits to a message are picked up too, matched back to the original row by a stored identifier, so a correction posted after the fact updates the record rather than creating a second one.

The whole thing is 518 lines in one file. It runs on a small server as a supervised service under a non-privileged user, restarts itself on failure, and has used 55 minutes of processor time in four months.

The bug that overwrote live data

The tool finds the next empty row before writing. That sounds like the simplest part of the job, and it was the part that nearly destroyed the sheet.

The spreadsheet library's method for reading a column drops trailing empty cells rather than returning them. So a sheet with data down to row 98 and nothing after returns a list that ends at 98, with no indication that rows 99 onward exist and are empty. The loop looking for the first blank entry found nothing, fell through, and returned its starting position. Which was row 11. Occupied.

It didn't error. It wrote a valid row into a valid cell, on top of a record that was already there.

The fix was to stop asking for a column and start asking for a fixed range, which returns a grid with the empty cells preserved and present. Same information, an honest shape.

Nothing was wrong with the logic, the API call, or the data. The library returned exactly what it documents. The assumption underneath, that a column read gives you the whole column, was the thing that was wrong, and it was invisible until a sheet happened to have empty rows at the bottom.

By the numbers

119
days running, one process, zero restarts
41MB
memory footprint
5s
from image posted to row written
55min
of CPU used in four months
← BACK TO WORK

Start a project

START A PROJECT →