[ PAID MEDIA ]
/google-ads-optimize
Full Google Ads search campaign optimization for a ACME Agency client.
ACME Agency, <id> and you@example.com mark values that are per-agency — your install fills them with YOUR clients and accounts. If a section references a helper script you don't have yet, it ships with that workflow's install.Google Ads Search Campaign Optimizer
Triggers
/google-ads-optimize ACME Agency— run interactively for a specific client/google-ads-optimize— will prompt for client name if not given- ClickUp task tagged
claude-codewith name/description matching: optimiz.*google,negativ.*klju,search.*term,optimiz.*bidding
What this skill does
- Identifies the client from the argument or ClickUp context
- Loads or builds
CLIENT.mdwith business knowledge (Drive questionnaire + website) - Determines analysis period (30 days, or 14 days if any campaign is < 30 days old)
- Pulls search terms, keywords, device, geo, and bidding strategy data
- Auto-applies campaign-level BROAD match negative keywords
- Reports only (no auto-apply) on bidding strategy, device, and geo opportunities
- Posts a full structured report to the client's Slack channel
How to run
# Full execution (applies negatives, posts to Slack)
node ACME Agency/scripts/google_ads_optimize.mjs --client "ACME Agency"
# Dry run — analysis only, no writes, no Slack post
node ACME Agency/scripts/google_ads_optimize.mjs --client "ACME Agency" --dry-run
Interactive step-by-step (when running manually, not via ClickUp executor)
Step 1 — Identify client
- Look up the client in
ACME Agency/clients/clients.json(by name, case-insensitive) - Must have
google_ads_idto proceed. If missing, check MCC<id>or ask the user.
Step 2 — Load or build CLIENT.md
Check ACME Agency/clients/<ClientFolder>/CLIENT.md.
If it exists: read it — it has business context, brand terms, competitors, and "never negate" list.
If it doesn't exist, fetch from:
- Google Drive:
gws drive files list --folder-id <drive_folder_id>→ find file with "upitnik" in name →gws drive files get --file-id <id> - Client website: fetch
landing_pageURL from clients.json - Synthesize and save as
ACME Agency/clients/<ClientFolder>/CLIENT.md
Note:gwsCLI must be run via bash shell on Windows, not via NodeexecFileSync. Ifdrive_folder_idis missing in clients.json, skip Drive step and log a warning.
CLIENT.md format:
# CLIENT.md — <ClientName>
## Business type
## Products/services
## Market/geography
## Target audience
## Competitors
## Keywords to never negate
## Brand terms
Step 3 — Determine analysis period
Pull all SEARCH campaigns and check campaign.start_date.
- If ALL enabled search campaigns have been running ≥ 30 days → use
LAST_30_DAYS - If ANY search campaign is < 30 days old → use
LAST_14_DAYSfor all queries
Step 4 — Pull data in parallel
<id>()— campaign stats +bidding_strategy_type+start_dategetSearchTerms()— actual queries users searched, by campaign and ad groupgetClientKeywords()— the client's active keywords (used as context for analysis)getDeviceBreakdown()— clicks/conversions segmented by MOBILE / DESKTOP / TABLETgetGeoBreakdown()— clicks/conversions by city/region (LOCATION_OF_PRESENCE only)
Step 5 — Analyze search terms
Uses analyzeSearchTerms() from lib/google_ads.mjs plus client context:
Auto-negate (apply automatically):
- Competitor brands (global blacklist + client's competitors from CLIENT.md)
- Retail/DIY stores
- Informational intent (test, forum, vergleich, recenzije, etc.)
- Wrong product type (cabrio, markise, wintergarten, etc.)
- Low-value intent (gebraucht, billig, gratis, etc.)
Competitor campaign rule:
- Campaigns whose name matches
/(konkur|competitor)/i→ still add informational/retail/wrong_product negatives, but skip competitor-brand terms
Protect via CLIENT.md:
Keywords to never negate→ added to safe list before analysisBrand terms→ treated as client's own keywords, never negated
Suggest for ACME Agencyw (don't auto-apply):
- High cost (>3x avg CPC), 0 conversions, no keyword overlap → flag for human
Step 6 — Apply negatives
- Campaign-level BROAD match via
addNegativeKeywords()inlib/google_ads.mjs - The function auto-deduplicates against existing negatives
- Add the marker (e.g.,
artosi) not the full search term (e.g.,artosi pergola preisvergleich) - Skip if
--dry-run
Step 7 — Bidding strategy analysis (report only)
For each SEARCH campaign:
- If conversions ≥ 30 AND strategy =
MAXIMIZE_CLICKS→ recommend switching toMAXIMIZE_CONVERSIONSor Target CPA - Suggested tCPA = total cost / total conversions
Step 8 — Device analysis (report only, 100+ clicks threshold)
Aggregate all SEARCH campaign data by device across the period:
- If device has 100+ clicks AND conv rate < 50% of account average → recommend bid adjustment down
- If device has 100+ clicks AND 0 conversions AND cost > €20 → recommend exclusion consideration
Step 9 — Geo analysis (report only, 100+ clicks threshold)
Aggregate geographic data:
- If city/region has 100+ clicks AND conv rate < 30% of average → recommend bid adjustment
- If city/region has 100+ clicks AND 0 conversions AND cost > €30 → recommend exclusion
Step 10 — Save raw output to JSON
Before delegating, save the full data payload to:
ACME Agency/clients/<ClientFolder>/<id>.json
Contains: campaign summary, raw search terms, applied negatives, suggested negatives, device data, geo data, bidding diagnostics. The analyst reads from this file in the next step.
Step 11 — Delegate analysis to the analyst agent
Invoke the analyst subagent via the Task tool with subagent_type: "analyst". Pass:
client: <client name>
data_source: <absolute path to <id>.json from Step 10>
data_type: google-ads
period: last 30 days (or whatever window was analyzed)
compare_to: previous 30 days (if comparison data is in the file)
focus: search-term waste + bidding/device/geo opportunities
output_format: sections
The analyst returns summary (Slack-ready), details (for thread), and <id>. Do NOT analyze the JSON yourself in the main context.
Step 12 — Deliver via slack-reporter
Invoke the slack-reporter subagent via the Task tool with subagent_type: "slack-reporter". Pass:
client: <client name>
channel: <channel ID resolved via getClientChannel(clientName)>
type: google-ads-optimize
headline: "*Google Ads — <ClientName> — <YYYY-MM-DD>*"
sections: <derived from analyst's summary block>
extra_sections:
- title: "Negatives applied"
bullets: <list from Step 6>
- title: "Suggested for ACME Agencyw"
bullets: <list from Step 6>
links:
- label: "Detalji"
url: <Drive doc URL if generated>
thread_details: <analyst's details + device/geo breakdowns>
language: <client's language from clients.json>
Do NOT call <id>() or any helper from lib/slack.mjs directly. The slack-reporter agent owns Slack delivery.
Verification (run AFTER Step 12 — prove it shipped, don't assume)
Rubric: verify.md. The mutation check is now enforced in google_ads_optimize.mjs: after applying negatives it re-fetches each campaign's live negative list (getExistingNegatives) and confirms every intended term is present — a 200 from the write is not proof. Watch the run for [optimize] Verify: N/M confirmed live; any miss is logged per-campaign with the API errors (result.errors, previously ignored) and lands in reportData.negativesVerification. A miss usually means a policy-exemptible keyword (e.g. <id>) was dropped — do NOT report those as applied; resubmit with <id> (see createSearchKeywords).
Then check the rest before declaring done:
- [ ] Raw JSON saved at
ACME Agency/clients/<folder>/<id>.jsonwith non-emptycampaigns[]andsearchTerms[] - [ ]
negativesVerificationshows 0 missing (all intended negatives confirmed live) — if not, list them, don't hide them - [ ] No negatives applied to wildcard wrongly (cross-check against CLIENT.md "Keywords to never negate" list — if any match, flag as a potential rollback)
- [ ] Analyst's
summaryblock has the 3 expected sections (Top-line, What changed, Recommendations) - [ ] Summary mentions specific search terms by name in the waste analysis
- [ ] Slack post via slack-reporter returned
tsnon-null AND posted in last 5 min - [ ] Channel matches
clients.json[client].slack_channel - [ ] Slack message body uses
*bold,•bullets, zero emojis - [ ] If
CLIENT.mdhad[Fill in]placeholders, this is explicitly noted in the report - [ ] Bidding/device/geo recommendations match what the data actually shows (no hallucinated cities or device IDs)
If any check fails, name the gap explicitly. Never claim success when verification fails — for Google Ads especially, wrong negatives can take down a real account.
Key files
| File | Purpose |
|---|---|
ACME Agency/scripts/google_ads_optimize.mjs | Main optimization script (standalone + exported function) |
ACME Agency/scripts/lib/google_ads.mjs | Google Ads API library: <id>, getSearchTerms, getDeviceBreakdown, getGeoBreakdown, analyzeSearchTerms, addNegativeKeywords |
ACME Agency/scripts/lib/slack.mjs | Slack reporting: <id> |
ACME Agency/scripts/clickup_executor.mjs | ClickUp task runner — delegates google_ads_negatives + google_ads_optimize to this skill |
ACME Agency/clients/clients.json | Client registry: google_ads_id, drive_folder_id, landing_page, folder |
ACME Agency/clients/<ClientFolder>/CLIENT.md | Per-client business knowledge (auto-created if missing) |
Important rules
- NEVER auto-apply: bidding strategy changes, device bid adjustments, geo exclusions, budget changes
- NEVER add competitor-brand negatives to competitor campaigns (but DO add informational/retail/wrong_product)
- Check CLIENT.md first — the "Keywords to never negate" section prevents wrong negations
- If CLIENT.md has
[Fill in]placeholders, knowledge hasn't been ACME Agencywed — note this in the analyst's findings - Delegate analysis to the analyst agent — never crunch the raw search-term JSON in the main context
- Delegate Slack output to slack-reporter — never call
<id>()directly anymore - If a Google Ads API call fails twice with the same error (
<id>,OAuthException,INVALID_CUSTOMER_ID), invoke thediagnosticiansubagent before retrying. These almost always have UI/permission/MCC-linkage root causes — don't loop on code workarounds. gwson Windows: always use bash shell, not Node.jsexecFileSync- API version:
v20, MCC:<id> LAST_7_DAYSdoes NOT include today — always useLAST_14_DAYS,LAST_30_DAYS, orDURING TODAY
Example ClickUp tasks that trigger this skill
- "Optimiziraj Google search kampanje za ACME Agency"
- "Dodaj negativne ključne riječi — ACME Agency"
- "Google Ads optimize — bidding strategy check ACME Agency"
- "Search term audit ACME Agency"