Your inbox, sorted into categories you describe in plain English, with the emails that need a reply today pulled to the top.
Every email gets one call to Jev, TypeSafe's
decision model. Jev doesn't write anything. It answers four narrow questions
about each email with real probabilities: which of your categories is it,
is a person waiting on you, is it a machine talking, and how soon does it
matter. Your code turns those numbers into Gmail labels and a short morning
report. When Jev isn't sure, the email gets a Jev/Review label instead of
a guess.
A whole day of mail costs a fraction of a cent. The demo's 16 emails cost $0.0009.
Built on One: Gmail and TypeSafe are both One connections, so it's one API key and two connection keys, not two separate integrations.
git clone https://github.com/withoneai/jev-email-classifier
cd jev-email-classifier && npm install
npm startThat runs with no keys and no accounts. It replays a fictional founder's
inbox (16 emails) and Jev's real, recorded answers to it through the same
pipeline a live run uses, then writes the report to reports/:
── jev email classifier (demo: no keys in the environment, replaying a recorded inbox)
9 categories from config/inbox.md: Customers, Sales leads, Investors, Partners and intros, Hiring, ...
read 16 new inbox emails from the last 2 days (alex@fernhill.dev)
sorted: 3 reply today, 3 reply this week, 2 for review, 8 filed
- Dana Okafor: Sync has been failing since last night
- Priya Raman: Following up on Tuesday
- Sam Whitfield: Re: Senior backend engineer, next steps
wrote reports/inbox-2026-09-25.md
The report is what you actually read. The top of it:
16 emails. 3 need a reply today, 3 this week, 2 need your eyes, and 8 were filed with nobody waiting.
Dana Okafor: Sync has been failing since last night
Hi Alex, our nightly sync has failed three runs in a row since yesterday around 11pm. Our finance team closes the month on Monday...
Someone is waiting, and it's time-sensitive. Labelled
Jev/CustomersandJev/Reply.
And the part that matters most, the two emails it would not guess on:
Ines Carvalho: Checking in
Not sure what this is: Sales leads 50% or Partners and intros 25%. Labelled
Jev/Review.Linear: Mira commented on FERN-481
Looks like Notifications, but Jev can't tell whether someone is waiting on you (50%). Labelled
Jev/Review.
Both are honest calls. "Checking in" really could be a prospect or a partner. And the Linear email is a machine notification carrying a direct question from a teammate, which is exactly the case where "is anyone waiting on me?" deserves a human look.
npm install -g @withone/cli
one init
one add gmail
one add typesafe # paste a TypeSafe API key (free at typesafe.ai)
cp .env.example .env # your One key and the two connection keys
npm run check # proves Gmail and Jev both answer, changes nothing
npm start # read-only: sorts and writes the report, no labels
npm start -- --apply # same, and adds the labels in Gmailone --agent list prints the connection keys. Run without --apply first
and read the report. When the sorting looks right, add --apply.
Emails that already carry a Jev/ label are skipped, so running it every
hour only ever judges new mail.
config/inbox.md is your inbox, described in plain English. Jev reads it
word for word on every email. It has three parts:
- About me. Who you are and what lands in your inbox. One paragraph.
- Categories. One
###heading per category, with a few sentences on what belongs there. Each heading becomes a Gmail label (Jev/Customers). Add, remove or rename them freely. - What needs a reply from me. Your own rules. The shipped version says a cold pitch asking for "15 minutes" never needs a reply.
When something lands in the wrong place, fix it here, in a sentence, the way you'd explain it to an assistant. That is how the shipped file got its Investors rule: on the first recording, Jev filed a VC's "the partnership met this morning" under partnership pitches. One added sentence ("anyone writing from a venture firm belongs here, even when they mention the partnership") fixed it, and nothing else moved.
- Read the inbox. Everything in
in:inboxfrom the last two days, skipping anything already labelled. The first 2,000 characters of each body, plus one observed fact: whether it carries aList-Unsubscribeheader. - Ask Jev four questions per email, in one call. A Choice (which of your categories, or "Other" if none fits), two yes/no questions (is a person waiting on you, was it sent by a system), and a Score (how soon it matters, on four levels from "whenever" to "this hour"). They run in parallel inside the one request.
- Decide, in code.
src/sort.tsis 60 lines and no model. Category confidence under 0.75 goes to Review. A "someone is waiting" answer near 50% goes to Review. A person waiting and not a machine talking getsJev/Reply, and lands in "reply today" or "this week" by urgency. Everything else is filed under its category. - Label (only with
--apply): creates any missingJev/...labels, then one batch call per label. - Write the report to
reports/inbox-<date>.md.
The split the repo commits to: Jev judges, code decides. Jev tells you "Sales leads, 50%". Whether 50% is good enough to act on is your call, and it lives in a number you can change without calling any model again.
All in .env, all optional:
| Variable | Default | What it changes |
|---|---|---|
CONFIDENCE_MIN |
0.75 | Raise it and more mail goes to Review. Lower it and more gets filed automatically. |
NEEDS_REPLY_MIN |
0.6 | How sure Jev must be that someone is waiting before it adds Jev/Reply |
REPLY_TODAY_MIN |
1.5 | Urgency (0 to 3) at which a reply moves to "today" |
AUTOMATED_MAX |
0.5 | Above this, it's a machine talking and never needs a reply |
LOOKBACK_DAYS |
2 | How far back to read |
LABEL_PREFIX |
Jev | Jev/Customers, or Inbox/Customers, or whatever you like |
JEV_MODEL |
jev-latest | Pin a version once you've tuned thresholds on it |
Jev charges $0.042 per million input tokens and nothing for output. Each email is one call of about 1,300 tokens, so:
- The demo's 16 emails: about $0.0009.
- A heavy inbox of 200 emails a day: about a cent a day.
No other model is called. The Gmail reads and label writes are free against your own quota.
- It never archives, deletes, marks read, or sends. The only write is
adding a label, and only with
--apply. Nothing is ever removed, so undoing a run is deleting theJev/labels. - It doesn't draft replies. Jev chooses, it doesn't write. If you want
drafts for the
Jev/Replypile, that's a separate step with a writing model, and this repo deliberately stays out of it. - It reads the first 2,000 characters. Enough to tell what nearly every email is. Long threads are judged on their latest message.
- It reads one page of Gmail. Up to
MAX_MESSAGES(default 100, max 500) per run. Run it more often rather than raising the lookback. - English is where Jev is strongest. Other languages work, less well. Watch the Review pile if your inbox is multilingual.
Every call goes through the One SDK. Gmail is typed; TypeSafe goes through One's raw passthrough with its catalog action id, which still handles auth and retries:
const res = await one.connection(process.env.ONE_CONNECTION_TYPESAFE).runRaw({
systemId: "conn_mod_def::GNh4bXcHPaU::V2ndvy5CQJmYHvkElwVM_w", // POST /v1/systemone
method: "POST",
path: "/v1/systemone",
body: { model: "jev-latest", state, questions },
});Pointing it at Outlook is a new src/gmail.ts for the read and the label
write. The questions, the policy and the report don't change.
- A free One account with Gmail and TypeSafe connected
- A TypeSafe API key
- Node 20+
MIT licensed. Fork it, rewrite config/inbox.md for your own inbox, and run
it without --apply until you trust it.