# /google-ads-optimize

> Full Google Ads search campaign optimization for a ACME Agency client.


# 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-code` with name/description matching:
  - `optimiz.*google`, `negativ.*klju`, `search.*term`, `optimiz.*bidding`

## What this skill does

1. Identifies the client from the argument or ClickUp context
2. Loads or builds `CLIENT.md` with business knowledge (Drive questionnaire + website)
3. Determines analysis period (30 days, or 14 days if any campaign is < 30 days old)
4. Pulls search terms, keywords, device, geo, and bidding strategy data
5. **Auto-applies** campaign-level BROAD match negative keywords
6. **Reports only** (no auto-apply) on bidding strategy, device, and geo opportunities
7. Posts a full structured report to the client's Slack channel

## How to run

```bash
# 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_id` to 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:
1. Google Drive: `gws drive files list --folder-id <drive_folder_id>` → find file with "upitnik" in name → `gws drive files get --file-id <id>`
2. Client website: fetch `landing_page` URL from clients.json
3. Synthesize and save as `ACME Agency/clients/<ClientFolder>/CLIENT.md`

> Note: `gws` CLI must be run via bash shell on Windows, not via Node `execFileSync`.
> If `drive_folder_id` is missing in clients.json, skip Drive step and log a warning.

CLIENT.md format:
```markdown
# 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_DAYS` for all queries

### Step 4 — Pull data in parallel
- `<id>()` — campaign stats + `bidding_strategy_type` + `start_date`
- `getSearchTerms()` — actual queries users searched, by campaign and ad group
- `getClientKeywords()` — the client's active keywords (used as context for analysis)
- `getDeviceBreakdown()` — clicks/conversions segmented by MOBILE / DESKTOP / TABLET
- `getGeoBreakdown()` — 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 analysis
- `Brand 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()` in `lib/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 to `MAXIMIZE_CONVERSIONS` or 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](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>.json` with non-empty `campaigns[]` and `searchTerms[]`
- [ ] `negativesVerification` shows 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 `summary` block 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 `ts` non-null AND posted in last 5 min
- [ ] Channel matches `clients.json[client].slack_channel`
- [ ] Slack message body uses `*` bold, `•` bullets, zero emojis
- [ ] If `CLIENT.md` had `[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 the `diagnostician` subagent before retrying. These almost always have UI/permission/MCC-linkage root causes — don't loop on code workarounds.
- `gws` on Windows: always use bash shell, not Node.js `execFileSync`
- API version: `v20`, MCC: `<id>`
- `LAST_7_DAYS` does NOT include today — always use `LAST_14_DAYS`, `LAST_30_DAYS`, or `DURING 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"
