# Reading search terms and existing negatives

Everything in this file is read-only. Three sources work; use whichever the user has.

## Google Ads web interface

- Search terms (Search and AI Max): Campaigns, Insights and reports, Search terms (help page, 2026-10-17). Remove filters, set the date range, add the Campaign, Ad group, Keyword, Match type and Added/Excluded columns plus Conversions and Conv. value, then download as CSV.
- Performance Max search terms: in the Search terms report choose "Search terms and landing pages for Performance Max".
- Shared negative keyword lists: Tools, Shared library, Exclusion lists, Negative keyword lists tab. Open each list to read its keywords and the campaigns it is applied to.
- Account-level negatives: the "Negative keywords" section of the account settings.
- Menu labels move. If a path above is not where the help page says, follow the live help page and tell the user.

## Google Ads Editor

- An account export (Account, Export, then "Export whole account" or "Export selected campaigns and ad groups", per Editor's export help page on 2026-10-17) gives keywords and ad group and campaign negatives with a `Criterion Type` column. Exports seen in practice are UTF-16 LE, tab separated, named `.csv`; `scripts/negatives.mjs` reads them directly as the `--keywords` file, treats rows whose type contains "Negative" as existing negatives, skips removed rows and treats keywords in paused campaigns or ad groups as paused.
- Seen in practice: the keywords inside shared negative lists were missing from the export and from Editor's local data, which only held each list's name and keyword count. A keyword was blocked by one of those lists and nothing in the export showed why. Always read shared list contents from the web interface or the API and pass them with `--existing`.
- In practice Editor 2.13 did not provide search terms for Search campaigns; Editor 2.11 added a search term report for Performance Max. Download Search terms from the web interface or the API.
- Run Get recent changes before exporting so the export matches the live account.

## Google's Google Ads MCP server

Google's guide (updated 2026-10-16) labels its server (`googleads/google-ads-mcp`) "Read-only (current release)", with three tools: `list_accessible_customers`, `search` (runs GAQL) and `get_resource_metadata` (describes a resource's fields). The repository notes that clients can list them with a namespace prefix, such as `customers_list_accessible_customers`, `search_search` and `metadata_get_resource_metadata`, depending on its `tools_config.yaml`. Setup is covered by Google's `google-ads-api-mcp-setup` skill in `google/skills`.

1. Call `list_accessible_customers` and confirm the intended customer with the user. Ask for the account currency and time zone too.
2. Before each query below, call `get_resource_metadata` for the resource in `FROM` and confirm every field and segment exists and is selectable and filterable together. Field names here were taken from Google's guides, samples and field reference on 2026-10-17 where marked; the rest are expected names that the metadata call must confirm.
3. Save results as CSV with the GAQL field names as headers. The script recognises names such as `search_term_view.search_term`, `search_term_view.status`, `campaign.name`, `ad_group.name`, `metrics.clicks`, `metrics.cost_micros` (divided by 1,000,000 automatically), `metrics.conversions` and any column ending in `.status`.
4. Say how many rows came back. A `LIMIT` or a truncated response means the result is partial and must not be summed as an account total.
5. Keep the exact query text: it is part of the evidence the summary cites.

Search and AI Max search terms (resource `search_term_view` exists in the current API reference; check the fields):

```sql
SELECT
  campaign.name,
  ad_group.name,
  search_term_view.search_term,
  search_term_view.status,
  segments.search_term_match_type,
  metrics.impressions,
  metrics.clicks,
  metrics.cost_micros,
  metrics.conversions,
  metrics.conversions_value
FROM search_term_view
WHERE segments.date BETWEEN '2026-06-24' AND '2026-09-22'
```

- Add `segments.search_term_match_source` to separate AI Max matches from keyword matches (named in Google's AI Max reporting guide).
- Adding `segments.keyword.info.text` splits each term by the keyword it matched; the script still sums n-grams correctly because each row is counted once.

Campaign cost for the same campaigns and dates. API results have no totals rows, so this is the comparison for the visible share. Sum `metrics.cost_micros` over the campaigns that appear in the search terms file, divide by 1,000,000 and pass it as `--total-cost`:

```sql
SELECT campaign.name, metrics.cost_micros, metrics.clicks, metrics.conversions
FROM campaign
WHERE segments.date BETWEEN '2026-06-24' AND '2026-09-22'
  AND metrics.cost_micros > 0
```

Performance Max search terms (Google's Performance Max reporting guide names `campaign_search_term_view` for this; the field reference warns that Performance Max rows are excluded when keyword segments are used, so leave them out):

```sql
SELECT
  campaign.name,
  campaign_search_term_view.search_term,
  metrics.impressions,
  metrics.clicks,
  metrics.cost_micros,
  metrics.conversions,
  metrics.conversions_value
FROM campaign_search_term_view
WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX'
  AND segments.date BETWEEN '2026-06-24' AND '2026-09-22'
```

Existing negatives. Removed criteria, detached lists and removed lists come back from these resources unless filtered: `ad_group_criterion.status` and `campaign_criterion.status` are ENABLED, PAUSED or REMOVED, and `campaign_shared_set.status` and `shared_set.status` are ENABLED or REMOVED (API reference, 2026-10-17). Keep the status columns in the saved CSV as a second guard; the script skips rows whose status is removed.

```sql
-- Ad group negatives
SELECT campaign.name, ad_group.name, ad_group_criterion.keyword.text,
  ad_group_criterion.keyword.match_type, ad_group_criterion.negative, ad_group_criterion.status
FROM ad_group_criterion
WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = TRUE
  AND ad_group_criterion.status != 'REMOVED'

-- Campaign negatives
SELECT campaign.name, campaign_criterion.keyword.text, campaign_criterion.keyword.match_type,
  campaign_criterion.negative, campaign_criterion.status
FROM campaign_criterion
WHERE campaign_criterion.type = 'KEYWORD' AND campaign_criterion.negative = TRUE
  AND campaign_criterion.status != 'REMOVED'

-- Which negative keyword lists each campaign uses (based on Google's find-and-remove-criteria-from-shared-set sample)
SELECT campaign.name, shared_set.id, shared_set.name, shared_set.type, campaign_shared_set.status
FROM campaign_shared_set
WHERE campaign_shared_set.status = 'ENABLED'
  AND shared_set.type = 'NEGATIVE_KEYWORDS'
  AND shared_set.status = 'ENABLED'

-- The keywords inside lists (same sample)
SELECT shared_set.id, shared_set.name, shared_set.type, shared_set.status, shared_criterion.type,
  shared_criterion.keyword.text, shared_criterion.keyword.match_type
FROM shared_criterion
WHERE shared_set.type IN ('NEGATIVE_KEYWORDS', 'ACCOUNT_LEVEL_NEGATIVE_KEYWORDS')
  AND shared_set.status = 'ENABLED'
```

- Shared set types in Google's shared sets guide (updated 2026-10-15): `NEGATIVE_KEYWORDS` lists attach to campaigns through `campaign_shared_set`; the `ACCOUNT_LEVEL_NEGATIVE_KEYWORDS` list attaches to the account through `CustomerNegativeCriterion`; `BRANDS` lists attach to a campaign as a campaign criterion with `brand_list` set, not through `campaign_shared_set`. Read brand exclusions in the web interface unless the metadata call confirms the brand list fields.
- Lists created in a manager account are created and edited in the manager's shared library (help page on manager account lists). If `campaign_shared_set` returns a list whose keywords do not come back from the client account's `shared_criterion` query, read them from the manager account (Google's guide uses the manager ID as `login-customer-id` for manager-level shared sets) or from the web shared library. Never skip the list.
- Save the existing negatives as one or more CSVs that contain only negatives, and pass them to the conflict check with `--existing a.csv,b.csv`.
- Join list keywords to the campaigns from `campaign_shared_set` first: one row per keyword per campaign, with `campaign.name` filled. Leave the campaign blank only for the account-level list or a list applied to every campaign.
- For the `--keywords` file, query keywords from `ad_group_criterion` with `ad_group_criterion.negative = FALSE` and `ad_group_criterion.status != 'REMOVED'`, and select `ad_group_criterion.status`, `ad_group.status` and `campaign.status`, so the check can separate CONFLICT from PAUSED.

## Servers that can write

Google's server was read-only in the release checked on 2026-10-17, and its guide sends mutate work to the Google Ads API Developer Assistant. If a connected Google Ads server exposes tools that add or remove criteria, confirm its publisher and version before using it. Keep using only its read tools for this workflow, and treat any write as a change list that needs the user's approval for that exact list, followed by a read-back of the resulting state.
