Templates · Queries, not files

Client reporting template

What belongs in a monthly client report — and why a link beats an attachment.

Updated September 5, 2026

A monthly client report has one job: let the client decide whether to keep spending, without a call. That takes about six widgets and one paragraph of narrative, and most templates ship twenty.

The structure below is the six, with the query for each. The delivery is the part that differs from every other template on this page: instead of generating a PDF each month, you pin the set once and the client gets a read-only URL that refreshes itself. No login for them, no rebuild for you.

If what you want is a file to download and hand over — a Looker Studio copy, a Google Sheets tab, a branded PDF — the vendors ranking on this query have those and TableBI does not. This page is the other kind of template: the queries, and a URL that keeps answering them.

Connect the source

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

tablebi connect gsc --site sc-domain:client.com
tablebi connect ga4 --property 123456789
tablebi connect google_ads --account 1234567890

Source for this template: whatever the client has connected — Search Console, GA4, Google Ads, Meta. One connected source and one live dashboard fit inside the free tier.

The widgets

1. Headline

Spend, revenue and blended efficiency for the month, against last month.

tablebi ask
WITH w AS (SELECT MAX(date) AS anchor FROM metrics)
SELECT SUM(cost) AS spend,
       SUM(revenue) AS revenue,
       roas(SUM(revenue), SUM(cost)) AS roas
FROM metrics, w
WHERE date > date_trunc('month', w.anchor) - INTERVAL '1 month'
GROUP BY date_trunc('month', date)
ORDER BY 1

2. Channel contribution

Where the money went and what each channel returned.

tablebi ask
WITH w AS (SELECT MAX(date) AS anchor FROM metrics)
SELECT platform, SUM(cost) AS spend,
       roas(SUM(revenue), SUM(cost)) AS roas,
       SUM(conversions) AS conversions
FROM metrics, w
WHERE date > w.anchor - 30
GROUP BY platform ORDER BY spend DESC

3. Organic trend

Whether the non-paid side is compounding or flat.

tablebi ask
WITH w AS (SELECT MAX(date) AS anchor FROM search_console_raw WHERE site = 'client.com')
SELECT date_trunc('week', date) AS week,
       SUM(clicks) AS organic_clicks,
       SUM(impressions) AS impressions
FROM search_console_raw, w
WHERE site = 'client.com' AND date > w.anchor - 90
GROUP BY 1 ORDER BY 1

4. Site behaviour

Did the traffic do anything once it arrived?

tablebi ask
WITH w AS (SELECT MAX(date) AS anchor FROM ga4_raw)
SELECT channel,
       SUM(sessions) AS sessions,
       SUM(conversions) AS conversions,
       SUM(revenue) AS revenue
FROM ga4_raw, w
WHERE date > w.anchor - 30
GROUP BY channel ORDER BY sessions DESC

5. What moved

The three biggest changes against the previous period, in either direction.

tablebi ask
WITH w AS (SELECT MAX(date) AS anchor FROM metrics)
SELECT platform, campaign,
       SUM(cost) FILTER (WHERE date > w.anchor - 30) AS spend_now,
       SUM(cost) FILTER (WHERE date <= w.anchor - 30) AS spend_prev,
       SUM(revenue) FILTER (WHERE date > w.anchor - 30) AS rev_now,
       SUM(revenue) FILTER (WHERE date <= w.anchor - 30) AS rev_prev
FROM metrics, w
WHERE date > w.anchor - 60
GROUP BY platform, campaign
ORDER BY ABS(COALESCE(rev_now,0) - COALESCE(rev_prev,0)) DESC
LIMIT 10

6. Next month

Not a query — one paragraph you write, naming what changes and why.

The widget that is not a query

The last row above has no SQL because it should not have any. A client report without a written recommendation is a data dump, and the client will ask for the recommendation on a call anyway — which is the call the report was supposed to replace.

Three sentences is enough: what changed, what you are doing about it, what you need from them. Write it after reading the six widgets, not before.

Put the caveats in the report, not in the follow-up email

Clients notice discrepancies, and every multi-source report has them. Three are guaranteed:

  • Search Console clicks are not GA4 sessions. Different populations, different deduplication. They are not supposed to match.
  • Per-query Search Console numbers sum to less than the total, because rare queries are anonymised under a privacy threshold.
  • Platform-reported revenue overlaps. Summing it exceeds real revenue whenever a buyer touched two channels.

Stating these in the report costs three lines and prevents the email that costs an hour. Every TableBI answer carries them attached to the number they apply to, so they are not something you have to remember to write.

A PDF is a snapshot of a moment nobody chose, and it generates the "can you re-send with the latest numbers" request about a week later. tablebi pin publishes the widget set as a public read-only URL that re-runs its own queries, so the client looks whenever they want.

The honest limitation: that URL is on a tablebi.com subdomain and carries no branding of yours. There is no white-label option and no custom domain today. If the report has to look like it came from you, the reporting platforms are built for exactly that and this is not.

Pin the set as one live URL

terminal
tablebi pin --title "Client reporting template"
→ https://you.tablebi.com/d/dsh_…  # public, read-only, refreshes itself

The URL needs no login from whoever you send it to, and it re-runs its own queries rather than freezing a moment. Here is one running on real data. It carries no branding of yours — there is no white-label or custom-domain option today, which is the trade against a reporting platform.

Questions people ask

What should be in a monthly client report?

A headline with spend, revenue and blended efficiency against last month; channel contribution; an organic trend; what the traffic did after it arrived; the biggest changes in either direction; and one written paragraph naming what you will do next. Six widgets and a recommendation, not twenty charts.

How do I explain why Search Console and GA4 numbers do not match?

They measure different things. Search Console clicks count a result being clicked in search; GA4 sessions count behaviour after landing, with its own deduplication and attribution. They are different populations and should not be expected to agree — say so in the report rather than reconciling them.

Can I white-label a TableBI client dashboard?

No. A pinned dashboard is a public read-only URL on a tablebi.com subdomain with no custom branding or custom domain. If branded delivery matters, AgencyAnalytics, DashThis and Whatagraph all build their products around it.

Is a live dashboard better than a monthly PDF?

For most clients yes, because it removes the "can you re-send with current numbers" loop and the report is never stale. A PDF still wins where a fixed record of the month is contractually required, or where the reader will not open a link.

Want the arithmetic before the wiring? The free marketing calculators run these same formulas in the browser.