search-campaign-audit
Google Ads Search campaign audit — match types, quality score, negative keyword gaps, search terms, ad strength, RSA asset coverage
SKILL.mdskills/search-campaign-audit/SKILL.md
GitHubSearch Campaign Audit
A deep audit of your Google Ads Search campaigns — the campaign type where you still control keywords, match types, and ads directly. Finds quality-score drags, wasted match-type spend, negative gaps, and weak ads.
Requires: Cogny Agent subscription ($9/mo) — Sign up
Works with
| Job | Primary | Also answers it | Differences to state |
|---|---|---|---|
| Campaigns, keywords, QS, search terms, ads | Google Ads (Google_Ads) |
Microsoft Ads (Bing_Ads) |
Bing money is in account currency units (not micros); percents are percents; QS components are 1–3 (below / average / above) |
| App Store search terms | — | Apple Search Ads (Apple_Search_Ads) |
Only for app campaigns; taps and installs, not clicks and conversions |
| Landing-page conversion cross-check | Google Analytics 4 | Plausible | GA4 key events vs Ads conversions; different attribution and counting |
| Keyword gaps and competitor paid terms | Google Ads tool_generate_keyword_ideas |
Semrush, Ahrefs | Search volumes are estimates and differ between tools |
Call list_integrations once, audit every connected search engine, and report them side
by side. Never sum spend across engines with different currencies.
Step 0: Cogny check
If no mcp__cogny__* tools exist (no list_integrations, no cogny_status), print this and stop:
Search Campaign Audit needs your marketing data through Cogny's MCP gateway.
1. Sign up at https://cogny.com and connect Google Ads and/or Microsoft Ads (plus GA4,
Plausible, Semrush or Ahrefs if you use them).
2. Add Cogny to .mcp.json in your project:
{"mcpServers": {"cogny": {"type": "http", "url": "https://app.cogny.com/mcp"}}}
3. Restart Claude Code; it opens a browser for the OAuth login on first use.
Nothing to run without Cogny.
If Cogny is connected but neither mcp__cogny__mcp__Google_Ads__tool_execute_gaql nor
mcp__cogny__mcp__Bing_Ads__tool_get_report is available, say "connect Google Ads or
Microsoft Ads in Cogny" and stop. An engine that is listed but unhealthy, or whose first read
returns 401/permission denied, gets "reconnect <engine> in Cogny". With only Microsoft Ads
connected, run the "Microsoft Ads" branch of each step.
Usage
/search-campaign-audit — audit every Search campaign in the account
/search-campaign-audit Brand - Exact — audit one campaign by name or id
Benchmarks
| Signal | Poor | OK | Good |
|---|---|---|---|
| Quality Score | ≤4 | 5–6 | 7–10 |
| Ad Strength | Poor / Average | Good | Excellent |
| Search term relevance | <50% spend on intent terms | 50–75% | >75% |
| Impression share lost (rank) | >40% | 20–40% | <20% |
Steps
Google Ads money fields are micros (divide by 1,000,000). An empty GAQL result (no rows, or the tool returns no output) means zero: no matching entities or no activity in the window. Report it as 0, not as an error.
1. Find the Search campaigns
SELECT campaign.id, campaign.name, campaign.status,
campaign.bidding_strategy_type, campaign_budget.amount_micros,
metrics.cost_micros, metrics.conversions, metrics.conversions_value,
metrics.search_impression_share, metrics.search_rank_lost_impression_share,
metrics.search_budget_lost_impression_share
FROM campaign
WHERE campaign.advertising_channel_type = 'SEARCH'
AND segments.date DURING LAST_30_DAYS
ORDER BY metrics.cost_micros DESC
Flag campaigns losing >40% impression share to rank (quality/bids) or to budget.
Microsoft Ads: mcp__cogny__mcp__Bing_Ads__tool_list_accounts, then
mcp__cogny__mcp__Bing_Ads__tool_get_campaigns (account_id, campaign_types: ["Search"])
and mcp__cogny__mcp__Bing_Ads__tool_get_report (account_id, report_type: "campaign",
start_date, end_date). Every tool_get_report call below also takes that account_id. Impression share columns are ImpressionSharePercent,
ImpressionLostToRankAggPercent and ImpressionLostToBudgetPercent.
2. Keyword performance & quality score
SELECT ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type,
ad_group_criterion.quality_info.quality_score,
ad_group_criterion.quality_info.creative_quality_score,
ad_group_criterion.quality_info.post_click_quality_score,
ad_group_criterion.quality_info.search_predicted_ctr,
campaign.name, ad_group.name,
metrics.cost_micros, metrics.conversions, metrics.clicks, metrics.impressions
FROM keyword_view
WHERE segments.date DURING LAST_30_DAYS AND metrics.cost_micros > 0
ORDER BY metrics.cost_micros DESC
LIMIT 100
Quality Score 0 or null means not scored (too little traffic), not a bad score. Count those keywords separately as "not scored" and leave them out of the QS ≤4 counts and the average QS.
Flag:
- Quality Score ≤4 (1–4, excluding unscored) on spending keywords — and read the three components to say why
(
creative_quality_score= ad relevance,post_click_quality_score= landing page,search_predicted_ctr= expected CTR) - High-spend keywords with zero conversions
- Duplicate keywords across ad groups (internal auction competition)
Microsoft Ads: tool_get_report with report_type: "keyword". QualityScore is 1–10;
ExpectedCtr, AdRelevance and LandingPageExperience are 1 (below average), 2 (average)
or 3 (above average). Map them to the same three diagnoses. Match type is BidMatchType.
3. Match-type mix
Aggregate spend and conversions by match_type (EXACT / PHRASE / BROAD).
Flag:
- Broad match taking a large share of spend with a worse CPA than exact/phrase — broad only works well with Smart Bidding and tight negatives
- Accounts that are 100% exact (likely missing volume) or 100% broad (likely bleeding)
4. Search term analysis
SELECT search_term_view.search_term, search_term_view.status, campaign.name,
ad_group.name, metrics.cost_micros, metrics.conversions, metrics.clicks
FROM search_term_view
WHERE segments.date DURING LAST_30_DAYS AND metrics.cost_micros > 0
ORDER BY metrics.cost_micros DESC
LIMIT 100
Flag:
- Irrelevant / off-intent terms with spend — negative keyword candidates
- Converting terms with status
NONE(not yet added as keywords) — harvest them - The total cost of zero-conversion terms — this is the headline waste number
Microsoft Ads: tool_get_report with report_type: "search_query". There is no
"added/excluded" status column, so compare SearchQuery against the keyword list from
tool_get_keywords (account_id, ad_group_id) to find harvest candidates. Terms that
waste money on Google usually waste it on Bing too; flag terms negated in one engine only.
Apple Search Ads (app campaigns only): tool_list_acls for the org_id, then
tool_get_search_term_report (org_id, campaign_id, start_date, end_date). Judge
terms by installs and cost per install, not conversions.
5. Negative keyword coverage
Negatives live in three places. Query all three before saying a campaign has none.
SELECT shared_set.name, shared_set.type, shared_set.member_count
FROM shared_set
WHERE shared_set.type = 'NEGATIVE_KEYWORDS'
SELECT campaign.name, campaign_criterion.keyword.text, campaign_criterion.keyword.match_type
FROM campaign_criterion
WHERE campaign_criterion.type = 'KEYWORD' AND campaign_criterion.negative = TRUE
AND campaign.advertising_channel_type = 'SEARCH' AND campaign.status = 'ENABLED'
SELECT campaign.name, ad_group.name, ad_group_criterion.keyword.text,
ad_group_criterion.keyword.match_type
FROM ad_group_criterion
WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = TRUE
AND campaign.advertising_channel_type = 'SEARCH' AND campaign.status = 'ENABLED'
Flag: no negatives at any level, no shared negative list (campaign/ad-group negatives only
are hard to maintain), brand terms bidding or matching in non-brand campaigns (check the
step 2 keywords and step 4 search terms against the brand name; give their spend),
brand terms not negated in non-brand campaigns, and no
list of perennial junk (free, jobs, diy, cheap — adjust to the business).
Microsoft Ads: tool_get_negative_keywords (account_id, entity_type: "Campaign",
entity_ids). It returns negatives set on campaigns or ad groups only, not shared lists,
so say so before calling a campaign "unprotected". Apple Search Ads:
tool_get_campaign_negative_keywords.
6. Ad strength & RSA coverage
SELECT ad_group.name, ad_group_ad.ad.id, ad_group_ad.ad.type,
ad_group_ad.ad_strength, ad_group_ad.status, campaign.name,
metrics.impressions, metrics.conversions
FROM ad_group_ad
WHERE campaign.advertising_channel_type = 'SEARCH'
AND campaign.status = 'ENABLED'
AND ad_group_ad.status = 'ENABLED'
AND segments.date DURING LAST_30_DAYS
The query above drops ads with no impressions in the window, so find empty ad groups without a date range:
SELECT campaign.name, ad_group.id, ad_group.name, ad_group.status
FROM ad_group
WHERE campaign.advertising_channel_type = 'SEARCH' AND campaign.status = 'ENABLED'
AND ad_group.status = 'ENABLED'
SELECT ad_group.id, ad_group_ad.ad.id, ad_group_ad.status
FROM ad_group_ad
WHERE campaign.advertising_channel_type = 'SEARCH' AND campaign.status = 'ENABLED'
AND ad_group.status = 'ENABLED' AND ad_group_ad.status = 'ENABLED'
An enabled ad group whose ad_group.id is absent from the second result has zero enabled ads.
Flag:
- Ad groups with only one RSA (no test running)
ad_group_ad.ad_strengthofPOORorAVERAGE- Any remaining Expanded Text Ads (deprecated)
- Ad groups with zero enabled ads
Microsoft Ads: tool_get_report with report_type: "ad" gives AdStrength, AdType
and AdStatus; tool_get_ads (account_id, ad_group_id) adds editorial status.
7. Asset (extension) coverage
Every field you filter on must also be in SELECT, so campaign.advertising_channel_type
and campaign.status are selected here:
SELECT campaign.name, campaign.advertising_channel_type, campaign.status,
campaign_asset.field_type, campaign_asset.status
FROM campaign_asset
WHERE campaign.advertising_channel_type = 'SEARCH' AND campaign.status = 'ENABLED'
AND campaign_asset.status = 'ENABLED'
Account-level assets apply to every campaign that has none of that type of its own:
SELECT customer_asset.field_type, customer_asset.status
FROM customer_asset
WHERE customer_asset.status = 'ENABLED'
A campaign is covered for a type if it has it at campaign level or the account has it. Flag campaigns missing sitelinks, callouts, or structured snippets at both levels — free real estate and CTR that most accounts leave on the table.
7b. Enhanced when connected: landing pages and keyword gaps
- Landing-page check (GA4 or Plausible). For keywords with QS landing-page "below
average", pull the final URL's sessions and key events: GA4
tool_list_account_summariesfor the property, thentool_run_report(property_id,date_ranges,dimensions: ["landingPage", "sessionGoogleAdsKeyword"],metrics: ["sessions", "keyEvents"]) or Plausiblemcp__cogny__mcp__Plausible__breakdown(site_id,dimension: "visit:entry_page",filtersonvisit:utm_source). A page that converts paid traffic far below the site average confirms the QS diagnosis. - Keyword gaps. Seed
mcp__cogny__mcp__Google_Ads__tool_generate_keyword_ideas(customer_id,seed_keywordsorpage_url) with the top converting terms. If nothing converted in the window, seed it with the 5–10 highest-spend relevant search terms from step 4 (or the top-spend keywords from step 2) and say the seeds are spend-based. If Semrush is connected,domain_paid_keywords(domain,database) on 1–3 competitors shows terms they buy that you don't; Ahrefskeywords_explorer(keywords,country) gives volume and CPC. Report only gaps with real intent; name the source of every volume.
8. Score and report
Search Campaign Audit — [Account Name]
Search Score: X/100 · 30-day spend: [X] · Conv: [X] · CPA: [X]
(one line per engine, each in its own currency: "Google Ads: SEK 41,200 · Microsoft Ads: SEK 6,900")
| Campaign | Spend | Conv | CPA | Avg QS | IS lost (rank) |
|----------|-------|------|-----|--------|----------------|
🔴 Critical — QS ≤4 on top spenders, zero-conversion keyword spend
🟡 Important — broad-match bleed, single-RSA ad groups, missing negatives
🟢 Optimization — extensions, search-term harvesting
Top 3 Actions:
1. [Highest $ impact]
2. ...
3. ...
9. Record findings
{
"title": "Quality Score 3 on 'crm software' — $620/mo at a 2.4x CPA penalty",
"body": "'crm software' (phrase) has QS 3: ad relevance below average, landing page below average. It spent $620 last month at $94 CPA vs $39 account average. Tighten the ad group to one theme, mirror the keyword in RSA headlines, and point it at a dedicated landing page.",
"action_type": "keyword_optimization",
"expected_outcome": "QS to 6+, CPC down 20-35%, CPA toward account average",
"estimated_impact_usd": 380,
"priority": "high"
}
Cloud uses create_ticket with the same content, fields mapped per section 2 of the routing guide.
Action types: keyword_optimization, negative_keyword, match_type,
creative_refresh, campaign_optimization, bidding_strategy.
Recording findings
Use create_ticket when the workspace has it (Cloud) and create_finding otherwise (Solo).
The fields differ; see section 2 of ../marketing-engineer/references/mcp-routing.md.
Critical rules
- Read the QS components, don't just report the number. The fix differs for ad relevance vs landing page vs CTR.
- Zero-conversion search-term spend is the headline waste figure — total it.
- Quote real numbers: QS values, match-type splits, term costs.
- Read-only. Never add negatives or pause keywords — recommend, don't act. Write tools
(
Google_Adstool_add_negative_keywords_to_campaign,Bing_Adstool_add_negative_keywords) are used only if the user asks, and the skill asks first. - One row per engine. Google and Microsoft numbers sit side by side, never summed across currencies; name the engine on every figure.