Twenty-five emails, one open house, no spreadsheet
Turns a flooded inbox of viewing enquiries into one sorted, live dashboard.
- Python
- Gmail API
- Claude API
Where this could work for other businesses
The clearest fit: any letting or estate agency juggling more than one live listing at once. Instead of one person mentally tracking who’s confirmed for which viewing across a dozen simultaneous properties, each listing gets its own tracker — a label applied after every reply, a dashboard that sorts itself. An agency handling twenty active listings has this problem at twenty times the scale I had it, and the fix is the same one, just run once per property.
Recruitment has the same shape. A recruiter scheduling interview slots across dozens of candidates faces an identical triage problem: who’s confirmed, who needs a nudge, who’s gone quiet. Swap “arrival time for a viewing” for “interview slot,” and the same read-classify-track loop applies without touching the underlying mechanism.
Any small business running scheduled site visits, open days, or group sign-ups — a contractor coordinating quotes, a course provider confirming attendees for a taster session — has the same underlying problem: replies come in unstructured, over days, in whatever format people happen to write in, and somebody has to turn that into “who’s actually coming.”
Strip the property domain away entirely and the pattern is: apply a label once you’ve handled something manually, let AI read everything under that label and turn it into a structured status, keep a permanent lock on anything a human corrects. That’s a general triage mechanism for any business drowning in one-off replies to a mass communication — not just viewings, any RSVP-shaped problem.
The problem
I was letting an apartment. I listed it, and inside a few days had around twenty-five inquiries, each one a reply I’d sent with open house details and a request to confirm. That left a growing pile of email threads in different states — some confirmed with a time, some said yes with no time, some hadn’t replied, a few had declined — and no way to see the shape of all of it at once without opening threads one by one.
It got messier than a simple inbox problem. Some inquiries arrived as differently-formatted notifications from the listing platform itself. Some people replied days later, after I’d already sent a reminder — to the reminder, not the original thread. No single filter was going to catch every version of “someone told me something about coming to my open house.”
The goal
Turn a label applied once in Gmail into a live, sorted dashboard of who’s actually coming and when — without maintaining a spreadsheet by hand.
What it does
![]()
After I reply to an inquiry, one Gmail label is the entire manual step. Everything after that is automatic: the tool reads every labelled thread, sends the conversation to Claude, and gets back a structured record — name, contact details, a summary of what they’re looking for, a confirmation status, and an arrival time if one was given.
Cards are sorted into status groups, and confirmed guests get pulled into an arrival timeline so I can see the whole evening’s shape in one glance rather than reconstructing it from a dozen threads:
![]()
Declined replies get their own section rather than disappearing, so a “no” is still visible and accounted for, not just quietly dropped:
![]()
And because not every inquiry gets a prompt reply, a separate section surfaces platform notifications that arrived but were never labelled — the ones I’d otherwise forget I hadn’t answered yet:
![]()
One command sends reminder emails to everyone still unconfirmed, with a confirmation prompt before anything actually goes out. And because I wanted to check the dashboard from my phone without running a server, there’s an option to encrypt it and publish it to a static host instead.
The build
Built with Claude Code, with the same boundary that shows up across every one of these tools: the AI reads and classifies, it never acts. It doesn’t send anything, doesn’t decide who gets a reminder, doesn’t touch a record once a human has corrected it. Every consequential action — labelling a thread, sending a batch of reminders, publishing the dashboard — stays a deliberate step I take.
Getting full coverage meant realising a single label wasn’t enough. One query for labelled, already-replied threads misses two real cases: platform notifications that arrived but were never labelled, and replies to a reminder email, which start a new thread ID the labelled-thread query has never seen. The fix was running three separate, narrower queries instead of one broad one — labelled replies, unlabelled platform notifications, and follow-up activity from people already in the system — each feeding the same classification step and the same database.
Classification itself is schema-constrained: Claude returns a defined structure, not free text, which the code can parse directly rather than trying to extract meaning from a paragraph. Getting consistent results took defining the status taxonomy explicitly and giving the model clear boundaries between adjacent categories, rather than assuming it would share my intuition for where “I’ll try to make it” lands. And because any classifier will occasionally get an ambiguous reply wrong, every record has a manual-override lock: once corrected by hand, a later refresh can never silently overwrite it. That’s the single most load-bearing design decision in the whole tool — anything that re-classifies against changing data needs a permanent way to record a human’s correction.
Remote access uses the same pattern I’ve used elsewhere for personal dashboards holding sensitive data: encrypt the whole page client-side before it’s published anywhere, and decrypt it in the browser with nothing but a password the server never sees. That turns a static host with zero infrastructure into something safe enough to hold real personal data.
Problems & solutions
Symptom: the first version only ever saw threads I’d manually labelled, and missed platform notifications sitting unanswered in the inbox. Diagnosis: a single query can only see what it’s built to see. Fix: a second query specifically for the unlabelled-notification case, surfaced in its own section rather than mixed into the main list.
Symptom: people who replied to a reminder email, rather than the original thread, never showed up anywhere. Diagnosis: a reply-to-a-reminder starts a new thread ID, invisible to a query built around the original label. Fix: a third query watching for new activity from anyone already known to the system, regardless of which thread it lands in.
Symptom: early classifications were inconsistent on ambiguous replies — the same kind of “maybe” landed in different categories depending on phrasing. Diagnosis: the model wasn’t told precisely enough where the boundaries between categories sat. Fix: write the taxonomy explicitly into the prompt, with the distinction between adjacent statuses spelled out rather than assumed.
Symptom: a routine refresh from Gmail would have silently overwritten a record I’d manually corrected. Diagnosis: without an explicit lock, “refresh from source” and “respect human corrections” conflict by default. Fix: a per-record override flag that permanently exempts a corrected entry from being overwritten by any future refresh.
The stack
| Component | Why |
|---|---|
| Python | The whole pipeline — inbox queries, classification calls, dashboard generation |
| Gmail API | Read access to labelled threads and notification emails, plus sending reminders |
| Claude API | Reads unstructured email text and returns a structured status for every thread |
Results
Twenty-five threads processed in under two minutes, and the database held up across repeated daily runs without duplicating records or losing a manual correction. The arrival timeline was the single most useful output — knowing the shape of who was arriving when made the evening genuinely easier to plan, instead of re-reading a dozen threads the morning of. A batch of reminder emails went out with one command and one confirmation, tracked so a second run never double-sent. And the unanswered-notification view caught inquiries I’d genuinely filed under “reply later” and forgotten — both got a reply before the deadline that mattered.
What’s next
Making the open-house date, time, and other per-run details configurable rather than fixed would let the same tool be reused for a future listing without touching the source. A lightweight browser-based edit view would let someone other than the original builder — a letting agent, a co-landlord — correct a record without running a command. And a small set of test threads with known-correct classifications would turn prompt tweaks into something measurable, rather than checked by eye.