Personal project · System & data model
An agent that reads a job-search inbox, works out what each email actually is, and clears the noise out — under one rule: it is allowed to leave junk in your inbox, but it is never allowed to bury an interview.
No jargon. Here is what happens from the moment you ask it to tidy your inbox to the moment an email leaves it — and why it gets cheaper every time it runs.
A week, usually — "the last 7 days". It searches your inbox the way you would, and it caps how much it will archive in one go.
It owns twelve labels. Anything already wearing one is excluded from the search itself, so a daily run only looks at genuinely new mail.
Most of your inbox isn't. Those emails are left completely alone — not labelled, not touched. It would rather ignore an email than guess at one.
First your own corrections, then its pattern rules, then templates it has been taught, then senders it knows. A job alert from SEEK looks like every other job alert from SEEK — that costs nothing to know.
And only if you switched the AI on for that run, and only until it hits the call budget you set. There is no way for a run to quietly spend money you didn't expect.
Not sure is a real answer, and it's the default one. The email gets a Review Manually label and stays exactly where it is. An unsure email is never archived — there is no code path that allows it.
Every label it intends to apply, every email it intends to archive, listed out — then it waits. Say no and nothing happens at all. Nothing is ever deleted; archiving only means taking it out of the inbox.
Every action is logged against a run ID, so a whole run can be reversed.
And once a sender has been sorted the same way enough times, the agent can propose a rule
for it — you approve it, and that sender never needs the AI again.
↺ loops back to step 4
An agent clearing your inbox has an obvious temptation: archive more, because archiving is the visible win. These are the guardrails against that, each in one sentence. Every one is a rule in code with a test behind it — not a polite request in a prompt.
Anything it can't place confidently becomes Review Manually and stays in the inbox. The fallback isn't a guess — it's leaving the email alone.
If an email shows interview, assessment, document or offer language, every archive-shaped rule is suppressed. A noisy sender doesn't get to hide the one message that mattered.
A model answer passes the same gate as everything else: the category must be archivable and confidence at least 0.80. A category it doesn't recognise is forced to Review Manually.
"Thank you for your application" that also says "confirm your residency" is a task, not a receipt. A specific request in the body overrides a reassuring subject line.
Archiving removes one label. Everything stays in All Mail, fully searchable. The permission it holds can't send mail or delete it — that isn't policy, it's the scope it was granted.
Each run has an ID and every label and archive is recorded with the email's prior state. One command replays a run backwards and makes the agent forget it ever touched those emails.
The AI is off unless you turn it on, and capped when you do. Once the budget is gone the remaining emails become Review Manually rather than more spend.
A dry run writes nothing at all. A real run prints the full plan and waits for a yes — and if you abort, no state is written and nothing is remembered.
Every email runs through a fixed funnel. Most of the inbox is eliminated at the relevance gate for free and never touched again; only job-search mail reaches the classifier, and only confident, archivable classifications reach the inbox itself.
The distinctive part. Four local sources are exhausted before a single API call, and the model isn't reached at all unless it was explicitly switched on for that run. One gate sits across all six tiers — including the model's own answer.
Figure 1 — The six-tier classification cascade. Four free sources are exhausted before a single API call is made, and the gate spans every tier: the model's own answer is not trusted merely because a model produced it.
There is no database. Memory is four rule files, one state file and two logs on disk — the right shape for a single-user agent whose rules must be readable, hand-correctable and diffable in Git. The relationship that matters is the loop at the bottom: decisions_log is mined into sender_rules, so the agent's own past is what makes its future free.
%%{init: {'theme':'base','themeVariables':{'fontFamily':'-apple-system,Segoe UI,Roboto,sans-serif','fontSize':'13px','primaryColor':'#f4f1e8','primaryTextColor':'#20303f','primaryBorderColor':'#16324f','lineColor':'#16324f'}}}%%
erDiagram
IRRELEVANT_SENDERS ||--o{ EMAIL_STATE : "gates before all"
MANUAL_OVERRIDES ||--o{ EMAIL_STATE : "wins first"
TEMPLATE_RULES ||--o{ EMAIL_STATE : "then"
SENDER_RULES ||--o{ EMAIL_STATE : "then"
EMAIL_STATE ||--o{ ACTIONS_LOG : "produces"
ACTIONS_LOG ||--o{ EMAIL_STATE : "undo forgets"
EMAIL_STATE ||--o{ DECISIONS_LOG : "writes"
DECISIONS_LOG ||--o{ SENDER_RULES : "suggests · human approves"
IRRELEVANT_SENDERS {
list sender_contains "never job-related"
list domains
}
MANUAL_OVERRIDES {
string name PK
list sender_contains
list subject_contains
list body_contains_any
string classification
bool safe_to_archive "explicit"
float confidence
}
TEMPLATE_RULES {
string name PK
list sender_contains
list body_contains_any "action markers"
string classification
float confidence
}
SENDER_RULES {
string name PK
list sender_contains
list domain_equals
string classification
float confidence
}
EMAIL_STATE {
string message_id PK
string classification
list labels
bool safe_to_archive
string subject
string classified_at
string labelled_at "null until applied"
string archived_at "null until archived"
}
DECISIONS_LOG {
string timestamp
string message_id
string sender
string subject
string phase "fetch · classify · apply"
string classification
float confidence
string source "override · tier1 · template · sender · claude"
list evidence
}
ACTIONS_LOG {
string run_id PK
string message_id
string action "labels_applied · archived"
list label_ids_added
list label_ids_before "for undo"
}
Figure 2 — Four rule files, one state file, two logs. The bottom edge is the learning loop; the edge above it is undo, running the other way.