Manage your Gmail filters as code. A single array in a Google Apps Script is the source of truth; running one function makes your Gmail account converge to it — true upsert semantics, which Gmail's native export/import XML cannot do (importing always appends and duplicates).
Running syncFilters():
- creates filters that are defined in the script but missing in Gmail
- deletes filters that exist in Gmail but are no longer defined
(only when
MANAGE_ALL = true) - leaves untouched filters that already match a definition
- auto-creates labels, including nested paths like
GitHub/My Projects(parents are created first)
Don't try to define which mail is important — define which mail is noise, and route it out of the inbox into labels. Whatever remains in the inbox is unfiltered, meaning it's either a real human writing to you or a brand-new noise pattern that deserves a new filter entry. The inbox becomes your filter backlog.
The bundled defaults implement that for a typical developer account:
| Filter | Destination | Notes |
|---|---|---|
| GitHub notifications from your repos | GitHub/My Projects |
repo list defined once in MY_PROJECT_REPOS |
| All other GitHub notifications | GitHub/Other |
exclusion list auto-generated from the same constant, so the two can never drift |
| Review-request notifications | GitHub/Review Requested |
overlay label only; GitHub encodes the reason in the CC address (review_requested@, push@, mention@, ...) |
| Dependency-bump PRs (dependabot/renovate) | GitHub/Dependencies |
matched by chore(deps) / build(deps) subjects |
| Slack channel notifications | Slack/Channels |
channel mails have in #channel in the subject; DMs don't and stay in the inbox (a commented-out filter routes DMs too if you prefer) |
| Google Calendar notifications | Calendar |
|
| Invites sent directly by people | Calendar |
matched by the invite.ics attachment — language- and sender-independent |
| Internal tools / HR / admin | HR & Admin |
placeholder domains — replace with your own |
| Security alerts & verification codes | Security |
starred + marked important so time-sensitive codes stand out inside the label |
- Go to https://script.google.com (logged in as the Gmail account you want to manage) and create a New project.
- Replace the contents of
Code.gswith this repository'sCode.gs. - In the left sidebar, click Services (+), find Gmail API, and
click Add. This enables the advanced Gmail service — without it
the script fails with
Gmail is not defined. - In the toolbar dropdown, select the
syncFiltersfunction and click Run. Grant the authorization prompt (the script asks for Gmail settings and labels scopes; it runs entirely in your account, no third party involved). - Open the execution log (
Ctrl+Enter/ View → Logs). The first run is a dry run: it prints the full plan (creates / deletes / unchanged) without touching anything. - If the plan looks right, set
DRY_RUN = falseat the top of the script and Run again.
- Gmail → Settings → Filters and Blocked Addresses should list every filter from the array.
- Run
syncFiltersonce more: the log should readPlan: 0 to create, 0 to delete, N unchanged— proof that convergence and change-detection work.
Notice a new kind of noise in your inbox → add one entry to the FILTERS
array (or add a repo name to MY_PROJECT_REPOS) → Run. Done.
Filters only apply to new incoming mail. To retro-apply a rule to existing messages, run the same query in Gmail's search box, select all, and apply the label / archive manually.
Add a time-driven trigger (clock icon → Triggers → Add trigger →
syncFilters, e.g. weekly) if you edit the script from multiple places
and want Gmail to converge automatically. For most people, running
manually after each edit is enough.
DRY_RUN true = log the plan only, change nothing
MANAGE_ALL true = the script OWNS all filters: anything created
by hand in the Gmail UI is deleted on next sync
false = additive-only; the script never deletes
MY_PROJECT_REPOS list of GitHub repo names treated as "yours"
Each entry in FILTERS supports:
query Gmail search string ("Has the words" in the UI)
label label to apply; "/" nests; auto-created if missing
archive skip the inbox
markImportant mark as important
star star the message
neverSpam never send to spam
markRead mark as read on arrival
Any valid Gmail search syntax works in query: from:, to:, cc:,
subject:, filename:, list:, has:attachment, OR, - negation,
parentheses, quoted phrases, etc.
The script matches filters by exact criteria+action signature. Filters
you created by hand or imported via XML usually store their conditions
in separate fields (from/subject) rather than the query field, so they
will NOT match the script's definitions and — with MANAGE_ALL = true —
will be deleted and replaced on the first real run. That is normally
exactly what you want, and the dry run shows you the full plan first.
If you'd rather keep manual filters alongside, set MANAGE_ALL = false.
Gmail is not defined— you skipped step 3; add the Gmail API advanced service.Cannot read properties of null (reading 'filter')— you're running an old version of the script; currentCode.gsguards the empty-filter-list response.- A filter is recreated on every run — Gmail sometimes normalizes a query string on save (e.g. collapsing whitespace). Check the DELETE and CREATE log lines: if they differ only in formatting, adjust your definition to match Gmail's normalized form.
- Labels created but mail not moving — filters never apply retroactively; see "Day-to-day usage" above.
- Gmail allows up to 1,000 filters per account; each filter's criteria
can be long, so grouping senders with
ORis preferred anyway. - Forwarding actions require pre-verified forwarding addresses and are not included in the defaults.
- Everything runs inside your own Google account under your own authorization. No data leaves it.