A sales team logs its leads in shared Google Sheets: one tab per agent and one block per day. Each lead is about twenty "Label: value" rows typed by hand. That layout is easy to fill in on a call, but very hard to work from. Phone cells hold two numbers. Rate cells hold a whole pricing quote. Rep names come with notes attached. Monthly volume might be three dollar figures separated by semicolons.
Before reps can call anyone, someone has to copy every new lead into a flat list, clean each field, work out the caller's time zone, look up the business's industry in another sheet, and sort the list so the right leads come first. This is repeated every day for every agent. Each copy is a chance to paste the wrong row, miss a lead or create a duplicate.
The goal was to remove that copying without changing how the agents work. The input sheets stay exactly as they are, and the automation adapts to them.
Two small Python workers do this. One turns the hand-made blocks into a clean table per day. The other merges the past week of those tables into one master sheet, tags each lead with its industry or time zone, and puts Non-POS leads first. Both are built so they can run again and again without producing duplicates.
Parsing a layout built for people
The input isn't a table. Groups of three columns repeat across the sheet, dates sit inside the data, and each lead is a run of labelled rows. The parser finds the header triplets, works out each date's row range from where the next date starts, then reads each block as a state machine (a number next to Lead: starts a record; any Label: row adds a field).
Tradeoff: the parser depends on agents keeping the triplet headers and the label spelling. If a block doesn't match, it is skipped rather than guessed at.
Staying inside the API quota
Polling every 3 seconds across several spreadsheets would quickly exceed Google's per-minute limit. Every call goes through one wrapper: a sliding-window limiter capped at 55/min, a 0.5 s gap after each success, and exponential backoff on 429 errors. Reads are grouped into ranges: a whole tab in one call, then one call per date block, instead of reading cell by cell.
Tradeoff: Worker A still writes one row per call, which keeps upserts simple but uses more of the quota than batching would.
Running repeatedly without duplicates
Serial numbers start at 1 in every agent's tab, so they aren't unique. Worker A builds the key {SourceTab}-{SerialNo} and updates the matching row instead of appending. Worker B rebuilds its window, keeps rows older than the window, dedupes on the whole row, and compares the result with the existing sheet; if nothing changed, it doesn't write.
These came from real bugs in the commit history: "fix the duplication issue", "fix overwriting issue for output sheet", "fix the update part not to over-ride".
Dates with no year, in many formats
Agents write 15-Dec-2025, 12/15/2025 or Dec-15, and tab names have no year. Worker A tries 9 formats. Worker B also handles Sheets date serials, and pads Jan-9 to Jan-09 so the sort order is correct (that fix is in the history). The 7-day window is measured from the newest date in the sheet, not from today, so a sheet that is a few days behind is still processed.
Known limitation: dates without a year are assumed to be in the current year, which is fragile across December → January.
Time zone without a lookup service
Reps need to know when they can call. The area code is mapped against a hard-coded table of 414 North American codes in 7 zones, so there is no network call and nothing extra to pay for. An unknown code falls back to Central.
Tradeoff: area codes that span two zones return the main one, and the table has to be updated by hand when new codes are added.
Joining two sheets with no shared ID
Industry data lives in a separate assignment sheet that has no lead ID. The join key is (normalized date, digits-only phone), and both sides are normalized the same way. When there's no match, the cell shows "Pacific - No Industry" instead of a blank, so the rep still has useful information.
The header row is also read at run time, so ops can move columns in the master sheet without asking for a code change.