Blog · SEO × Dashboards

SEO dashboard: build one your agent maintains, in one command

Published July 17, 2026 · 8 min read

An SEO dashboard is only worth having if it answers your actual Monday questions and stays current without you touching it. Most of what ranks for this term is template galleries — fifteen screenshots, forty KPIs, no opinion about which numbers matter. This guide goes the other way: define the five questions an SEO dashboard exists to answer, write five small SQL queries against raw Search Console data, and publish them as one live, self-refreshing URL with a single command. And the maintenance doesn't fall on you or on a BI seat — it falls on an AI agent that can query your search data directly.

The five questions an SEO dashboard should answer

Strip away the template clutter and a dashboard is just a set of standing questions. For SEO, five cover nearly everything that changes a decision:

  1. Which queries are rising, and which are falling? Not a wall of every keyword — the movers. A query that gained or lost meaningful clicks week over week is a signal; the other three thousand flat rows are noise.
  2. Which pages earn less CTR than their position should deliver? A page sitting at position 3 with a 1% CTR has a title or snippet problem. Fixing it is the cheapest win in SEO — no new content, no links, just better packaging for rankings you already own.
  3. How fresh is the data I'm looking at? Search Console finalizes data two to three days late. A dashboard that doesn't say how current it is turns every end-of-chart dip into a false alarm.
  4. How is the portfolio doing as a whole? If you run more than one site, you need the one-row-per-site view before you need any drill-down.
  5. What actually changed this week? Page-level week-over-week deltas — the difference between reporting and rereading the same totals every Monday.

Notice what's not on the list: gauges, word clouds, an "SEO health score." A dashboard earns its screen space by changing what you do next, and the five questions above are the ones that do.

Why static SEO dashboards rot

If you've ever assembled one of these in a spreadsheet or a template tool, you know how it ends. Two failure modes do the killing.

Data hauling. Every static dashboard runs on exports: download the CSV, paste it into the sheet, fix the columns that shifted, screenshot it for the deck. The export is stale the moment it lands, and because the hauling is manual it happens weekly at best — right up until the week you're busy, and then the dashboard is quietly three weeks old.

Definition drift. One chart excludes brand queries, another doesn't. One tab uses a 28-day window, another uses "this month." CTR gets recomputed slightly differently in two places. A month in, two numbers on the same page disagree and nobody can say why — and the moment a dashboard needs an asterisk, people stop trusting all of it.

The fix isn't a prettier template. It's structural: data that syncs itself, every number defined in SQL you can actually read, and upkeep owned by something with infinite patience for chores. That last part is where an agent comes in.

Connect Search Console once

The build below uses TableBI, a hosted data backend designed to be driven by Claude Code — Anthropic's terminal-based coding agent — or any agent that can run a CLI. The shape is three verbs: pipe sources in, ask with read-only SQL, pin the result to a live URL. Install the CLI and plant the skill so the agent knows the commands exist:

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

Then connect Search Console — one OAuth round-trip in the browser:

terminal
tablebi connect gsc --site sc-domain:example.com

Data syncs into two altitudes. facts holds cross-channel unified definitions — useful the day you put ads data next to search. search_console_raw is the one this dashboard runs on: lossless rows straight from GSC, with site, query, page, clicks, impressions and position columns. From then on, an agent session opens with tablebi context --json, which returns connected sources, data freshness and the command cheatsheet in one JSON blob it can hold in context.

The five widgets, in SQL

Each question becomes one query. Question 3 — freshness — needs no SQL at all: every answer ships with a trust block that states per-source freshness, so five widgets cover the list. One convention first: every window is anchored to MAX(date), never current_date.

Why MAX(date)? GSC finalizes data 2–3 days late, so a window that ends "today" ends in empty days and drags every average down. WHERE date >= (SELECT MAX(date) FROM search_console_raw) - 28 always covers 28 real days of data.

1 — The pulse. Daily clicks over 28 days, as a line. It's the chart everyone looks at first, so give it to them:

claude code → tablebi
tablebi ask "SELECT date, SUM(clicks) AS clicks
             FROM search_console_raw
             WHERE date >= (SELECT MAX(date) FROM search_console_raw) - 28
             GROUP BY date ORDER BY date"

2 — Query movers. Clicks this week versus the week before, ranked by the swing. Flip the sort and the same widget shows what's bleeding:

claude code → tablebi
tablebi ask "SELECT query,
             SUM(CASE WHEN date >= (SELECT MAX(date) FROM search_console_raw) - 7
                 THEN clicks ELSE 0 END) AS last_7d,
             SUM(CASE WHEN date < (SELECT MAX(date) FROM search_console_raw) - 7
                 THEN clicks ELSE 0 END) AS prior_7d
             FROM search_console_raw
             WHERE date >= (SELECT MAX(date) FROM search_console_raw) - 14
             GROUP BY query
             ORDER BY last_7d - prior_7d DESC LIMIT 15"

3 — CTR laggards. The highest-leverage query in the set: pages ranking in the top five positions whose CTR doesn't match the real estate. Note the ctr() macro — the definition lives in the backend, not re-derived by hand in every chart:

claude code → tablebi
tablebi ask "SELECT query, page, AVG(position) AS avg_position,
             SUM(impressions) AS impressions,
             ctr(SUM(clicks), SUM(impressions)) AS ctr
             FROM search_console_raw
             WHERE date >= (SELECT MAX(date) FROM search_console_raw) - 28
             GROUP BY query, page
             HAVING AVG(position) <= 5 AND SUM(impressions) >= 500
             ORDER BY ctr ASC LIMIT 15"

Everything this returns is a rewrite candidate: you already rank; the snippet just isn't earning the click.

4 — Portfolio overview. One row per site — the thirty-second answer to "how are we doing overall":

claude code → tablebi
tablebi ask "SELECT site, SUM(clicks) AS clicks,
             SUM(impressions) AS impressions,
             ctr(SUM(clicks), SUM(impressions)) AS ctr
             FROM search_console_raw
             WHERE date >= (SELECT MAX(date) FROM search_console_raw) - 28
             GROUP BY site ORDER BY clicks DESC"

5 — Pages, week over week. The same CASE pattern as the movers, grouped by page instead of query — which URLs gained ground and which gave it up:

claude code → tablebi
tablebi ask "SELECT page,
             SUM(CASE WHEN date >= (SELECT MAX(date) FROM search_console_raw) - 7
                 THEN clicks ELSE 0 END) AS last_7d,
             SUM(CASE WHEN date < (SELECT MAX(date) FROM search_console_raw) - 7
                 THEN clicks ELSE 0 END) AS prior_7d
             FROM search_console_raw
             WHERE date >= (SELECT MAX(date) FROM search_console_raw) - 14
             GROUP BY page ORDER BY last_7d DESC LIMIT 15"

You don't have to write any of this yourself. Describe the widget in plain English inside a Claude Code session — "show me top-5 rankings with weak CTR" — and the agent writes the SQL, runs it read-only, and shows you the table to sanity-check. The SQL stays visible either way, which is exactly what keeps definitions from drifting.

Pin it: one command, one live URL

Once the five queries look right, freeze them into a dashboard. ::line renders a widget as a line chart; the default is a table:

claude code → tablebi
tablebi pin --title "SEO overview" \
  --widget "Daily clicks (28d)::line=SELECT date, SUM(clicks) AS clicks FROM search_console_raw …" \
  --widget "Query movers, 7d vs prior=SELECT query, … FROM search_console_raw …" \
  --widget "CTR laggards (top-5 positions)=SELECT query, page, … FROM search_console_raw …" \
  --widget "Portfolio by site=SELECT site, … FROM search_console_raw …" \
  --widget "Pages, week over week=SELECT page, … FROM search_console_raw …"
✓ published → https://<ws>.tablebi.com/d/dsh_…  (public, read-only, self-refreshing)

What comes back is a public, read-only URL. It refreshes itself — syncs are triggered by visits and run on a schedule — so the link you drop in a Slack channel in January is still current in June. Viewers see a "Powered by TableBI" footer, and next to each widget an ask button: anyone looking at the dashboard can copy a one-sentence question to send back to whoever owns it.

Maintenance becomes a conversation. "Filter brand queries out of the movers widget" is one sentence to your agent; it edits the SQL and re-pins. The dashboard never turns into the thing nobody dares touch — which is how the static ones die.

Here's a real one — live Search Console data across a portfolio of sites, pinned exactly this way. It's not a mockup; it refreshes as the source syncs:

How to read GSC's 2–3 day delay

Search Console is not a realtime feed: Google finalizes search data two to three days after the fact. That's official behavior, not a broken sync, and it shapes how you read the dashboard:

  • The last day or two of any clicks chart will sag. That's data still being finalized, not a ranking crash. Judge the trend; ignore the ragged edge.
  • Comparisons should only cover complete days. Anchoring windows to MAX(date), as every query above does, keeps week-over-week math honest.
  • Freshness is displayed, not guessed. Answers carry a trust block — per-source freshness plus caveats like "GSC clicks ≠ GA4 sessions," two populations that will never reconcile — so both you and the agent see how current each source is before comparing anything to anything.

Where this fits — and where it doesn't

If you need drag-and-drop self-serve BI for a large team — dozens of stakeholders assembling their own views, permission governance, an admin console — a traditional BI platform is the right buy; that's what the category is built for. This approach is a different architecture: agent-driven instead of GUI-driven, a live public URL instead of a seat-gated workspace, SQL as the single source of truth instead of a modeling layer, and pricing that isn't per seat. For a solo operator or a small team already working alongside an agent, that's the lighter, truer shape.

If Search Console is your main habitat, the broader agent workflow — ad-hoc questions, weekly summaries, anomaly checks — is covered in Claude Code for SEO. When the deliverable is a document rather than a link, see the marketing report template guide. And for the general pattern of agent-built dashboards beyond search, there's Claude Code dashboards.

FAQ

What metrics should an SEO dashboard include?

Clicks, impressions, CTR and position from Search Console — organized as standing questions rather than a KPI wall: a daily clicks trend, week-over-week query and page movers, CTR laggards in top positions, and a per-site portfolio row if you run more than one site.

Why is my Search Console data 2–3 days behind?

That's Google, not your pipeline: Search Console finalizes data with an official 2–3 day delay. Anchor date windows to the newest available date rather than today, and treat the last day or two of any chart as provisional rather than a traffic drop.

Do I need to know SQL to build this dashboard?

No. You describe each widget in plain English inside a Claude Code session and the agent writes the query, runs it read-only, and shows you the result to sanity-check. The SQL stays visible, so every definition can be audited instead of trusted blind.

Can people I share the dashboard with edit it?

No. The pinned URL is public but read-only, and it refreshes itself as sources sync. Each widget has an "ask" button that lets a viewer copy a one-sentence question to send to the dashboard owner; changing the dashboard is a re-pin by the owner.

Try it

Give your Claude Code a data backend in five minutes.

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