The Query Cookbook
GAQL account hygiene: disapprovals, budget caps, conversions and change history
Four read-only queries that surface disapproved ads, budget-constrained campaigns, the state of your conversion actions and who changed what recently.
Most account problems are not strategy problems, they are hygiene problems: an ad quietly disapproved, a campaign throttled by its own budget, a conversion action switched off, a change nobody remembers making. None of these show up in a performance chart until they have already cost you. This pack is the morning walk round the account that catches them.
Every query reads only. Nothing here changes an ad, a budget or a conversion setting.
Before you start
- These use windows like
LAST_30_DAYSandLAST_14_DAYS. Adjust to taste, but note the change-history query has a hard limit on how far back it can look (see below). - GAQL has no
COUNT, so where the brief is "how many disapproved ads", you get the rows and count them yourself, or group them in the sheet or script you run this from. campaign_budget.amount_microsis in millionths of the account currency.
Disapproved ads, by campaign
A disapproved ad is an ad that is not running, silently. This query lists every ad currently disapproved, with its campaign, ad group and the policy review status behind the decision, skipping removed ads so you only see live problems. Read the approval status: DISAPPROVED is off entirely, while APPROVED_LIMITED runs but with restrictions worth knowing about.
SELECT
campaign.name,
ad_group.name,
ad_group_ad.ad.id,
ad_group_ad.ad.type,
ad_group_ad.policy_summary.approval_status,
ad_group_ad.policy_summary.review_status
FROM ad_group_ad
WHERE ad_group_ad.policy_summary.approval_status = 'DISAPPROVED'
AND ad_group_ad.status != 'REMOVED'
To count them per campaign, run it and tally the rows by campaign.name; there is no aggregate function in GAQL to do the counting for you.
Campaigns held back by their budget
A campaign losing impression share to budget is one where demand exists and money is the only thing stopping it. This query lists enabled campaigns that lost more than ten per cent of their search impression share to budget over the window, alongside the budget amount and the spend, so you can see which constrained campaigns are worth funding. The lost-impression-share fields are fractions from 0 to 1, so 0.1 is ten per cent.
SELECT
campaign.name,
campaign_budget.amount_micros,
metrics.cost_micros,
metrics.search_budget_lost_impression_share,
metrics.search_rank_lost_impression_share
FROM campaign
WHERE segments.date DURING LAST_30_DAYS
AND campaign.status = 'ENABLED'
AND metrics.search_budget_lost_impression_share > 0.1
ORDER BY metrics.search_budget_lost_impression_share DESC
The rank-lost figure rides along for context: budget-lost says "add money", rank-lost says "the problem is bids or quality, not budget", and seeing both stops you funding a campaign whose real issue is not money.
Conversion actions and their status
Everything downstream depends on conversion actions being set up the way you think they are. This query lists every conversion action in the account with its status, type, category, counting method and whether it counts towards the primary goal. Look for anything important sitting at a status other than ENABLED, and for the counting type ("one" versus "every") being wrong for the action.
SELECT
conversion_action.name,
conversion_action.status,
conversion_action.type,
conversion_action.category,
conversion_action.counting_type,
conversion_action.primary_for_goal
FROM conversion_action
ORDER BY conversion_action.name
Recent change history
When a metric jumps and nobody admits to touching anything, the change history is the record of record. This query lists recent changes with the timestamp, the user, the kind of resource affected, the client used and the fields that changed. It is the fastest way to connect a Tuesday performance shift to a Monday afternoon edit.
SELECT
change_event.change_date_time,
change_event.user_email,
change_event.change_resource_type,
change_event.client_type,
change_event.changed_fields
FROM change_event
WHERE change_event.change_date_time DURING LAST_14_DAYS
ORDER BY change_event.change_date_time DESC
LIMIT 200
Two rules the change_event resource enforces, and this query already satisfies: you must filter on change_event.change_date_time, and the range cannot reach further back than the last 30 days (the API only retains that window), and you must supply a LIMIT (the maximum is 10,000). Drop either and the query is rejected, so keep the date filter and the LIMIT in place.
How to run these
Any GAQL runner takes these unchanged. The three usual routes:
- The Google Ads API: pass each query to
GoogleAdsService.SearchStreamagainst your customer id. - The official interactive Query Builder in the Google Ads API docs: paste a query to validate the fields and preview the response.
- A Google Ads Script: hand the query to
AdsApp.search(query)and iterate the rows, keeping the whole check inside the account with no API credentials.
What these queries will not do
They report, they do not fix. A disapproved ad still needs a human to read the policy and decide whether to appeal or rewrite; a budget-constrained campaign still needs a deliberate funding decision; a switched-off conversion action might be off on purpose. The change-history window is capped at the last 30 days by Google, so this is not a long-term audit log; if you need one, export the rows on a schedule and keep them yourself. And because GAQL cannot count, treat every "how many" question here as "list them, then tally", not as something the query answers on its own.