Blog · Claude Code × Google Ads

Claude Code × Google Ads: analyze campaign spend from the CLI

Published July 17, 2026 · 7 min read

The Google Ads UI is built for editing campaigns, not for answering questions — and Claude Code, out of the box, can't see your account at all. The fix is one OAuth round-trip: tablebi connect google_ads pipes the account into a data backend your agent queries with read-only SQL. From then on, "rank my campaigns by CPA" or "which ad groups spent money and converted nothing?" are one-line questions in the terminal where you already work — no tab-switching, no CSV exports, no pivot tables. This is the practical guide to connecting Google Ads to Claude Code: the three queries that replace the export ritual, and how to pin the result as a weekly report that refreshes itself.

The report you want is never one screen away

Everyone who runs Google Ads knows the ritual. The question is simple — "what's actually converting, and what's just spending?" — but the path is not: open the Campaigns view, adjust the date range, add a segment, realize the column you need lives one level down, drill into each campaign for its ad groups, download a CSV per view, rebuild the picture in a spreadsheet. Next week: the same ritual, from scratch. The UI is a competent editor; as a question-answering surface it makes you assemble every answer by hand, one drill-down at a time.

The official escape hatch is the Google Ads API, and it's a genuine project: a developer token, an OAuth application, GAQL (a query language you'll use nowhere else) and the account-hierarchy plumbing around it. Nobody stands all of that up to answer one weekly question. So the data stays trapped behind the UI — until an agent with a data backend inverts the deal: sync the account once, and every future question is just SQL.

Connect Google Ads to Claude Code once

terminal
npm i -g @tablebi/cli
tablebi login
tablebi install

# browser opens for OAuth; the account starts syncing
tablebi connect google_ads

tablebi install plants a skill in ~/.claude/skills, so Claude Code recognizes these commands from then on — "how did Google Ads do last week?" in plain English is enough to trigger the right queries. (The broader multi-source setup — Search Console, GA4, Meta — is walked through in Claude Code for marketing.)

Synced data lands at two altitudes:

  • facts — the cross-channel table where Google Ads rows share one set of definitions with every other connected source. Dimensions: date, platform, account, campaign, ad_group, ad, channel, country; measures: clicks, impressions, cost, conversions, conversion_value. Ranking and comparison queries live here.
  • google_ads_raw — the platform-faithful table, fields as Google reports them, for when the unified view isn't granular enough.

Worth saying explicitly: this is a read-only path. tablebi ask executes read-only SQL over synced data; nothing your agent does here can touch bids, budgets or campaign settings in Google Ads itself.

Two operational notes. Very large accounts sync in segments, streamed — you're not waiting on one giant full-history load. And each new session starts with tablebi context --json, a single call that rehydrates the agent with definitions, connected sources and how fresh each one is.

Three queries that replace the export ritual

1. The campaign leaderboard: spend, conversions, CPA

claude code → tablebi
tablebi ask "SELECT campaign, SUM(cost) AS spend, SUM(conversions) AS conv,
             cpa(SUM(cost), SUM(conversions)) AS cpa
             FROM facts
             WHERE platform = 'google_ads'
               AND date >= (SELECT MAX(date) FROM facts) - 28
             GROUP BY campaign ORDER BY spend DESC LIMIT 20"

Two idioms to notice. cpa(…) is a definition macro — the arithmetic is frozen in the engine, alongside roas, ctr, cpc, cpm and cvr, so the agent never re-derives cost-per-anything wrong at 11pm. And the date window anchors to (SELECT MAX(date) FROM facts) - 28 — the newest synced day, not the wall clock; the query layer rejects current_date by design, so a window can never silently include days that haven't synced yet.

2. Ad groups burning money with zero conversions

claude code → tablebi
# the cross-campaign scan that's painful to assemble in the UI
tablebi ask "SELECT campaign, ad_group, SUM(cost) AS spend, SUM(clicks) AS clicks
             FROM facts
             WHERE platform = 'google_ads'
               AND date >= (SELECT MAX(date) FROM facts) - 28
             GROUP BY campaign, ad_group
             HAVING SUM(cost) > 0 AND SUM(conversions) = 0
             ORDER BY spend DESC"

This is the query that pays for the setup. In the web UI it means visiting every campaign or exporting everything; here it's one pass over the whole account. The follow-ups are exactly what an agent is good at: "now break the worst one down by ad", "show me its daily spend", "which country is it bleeding in?" Each is another one-liner, not another export — and when the unified columns aren't enough, the same questions run against google_ads_raw.

3. Google versus Meta, one definition of ROAS

claude code → tablebi
tablebi ask "SELECT platform, SUM(cost) AS spend,
             roas(SUM(conversion_value), SUM(cost)) AS roas,
             cpa(SUM(cost), SUM(conversions)) AS cpa
             FROM facts
             WHERE platform IN ('google_ads', 'meta_ads')
               AND date >= (SELECT MAX(date) FROM facts) - 28
             GROUP BY platform ORDER BY spend DESC"

This is the point of the facts altitude: platform is just another dimension, and cost and conversion value mean the same thing on every row, so a cross-network ROAS comparison is a GROUP BY rather than a data-engineering project. Treat the result as directional — Google and Meta attribute conversions differently, and the trust block that ships with every answer exists to surface exactly this kind of caveat, alongside per-source freshness and lineage, so a two-row table can't hide it. The Meta side of this workflow has its own guide: Meta Ads reporting.

These three are the skeleton, not the ceiling. The same handful of columns answers "which country converts cheapest?" (group by country), "is CTR decaying on my top spender?" (ctr(SUM(clicks), SUM(impressions)) over date) or "how does brand compare to non-brand?" (a campaign-name filter). The pattern never changes: a question in plain English, one deterministic query, an answer with its trust block attached — and no browser tab anywhere in the loop.

Pin it: a Google Ads weekly that refreshes itself

Once the queries above have earned a permanent place, stop re-running them and pin them:

claude code → tablebi
tablebi pin --title "Google Ads weekly" \
  --widget "Daily spend (28d)::line=SELECT date, SUM(cost) AS spend
    FROM facts WHERE platform = 'google_ads'
    AND date >= (SELECT MAX(date) FROM facts) - 28
    GROUP BY date ORDER BY date" \
  --widget "Campaign leaderboard=SELECT campaign, SUM(cost) AS spend,
    SUM(conversions) AS conv, cpa(SUM(cost), SUM(conversions)) AS cpa
    FROM facts WHERE platform = 'google_ads'
    AND date >= (SELECT MAX(date) FROM facts) - 28
    GROUP BY campaign ORDER BY spend DESC LIMIT 10"
✓ published → https://dk.tablebi.com/d/dsh_…  (public, read-only, self-refreshing)

Widgets tagged ::line render as line charts; the rest render as tables. The URL is public, read-only and self-refreshing — syncs run on a schedule and are also triggered when someone opens the page, so the Monday check-in shows Monday's numbers with no Monday exports. Hand it to the founder, the client or your own future self, and keep iterating on the queries behind it whenever a question comes back. For the full query-side repertoire beyond ads — raw-table forensics, freshness details, more macro patterns — see Claude Code analytics.

FAQ

Can Claude Code access Google Ads directly?

No — Claude Code has no built-in Google Ads connector, and the official API route requires a developer token, an OAuth application and GAQL. TableBI bridges the gap: one browser OAuth via tablebi connect google_ads, and the agent gets read-only SQL over the synced account.

Can the agent change my campaigns or budgets?

No. tablebi ask executes read-only SQL over synced data. Nothing in this workflow writes back to Google Ads — bid and budget changes stay in Google Ads, made by a human.

How fresh is the synced Google Ads data?

Every answer ships with a trust block showing exactly which date each source is synced through. Pinned dashboards refresh themselves: syncs run on a schedule and are also triggered when someone opens the page.

Does this work on very large Google Ads accounts?

Yes. Google Ads syncs are segmented and streamed rather than loaded as one giant window, so account size doesn't change the workflow.

Try it

Give your Claude Code a data backend in five minutes.

terminal
npm i -g @tablebi/cli && tablebi install