Blog · Reporting automation

Automated marketing reports: build it once, never rebuild it

Published July 27, 2026 · 9 min read

Most "automated" marketing reporting automates the wrong link in the chain. It schedules an export, or it emails a PDF on the first of the month. Meanwhile the parts that actually cost you a day — reconciling channels that count conversions differently, noticing that Search Console hasn't finalized yet, working out why the number moved, and rewriting the same commentary in slightly different words — stay manual. This piece is about automating the whole chain, including the middle, and about what changes when the thing doing the analysis is your own AI agent rather than a scheduler.

The five links, and which ones you've actually automated

Every recurring marketing report is the same pipeline:

  1. Collect — pull each source: Search Console, GA4, Meta Ads, Google Ads, plus whatever arrives as a CSV.
  2. Normalize — reconcile them so spend is spend, a click is a click, and a conversion means one thing across platforms.
  3. Gate — decide which days are complete enough to report. This is the link nobody automates and everybody gets wrong.
  4. Analyse — find what moved, and why it matters.
  5. Deliver — get it in front of the person who asked.

Scheduled exports automate link 1. Email automation automates link 5. Links 2 through 4 — the expensive ones — are where your Tuesday goes. Worse, automating only the ends creates a specific failure: a report that arrives reliably, on time, and quietly wrong, because nothing in the pipeline knew that Meta was still re-attributing last week's conversions.

Automation without a freshness gate is just faster wrongness. Search Console finalizes roughly two to three days late. Meta re-attributes conversions over a trailing window of about a week. Any automated report that treats "yesterday" as complete will publish a decline that isn't real — and then you'll spend the morning explaining it.

Automate link 2 by normalizing on ingest

The reason reconciliation never stays automated is that most setups do it at report time — a blend here, a VLOOKUP there, a hand-tuned mapping in a BI tool. Every new campaign naming convention breaks it.

TableBI moves that work to ingest. Connect each source once, and everything lands in a unified facts table with shared dimensions and shared metric definitions, plus the lossless per-platform raw tables underneath for when you need native detail:

terminal
# install once, and plant the skill so your agent knows the CLI exists
npm i -g @tablebi/cli
tablebi login
tablebi install

# one workspace per client or brand; OAuth opens in the browser
tablebi connect gsc --site sc-domain:acme.com -w acme
tablebi connect ga4        -w acme
tablebi connect google_ads -w acme
tablebi connect meta_ads   -w acme

After that, the rate metrics are defined exactly once, as macros over sums — roas, cpa, cpc, cpm, ctr, cvr, aov. That detail sounds pedantic until you realize it's what makes the report reproducible: an automated pipeline that re-derives CPA per sheet will eventually average a column of averages and publish a number nobody can trace. Run tablebi schema -w acme --json to see the full contract your agent works against.

Refreshes are managed for you — sources sync on a schedule rather than because you remembered, and a pinned dashboard re-queries live data whenever it's opened. You can also force a pull whenever you need one:

terminal
# incremental by default; waits for completion unless you pass --no-wait
tablebi sync google_ads -w acme

Automate link 3: make the report refuse to lie

This is the step that turns automation from a liability into an asset. Before any recurring report gets written, check whether the data is actually ready:

terminal
# anything stale or unconnected, per source
tablebi pending -w acme --json
tablebi → agent
{
  "asOf": "2026-07-27",
  "overall": "fresh",
  "sites": [
    { "platform": "search_console", "dataThrough": "2026-07-25",
      "ageDays": 2, "stale": false }
  ]
}

Two facts fall out of that payload that a scheduler could never supply: the report should end at July 25, not July 27, and no source needs a human. Every --json response from any command carries the same kind of trust block — per-source freshness, the lineage behind each metric, and caveats that stop an agent drawing a confident wrong conclusion (for instance, that Search Console clicks and GA4 sessions are different populations and were never supposed to match).

Give your agent a standing instruction — rehydrate, gate on freshness, then report — and the pipeline becomes self-checking:

claude code → tablebi
# one call: definitions, sources, freshness, existing dashboards
tablebi context -w acme --json

Automate link 4 without letting a model invent numbers

Here's the distinction that matters more than any feature: the numbers should be computed, the narrative should be generated. Tools that pipe your raw data into a hosted LLM and ask it for "insights" blur those, and you end up unable to say where a figure came from.

TableBI hosts no model. Figures come out of a deterministic SQL engine; the interpretation happens in your own Claude Code session, under your own key, at $0 inference cost to the backend. A month-over-month comparison is a query, not a prompt:

claude code → tablebi
# this period vs the one before it, per channel
tablebi ask "SELECT channel,
             SUM(CASE WHEN date >= (SELECT MAX(date) FROM facts) - 29
                      THEN cost END) AS spend_now,
             SUM(CASE WHEN date <  (SELECT MAX(date) FROM facts) - 29
                      THEN cost END) AS spend_prev,
             roas(SUM(conversion_value), SUM(cost)) AS roas
             FROM facts
             WHERE date >= (SELECT MAX(date) FROM facts) - 59
             GROUP BY channel ORDER BY spend_now DESC" -w acme --json

Then the agent does what agents are good at: reading that JSON, noticing that one channel's spend rose while its ROAS fell, checking the raw altitude to see whether it was one campaign or all of them, and writing two paragraphs a client can act on. The commentary is automated. The arithmetic never was.

Automate link 5 by deleting the deliverable

The fastest way to automate sending a report is to stop sending one. Pin the analysis instead — a public read-only URL that stores the query rather than a snapshot, so it's current every time the client opens it:

claude code → tablebi
tablebi pin --title "Acme · monthly marketing" \
  --widget "Spend by channel (30d)::line=SELECT date, SUM(cost) AS spend FROM facts …" \
  --widget "ROAS by channel=SELECT channel, roas(SUM(conversion_value), SUM(cost)) AS roas FROM facts …" \
  --widget "Organic clicks (90d)::line=SELECT date, SUM(clicks) AS clicks FROM search_console_raw …" \
  -w acme
✓ published → https://dk.tablebi.com/d/dsh_…  (public, read-only, self-refreshing)

Here's a real one, pinned exactly this way — live data, not a mockup:

The periodic narrative still has value — clients want to be told what happened, not handed a chart and wished luck. But it belongs on the live board rather than in a separate artifact that immediately drifts out of sync with it. tablebi dashboard annotate attaches your agent's written summary to the dashboard, so the "why" travels with the numbers. dashboard list shows every board and its publish state; set-spec edits the widgets when the client's questions change.

What this looks like across many clients

Reporting automation usually dies on economics rather than technology: the tooling works fine, but a fourth client means a fourth seat, and suddenly the retainer maths stops working. The workspace model sidesteps that — one workspace per client, the same routine run with a different -w, and published dashboards that viewers don't need licences to open.

Practically, the recurring job your agent runs looks like this, per workspace:

  1. tablebi context -w <client> --json — rehydrate definitions, sources, freshness.
  2. tablebi pending -w <client> --json — stop and flag if anything is stale or disconnected.
  3. Two or three tablebi ask queries for the period comparison and the outliers.
  4. Drop to a raw table for anything that looks odd, to find the cause rather than guess it.
  5. tablebi dashboard annotate with the written summary — and only escalate to a human when the gate trips.

Nothing there is a new product to buy; it's a routine your agent already knows how to run once the skill is planted. If you'd rather see the multi-client angle in full, that's the agency reporting tool guide. For starting widget sets you can copy, see marketing report templates you can pin as live dashboards, and for the paid-media slice specifically, PPC reporting across Google Ads and Meta. The general setup lives in Claude Code for marketing.

FAQ

What does automating a marketing report actually involve?

Five links: collect, normalize, gate on freshness, analyse, deliver. Most reporting automation covers collection and delivery only, which leaves the slow, error-prone middle to you — and produces reports that arrive on time and are quietly wrong.

Can AI write my marketing report?

The narrative, yes — the numbers shouldn't come from a language model. Figures come from a deterministic SQL engine and your own Claude Code writes the interpretation on top, so the commentary is generated while the numbers stay reproducible and traceable.

How do I stop an automated report from publishing wrong numbers?

Gate on freshness. Search Console finalizes two to three days late and Meta re-attributes conversions over a trailing week, so treating yesterday as complete guarantees a fake decline. tablebi pending lists anything stale, and every query returns a freshness block.

Is a live dashboard better than a scheduled PDF?

Usually. A pinned dashboard stores the query, not a snapshot, so it re-runs against live data on every open and never goes stale between sends. Keep the written summary — attach it to the board rather than making it the only place the numbers live.

Does this scale to many clients?

It scales when per-client cost is a workspace rather than a seat. One workspace per client keeps sources and dashboards separate, the same routine runs against each with a different -w flag, and dashboard viewers don't need licences.

Try it

Automate the whole chain, not just the export.

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