Predict email intent and route it
Classify inbound Northwind mail and apply the right Gmail label automatically.
Published Oct 22, 2025
Northwind runs a single shared inbox, and everything lands in it — sales enquiries, support tickets, invoice questions, partnership pitches, and the usual spam. Sorting that by hand every morning is a chore, and a misrouted sales lead can sit unread for a day.
This script reads each new, unread thread and asks Claude which of a fixed set of intents it belongs to — sales, support, billing, partnership or spam — then applies a matching Gmail label. Once mail is labelled, Gmail filters and team members can take over: each person watches the labels they own instead of wading through the whole inbox.
What you’ll need
- A Gmail account whose inbox you want sorted. The script runs as that account.
- An Anthropic API key saved as
ANTHROPIC_API_KEYin Script Properties — see Store API keys and secrets securely. - Nothing else to set up. The script creates the
intent/...labels and theintent-routedmarker label the first time it needs them.
The script
// The set of intents Claude must choose from. Edit this list to match
// how your team actually splits its inbox.
const INTENTS = ['sales', 'support', 'billing', 'partnership', 'spam'];
// How many characters of the body to send. Enough to classify on,
// short enough to keep the request cheap and fast.
const BODY_CHARS = 1000;
/**
* Finds new unread inbox threads, asks Claude to classify each one,
* and applies an "intent/<label>" label plus a routed marker.
*/
function routeByIntent() {
// 1. Only pick up threads not already handled by this script.
const threads = GmailApp.search('in:inbox is:unread -label:intent-routed');
if (!threads.length) return;
// 2. The marker label keeps already-routed threads out of future runs.
const routed = GmailApp.getUserLabelByName('intent-routed')
|| GmailApp.createLabel('intent-routed');
for (const t of threads) {
// 3. Classify on the first message — the one that started the thread.
const m = t.getMessages()[0];
const intent = callClaude(
'Classify this Northwind email as one of: ' + INTENTS.join(', ') + '. ' +
'Return only the label.\n\n' +
'Subject: ' + m.getSubject() + '\n' +
'Body: ' + m.getPlainBody().slice(0, BODY_CHARS)
);
// 4. Guard against an unexpected reply — only act on known intents.
const clean = intent.toLowerCase().trim();
if (!INTENTS.includes(clean)) continue;
// 5. Apply the intent label, creating it on first use.
const label = GmailApp.getUserLabelByName('intent/' + clean)
|| GmailApp.createLabel('intent/' + clean);
t.addLabel(label);
// 6. Mark the thread routed so it is skipped next time.
t.addLabel(routed);
}
}
/**
* Minimal Anthropic API call. The key lives in Script Properties — it
* is never pasted into the code. Returns Claude's plain-text reply.
*/
function callClaude(prompt) {
const key = PropertiesService.getScriptProperties()
.getProperty('ANTHROPIC_API_KEY');
const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
method: 'post',
contentType: 'application/json',
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
payload: JSON.stringify({
model: 'claude-haiku-4-5-20251001',
max_tokens: 20,
messages: [{ role: 'user', content: prompt }],
}),
});
return JSON.parse(res.getContentText()).content[0].text.trim();
}
How it works
routeByIntentsearches the inbox for unread threads that do not yet carry theintent-routedlabel, so each run only sees fresh mail. If there is nothing new, it stops immediately.- It looks up — or creates — the
intent-routedmarker label, which is how the script remembers what it has already handled. - For each thread it takes the first message, the one that opened the thread, and builds a prompt with the subject and the first 1,000 characters of the body.
callClaudesends that prompt to Claude Haiku, a small fast model that is ideal for a short classification, and asks for just the label word back.- The reply is lower-cased and checked against the
INTENTSlist. If Claude returns anything unexpected, the thread is skipped rather than mislabelled. - The script applies an
intent/<label>label — creating it the first time — and then adds theintent-routedmarker so the thread is excluded from every later run.
Example run
Three unread threads are sitting in the inbox:
| Subject | Classified as | Label applied |
|---|---|---|
| ”Quote for a new marketing site?“ | sales | intent/sales |
| ”Login page throwing a 500 error” | support | intent/support |
| ”Your October invoice — wrong VAT?“ | billing | intent/billing |
After the run, each thread carries both its intent/... label and the
intent-routed marker. A salesperson watching intent/sales sees the first
thread straight away; the next run skips all three because they are now routed.
Trigger it
Run it on a schedule so mail is sorted without anyone thinking about it:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
routeByIntent, event source Time-driven, type Minutes timer, interval Every 15 minutes. - Save and approve the authorisation prompt the first time.
A 15-minute cycle keeps the inbox tidy without burning quota. You can also run
routeByIntent by hand from the editor to test it first.
Watch out for
- Classification is a judgement call, not a fact. Mail that genuinely spans two intents will be forced into one. Skim each label occasionally so a misrouted thread does not go unseen.
- The marker label is the memory. If you remove
intent-routedfrom a thread, the next run will reclassify it. That is a handy way to redo a bad call, but do not strip it in bulk by accident. - A label is not a delete. Threads tagged
intent/spamstill sit in the inbox — pair the label with a Gmail filter if you want them archived. - Each thread costs one API call. A busy inbox of a few hundred new threads a day is cheap on Haiku, but the cost scales with volume — watch it if traffic grows.
- Long threads are classified on the first message only. If a conversation changes topic, the original intent label sticks.
Related
Build an AI keyword-clustering tool
Group Northwind's tracked search terms into topic clusters — for SEO content planning.
Updated Feb 19, 2026
Build an AI customer-churn predictor
Flag at-risk Northwind accounts from behavioural signals — usage, support tickets, billing.
Updated Feb 15, 2026
Build a context-aware AI data validator
Catch values that look wrong in context — '£10' for a Northwind retainer is suspicious.
Updated Feb 7, 2026
Auto-categorize a photo library
Tag Northwind Drive images by visual content — product, team, event, behind-the-scenes.
Updated Feb 3, 2026
Build an AI bug-triage system
Categorise and prioritise Northwind's reported issues automatically — type, severity, owner.
Updated Jan 22, 2026