Auto-categorize emails by project keyword
Apply Gmail labels from a keyword map stored in a Sheet, so every email lands under the right project tag.
Published Oct 14, 2025
Northwind tags client emails by project so each account’s correspondence stays together. Gmail’s own filters can do this, but editing them by hand stops scaling past about five projects — every new client means clicking through the filter UI, and there is no single place to see all the rules.
This automation moves the rules into a Sheet. Each row pairs a keyword with a Gmail label; the script reads the sheet, searches recent mail for each keyword, and applies the matching label. Adding a project becomes one new row instead of a trip through Gmail’s settings.
What you’ll need
- A
Filterssheet with two columns and a header row:keywordandlabel. An example row:acme rebrandinkeyword,clients/acme/rebrandinlabel. - The Gmail labels named in the sheet must already exist — the script applies labels, it does not create them.
- The
Filterssheet’s file ID, dropped into the config constant at the top of the script.
The script
// The sheet holding the keyword-to-label rules.
const FILTERS_SHEET = '1abcFiltersSheetId';
// How far back to search — keeps each run fast and within quota.
const SEARCH_WINDOW = 'newer_than:1d';
/**
* Reads the keyword/label rules from the Filters sheet and applies each
* label to recent threads matching its keyword.
*/
function applyKeywordLabels() {
// 1. Read the Filters sheet and build a column-name lookup.
const [header, ...rows] = SpreadsheetApp.openById(FILTERS_SHEET)
.getSheets()[0].getDataRange().getValues();
const col = Object.fromEntries(header.map((h, i) => [h, i]));
if (!rows.length) {
Logger.log('No filter rules defined — nothing to do.');
return;
}
for (const row of rows) {
const keyword = row[col.keyword];
const labelName = row[col.label];
// 2. Skip incomplete rows.
if (!keyword || !labelName) continue;
// 3. Skip rows whose label does not exist in Gmail.
const label = GmailApp.getUserLabelByName(labelName);
if (!label) {
Logger.log('Label not found, skipping: ' + labelName);
continue;
}
// 4. Search recent mail for the keyword, excluding already-labelled
// threads so we do not relabel the same thread every run.
const query = '"' + keyword + '" ' + SEARCH_WINDOW +
' -label:' + labelName;
const threads = GmailApp.search(query);
// 5. Apply the label to every matching thread.
threads.forEach((t) => t.addLabel(label));
}
}
How it works
- The script reads the whole
Filterssheet, splits off the header, and builds acollookup so columns can be addressed by name. - If there are no rules, it logs a message and stops.
- For each rule it reads the keyword and label, skipping any row missing either value.
- It looks the label up in Gmail with
getUserLabelByName. If the label does not exist, it logs the name and moves on rather than crashing. - It builds a Gmail search query — the quoted keyword, a recency filter, and
-label:so threads that already carry the label are excluded. - It runs the search and adds the label to every thread that comes back.
Example run
The Filters sheet:
| keyword | label |
|---|---|
| acme rebrand | clients/acme/rebrand |
| northwind summit | events/summit |
On a run, an email from yesterday with the subject “Acme rebrand — final
artwork” matches the first rule and gets the clients/acme/rebrand label. A
thread about “Northwind Summit logistics” gets events/summit. A thread
already carrying clients/acme/rebrand is skipped because of the
-label: clause, so it is never relabelled.
Trigger it
This should run continuously so labels appear soon after mail arrives:
- In the Apps Script editor, open Triggers (the clock icon).
- Click Add Trigger.
- Choose
applyKeywordLabels, event source Time-driven, type Minutes timer, and pick Every 15 minutes.
Watch out for
- Single-word keywords with double meanings (for example
Acmeon its own) catch unrelated threads. Use multi-word phrases, or add afrom:clause in the keyword column to scope the match. - The search only covers the last day (
SEARCH_WINDOW). If a run is missed for longer than that, older mail will not be picked up — widen the window or accept the gap. - Labels are never created. A typo in the
labelcolumn means that rule is silently skipped; check the execution log if a project’s mail is not being tagged. - Labels are added, never removed. If you change a rule, previously labelled threads keep the old label until you clear it by hand.
- Gmail search counts toward daily quota. Many rules running every 15 minutes add up — keep the rule list lean, or run less often if you hit limits.
Related
Convert long email threads into a summary note
Collapse a thread's history into a Doc for handover — perfect for client transitions or vacation cover.
Updated Jun 6, 2026
Pull event RSVPs from emails into a Sheet
Parse yes/no replies to event invites and tally attendance automatically.
Updated Jun 2, 2026
Turn forwarded emails into project tasks
Forward to [email protected] and a row lands in the Projects sheet under the right client.
Updated May 30, 2026
Turn starred emails into a task list
Sync every starred thread into the Northwind Tasks sheet automatically.
Updated May 26, 2026
Alert when a label hits a backlog threshold
Warn the Northwind team in Slack when a Gmail label has more than N unread threads.
Updated Mar 31, 2026