Blog · Claude Code × GA4

Claude Code + GA4: query Google Analytics 4 from your terminal

Published July 20, 2026 · 8 min read

Google Analytics 4 is the source most agents can't reach — and the one you ask about most. "Did traffic actually drop or is that just the weekend?" "Which channel brought the conversions last month?" "Is organic recovering?" Claude Code can answer every one of those questions the moment it can see the numbers. This guide covers the two realistic paths to getting GA4 into a terminal-native agent — the Data API you write yourself, and a connector that does it in one command — plus the queries worth running once you're in, and the trap that makes GA4 numbers lie if nobody warns the agent about it.

Why GA4 is awkward for a coding agent

Every other analytics chore has an obvious CLI shape. GA4 doesn't, for three reasons that compound:

  • The UI is not scriptable. Explorations are excellent for a human staring at a screen and useless to an agent. The export path is a download button, and every download is a snapshot that is wrong by tomorrow.
  • The Data API has real surface area. OAuth or a service account, property IDs, dimension and metric names that don't match what the UI calls them, request-level quotas, pagination, sampling rules. It's a day of work before your first correct number, and it's your day of work every time the schema shifts.
  • GA4 alone doesn't answer the question you're asking. "Was that campaign worth it" needs GA4 conversions and the ad platform's spend, in one table, under one definition of what a conversion is. Two APIs is more than twice the work of one, because now you also own the join.

So the common workaround is paste-a-CSV-into-chat. That works exactly once. It doesn't refresh, it loses the context of how fresh the data was, and it teaches your agent nothing it can reuse tomorrow.

The shortcut: give the agent a data backend

TableBI is a data backend built to be driven from a CLI by an agent like Claude Code. GA4 is a first-class live connector: one OAuth round-trip in the browser, pick the property, and the sync runs on a schedule from then on. Three moves — Pipe the source in, Ask in read-only SQL, Pin the answer as a live dashboard.

No second model in the loop: TableBI hosts no LLM and runs $0 inference. Your Claude Code session is the brain; the backend returns deterministic SQL results plus trust metadata. Your GA4 data is never routed through somebody else's model.

Connect GA4 in one command

Install the CLI and plant the skill so the agent knows the tool exists and when to reach for it:

terminal
# install the CLI, then teach your agent to drive it
npm i -g @tablebi/cli
tablebi login
tablebi install

tablebi install drops a skill into ~/.claude/skills; after that, "connect my GA4" inside Claude Code is enough — the agent knows the command. Connecting is one OAuth round-trip: the browser opens, you authorize, you pick the property, and the first sync runs.

terminal
# browser opens for OAuth → choose the property → first sync
tablebi connect ga4

# pull fresh rows later (incremental, with retries handled for you)
tablebi sync ga4

# what's connected, and how fresh is each source?
tablebi sources

That is the entire integration. No Data API client, no service account key in your repo, no dimension-name lookup table, no pagination loop to maintain.

Two altitudes: ga4_raw and facts

GA4 data lands at two levels, and knowing which to query is most of the skill:

  • ga4_raw — what GA4 reported, nothing averaged away: date, property, channel, country, sessions, users, page_views, conversions, revenue. This is where you go for GA4-native questions and for forensics when a number looks wrong.
  • facts — the cross-channel table where GA4 sits beside Search Console, Meta Ads and Google Ads under shared dimensions (date, platform, account, campaign, channel, country…) and shared measures (impressions, clicks, cost, conversions, conversion_value). This is where "was it worth it" gets answered.

Start any session by rehydrating — definitions, connected sources, freshness and the command cheatsheet in one JSON blob the agent holds in context:

terminal
tablebi context --json

Four GA4 queries worth stealing

1. Sessions and conversions by channel. The default "where is traffic actually coming from" question, over the last 28 days:

claude code → tablebi
tablebi ask "SELECT channel, SUM(sessions) AS sessions,
             SUM(conversions) AS conv, SUM(revenue) AS revenue
             FROM ga4_raw
             WHERE date >= (SELECT MAX(date) FROM ga4_raw) - 28
             GROUP BY channel ORDER BY sessions DESC"

2. Week over week, by channel. Trends beat totals — this is the shape that tells you whether a dip is real or a weekday artifact:

claude code → tablebi
tablebi ask "SELECT channel,
             SUM(CASE WHEN date > (SELECT MAX(date) FROM ga4_raw) - 7
                      THEN sessions ELSE 0 END) AS this_week,
             SUM(CASE WHEN date <= (SELECT MAX(date) FROM ga4_raw) - 7
                      THEN sessions ELSE 0 END) AS prev_week
             FROM ga4_raw
             WHERE date >= (SELECT MAX(date) FROM ga4_raw) - 14
             GROUP BY channel ORDER BY this_week DESC"

3. Conversion rate per channel, without doing the arithmetic in the model. Definition macros (ctr, cpa, cvr, roas, aov…) live in the query engine, so the agent never re-derives a ratio slightly wrong:

claude code → tablebi
tablebi ask "SELECT channel, cvr(SUM(conversions), SUM(clicks)) AS cvr,
             SUM(conversions) AS conv
             FROM facts WHERE platform = 'ga4'
               AND date >= (SELECT MAX(date) FROM facts) - 28
             GROUP BY channel ORDER BY conv DESC"

4. The one GA4 can't answer alone — spend against outcome. Same table, both sides of the equation:

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

Notice what every window has in common: (SELECT MAX(date) FROM …) - N, never current_date. Analytics data always lands late, so clock-anchored math silently reports a half-empty final day as a crash. Anchoring to the data's own high-water mark is the difference between a real alarm and a fake one — the same discipline we walk through in Claude Code analytics.

The trap: GA4 sessions are not Search Console clicks

The single most common bad analysis in this category is an agent trying to reconcile GA4 sessions with GSC clicks, finding a 20% gap, and confidently inventing a story about tracking being broken. They are different populations counted different ways: a GSC click is a click on a search result; a GA4 session is on-site behavior with its own deduplication and its own consent-dependent losses. They were never supposed to be equal.

So every answer ships with a trust block — per-source freshness (the exact date each source runs through), caveats, and lineage. That GSC-versus-GA4 mismatch is written down as an explicit caveat, and the agent reads it before you do. The result is an agent that explains the gap instead of raising a false alarm about it. If Search Console is your main lane, Claude Code for SEO covers that side in depth.

Pin it: GA4 on a live URL

When a GA4 view is worth keeping, don't screenshot it — pin it. A title, a chart kind after two colons (line, bar, hbar, pie, area, scorecard), and the query:

claude code → tablebi
tablebi pin --title "GA4 traffic overview" \
  --widget "Daily sessions (28d)::line=SELECT date, SUM(sessions) AS sessions FROM ga4_raw …" \
  --widget "Sessions by channel::hbar=SELECT channel, SUM(sessions) FROM ga4_raw …"
✓ published → https://dk.tablebi.com/d/dsh_…  (public, read-only, self-refreshing)

The output is a URL you hand to a client or a co-founder — no React app, no BI seat. Here's a real pinned dashboard, refreshing as its sources sync:

Connector vs. writing the Data API yourself

Rolling your own is a legitimate choice — Claude Code will happily write the client. It's the second week that decides it:

  • Auth and refresh are ongoing, not one-time. Tokens expire, scopes change, and the failure surfaces as an empty result set, not an exception. A managed connector absorbs that.
  • Incremental sync is the actual work. Backfill, re-pulls for late-arriving rows, retries — that's most of the code in any pipeline, and none of it is your product.
  • One source is never the question. The moment you want GA4 conversions next to ad spend, a hand-rolled client becomes a hand-rolled warehouse with a hand-rolled definition layer.
  • Definitions decide whether the analysis is right. An agent handed two raw exports will pick an interpretation and narrate it with total confidence. The failure mode isn't bad math — it's mislabeled math.

For the dashboard side of this workflow, see Claude Code dashboards; for bringing in channels that have no live API at all, CSV analysis with Claude Code covers the file path into the same definitions.

FAQ

Can Claude Code read my Google Analytics 4 data?

Not on its own — it's a coding agent with no connection to GA4. Run tablebi connect ga4 once, authorize in the browser and pick the property; from then on the agent queries GA4 with read-only SQL from the terminal.

Do I need the GA4 Data API or a service account?

No. The connector handles authorization, property selection, pagination and incremental syncing. You never write a Data API client or keep a service account key around.

What GA4 fields can I query?

ga4_raw holds date, property, channel, country, sessions, users, page_views, conversions and revenue as GA4 reported them. facts holds the cross-channel view, so GA4 conversions sit beside ad spend under one set of definitions.

Why don't my GA4 sessions match Search Console clicks?

Different populations, counted differently — a search-result click versus on-site behavior with its own deduplication. That mismatch ships as an explicit caveat in every trust block, so your agent explains the gap instead of "fixing" it.

Try it

Put GA4 one command away from your Claude Code session.

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