Pulls the last N days of search-term data, classifies every term by intent (Brand / Competitor / Informational / Transactional / Navigational / Unclear), flags the obvious negative-keyword candidates, and either lists them for your review (dry run) or applies them to a shared negative-keyword list (live mode).
Solves the broad-match-with-smart-bidding control problem: Google now matches your ads to far more search queries than the keyword you actually bid on, and the only practical lever you have is negative keywords. This script automates the review.
Broad match + Smart Bidding is the default for most new campaigns in 2026. It pushes traffic toward intent Google decides is similar to your bid keyword, but "similar" is generous — informational queries, navigational queries, and outright off-topic terms slip through. Manually reviewing the Search Terms report every week is tedious and the obvious patterns are the same every account: "how to", "what is", "login", "support".
This script encodes those patterns and lets you act on them in bulk. Paid SaaS tools (Negator.io, ShopStory) charge $29–$199/mo for this. You get it for free, with full control over the rules.
Every run reports what it would do. Only when you explicitly set DRY_RUN = false does the script touch your account.
- DRY RUN (default) — pulls search terms, classifies, writes to Sheet, emails a digest of suggestions. Nothing is added to your campaigns.
- LIVE — same as above, plus adds the suggested terms (EXACT match by default) to a shared negative-keyword list named
Aumlytics Auto Negatives(or whatever you setNEGATIVE_LIST_NAMEto). The list is auto-created if missing. You then apply that list to the campaigns you want protected. No campaign-level or ad-group-level negatives are ever created — everything goes through the shared list so it's audit-friendly and instantly reversible.
| File | When to use |
|---|---|
single-account.js |
One Google Ads account. |
mcc.js |
Manager / MCC account — runs across all child accounts (or a labeled subset) in parallel and applies negatives per child. |
A search term gets the first intent it matches, in this priority order:
- Brand — contains any term from
CONFIG.BRAND_TERMS(your brand name, common misspellings). Never flagged as a negative, even if it never converts. - Competitor — contains any term from
CONFIG.COMPETITOR_TERMS. By default not flagged either (most B2B accounts WANT competitor traffic). EnableRULES.competitor_blockif you don't. - Navigational — contains "login", "support", "contact", "phone number", etc. Users looking for help, not to buy. Almost always a waste in PPC.
- Informational — contains "how to", "what is", "tutorial", "guide", "vs", etc. Top-of-funnel research, rarely converts on first click.
- Transactional — contains "buy", "price", "near me", "for sale", etc. Never flagged — these are your highest-intent terms.
- Unclear — no pattern matched. Default for everything that isn't obviously one of the above.
The pattern lists are intentionally short and English-language by default — edit them in CONFIG to fit your account's language and vocabulary.
| Rule | Triggers when | Default |
|---|---|---|
informational_no_conv |
Intent = Informational, cost ≥ $25, conversions = 0 | Enabled |
unclear_waste |
Intent = Unclear, cost ≥ $50, conversions = 0 | Enabled |
navigational_any |
Intent = Navigational, clicks ≥ 3 | Enabled |
competitor_block |
Intent = Competitor, clicks ≥ 1 | Disabled — most accounts want competitor bids |
Tune the thresholds to your account size. A $500/day account should use much higher minCost values than a $30/day account.
- Open the account → Tools → Bulk actions → Scripts → +
- Paste
single-account.js - Edit
CONFIG:EMAIL— requiredBRAND_TERMS— your brand variants. Don't skip this — without it your brand traffic gets misclassified as Informational/Unclear and could be suggested as a negative.COMPETITOR_TERMS— optional but recommended for cleaner classificationRULES.*.minCostandminClicks— tune to your account size
- Click Preview. Read the log. Authorize.
- Click Run with
DRY_RUN = true. Read the email digest carefully. - Open the Sheet, filter
Suggested Negative = Yes, scan the terms. Anything that looks wrong → tune your CONFIG patterns and re-run. - Only after at least 1–2 dry-run cycles you trust: flip
DRY_RUN = falseand run. - Manually apply the shared negative list to your campaigns in the Google Ads UI (Tools → Shared library → Negative keyword lists → tick the list → Apply to campaigns). The script creates the list; YOU control which campaigns use it.
- Schedule weekly.
- From your MCC → Tools → Bulk actions → Scripts → +
- Paste
mcc.js CONFIG.BRAND_TERMSandCONFIG.COMPETITOR_TERMSare shared across all child accounts in v1. If your MCC has accounts with very different brands, fork the script and key offacct.getName()insideprocessAccount().- Use
ACCOUNT_LABELto limit scope to a labeled subset. - Always start with
DRY_RUN = truefor the first run. - Weekly cadence with LIVE mode is the common production pattern.
- AdWords (read) — to query search terms
- AdWords (write) — only used when
DRY_RUN = false, to add negatives to a shared list. Not requested in dry run. - Spreadsheets — to write the output Sheet
- Mail — to send the digest
If your Workspace admin restricts script scopes, dry-run mode needs only read + Sheets + Mail.
For each unique suggested term (deduplicated by MATCH_TYPE:term):
- A new entry in the shared negative keyword list named
CONFIG.NEGATIVE_LIST_NAME - Match type from
CONFIG.NEGATIVE_MATCH_TYPE(defaultEXACT) - Format:
[search term]for EXACT,"search term"for PHRASE, plain text for BROAD
The script SKIPS:
- Terms already in the list (duplicates)
- Terms whose
addNegativeKeywordcall throws (logged in the digest) - Terms shorter than
CONFIG.MIN_TERM_LENGTH(default 4 chars)
The script never modifies campaigns directly. To revert:
- Easy: Tools → Shared library → Negative keyword lists →
Aumlytics Auto Negatives→ un-apply from campaigns - Full clean: delete entries from the shared list manually, OR delete the list entirely
The shared list pattern means every negative the script adds is grouped in one place — no hunting through 50 campaigns for what changed.
- Intent classification is keyword-pattern based. No NLP model, no LLM, no contextual understanding. A search like "best login solutions for enterprise" gets classified Navigational because it contains "login", even though it's clearly commercial. You'll see edge cases — tune the patterns or accept the noise.
- Per-account brand/competitor lists in MCC mode require a fork. v1 uses one shared list across all child accounts.
- No Phrase / Broad suggestion — defaults to EXACT for safety. You can switch with
NEGATIVE_MATCH_TYPEbut understand the implications first. - No mining of n-grams. Each search term is evaluated whole. If "how to install" appears 100x as part of longer queries but never as a 3-word query, the script won't suggest it as a negative.
MIT — see LICENSE in the parent repo.