Blog · Reporting

Marketing automation reporting: getting lifecycle data out of its silo

Published August 31, 2026 · 8 min read

Your automation platform has excellent reporting. That's the problem. It will show you open rate by segment, click-through by send hour, a funnel diagram of the onboarding sequence, and a cohort chart that genuinely tells you something. What it will never show you is what you paid to get those addresses — because it has no idea. Every automation tool reports the world it can see, and it can only see itself. This piece is about the join that makes lifecycle numbers comparable to everything else you spend money on.

What the platform structurally can't answer

Three questions come up in every quarterly review, and no automation platform can answer any of them from inside its own reporting:

  • "Was this segment worth acquiring?" The platform knows the segment converts at 4%. It doesn't know one half of it came from a $9 CPA search campaign and the other half from a $60 CPA prospecting audience, which is the entire difference between a good programme and a bad one.
  • "Did the nurture sequence do anything, or did paid just get busier?" Sequence performance rose 20% in a month when paid also raised budget 40%. Inside the automation tool those two facts live in different universes.
  • "What's our actual cost per customer?" Ads report spend. Automation reports conversions. Neither reports the ratio, and the spreadsheet where someone reconciles them by hand each month is where the number quietly becomes fiction.

The instinct is to fix this by buying an attribution product. That's a much bigger, much more expensive claim than the one you actually need, and it will be wrong in ways you can't audit. The smaller move — putting both sides on one spine and looking at them together — answers most of the real question and doesn't require believing anything you can't check.

The spine: date × platform × campaign

Everything hinges on picking the grain correctly, and it's easy to pick wrong in both directions.

Too fine is the common mistake. Automation platforms think in contacts and events — this person opened this send at this timestamp. Ad platforms never report individuals; they report aggregates by campaign and day, because that's all the privacy model permits. Export an event log and there is simply nothing on the ads side to join it to.

Too coarse — monthly totals per channel — reconciles fine and tells you nothing actionable, because you can't isolate a sequence or a campaign.

The grain that works is date by platform by campaign. It's the finest resolution both sides genuinely report, which makes it the only honest place to join them. Practically: export your automation platform's campaign performance report, not its contact activity export, and make sure it's aggregated per campaign per day before it leaves the tool.

Rule of thumb: if a column in your export could identify one person, you've exported the wrong report. The join you want lives one altitude above that.

Getting it in

Search Console, GA4, Google Ads and Meta Ads connect over OAuth and refresh themselves. Automation platforms — HubSpot, Klaviyo, Mailchimp, Customer.io, whichever — arrive as an export, and land in the same tables:

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

# the acquisition side, connected once
tablebi connect google_ads
tablebi connect meta_ads
tablebi connect ga4
tablebi connect gsc --site sc-domain:example.com

# the lifecycle side, exported and labelled
tablebi connect csv --file lifecycle-aug.csv --platform lifecycle

That --platform lifecycle tag is doing the structural work. It's what puts those rows into the shared facts table alongside the ad platforms, under the same column names, so that clicks means clicks and cost means cost regardless of which tool reported the row. Confirm how the file was interpreted before you trust a single number off it:

terminal
tablebi sources          # what's connected, and how fresh each source is
tablebi schema           # dimensions, measures, definition macros
tablebi sample           # actual normalized rows from the CSV
tablebi values --dimension campaign   # check campaign names line up across platforms

That last check is the one people skip and regret. If your ads platform calls it Q3-onboarding and your automation platform calls it Q3 Onboarding Sequence, no join is going to save you — fix the naming at the source before building anything on top.

The questions, as queries

With both sides on one spine, the reconciliation that used to be a monthly spreadsheet ritual becomes a query. Total picture first:

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

Then the one the automation platform can't do — acquisition cost and lifecycle outcome side by side on the same campaign:

claude code → tablebi
tablebi ask "SELECT campaign,
             SUM(CASE WHEN platform != 'lifecycle' THEN cost END) AS acq_spend,
             SUM(CASE WHEN platform =  'lifecycle' THEN conversions END) AS lifecycle_conv
             FROM facts
             WHERE date >= (SELECT MAX(date) FROM facts) - 90
             GROUP BY campaign
             HAVING SUM(CASE WHEN platform != 'lifecycle' THEN cost END) > 0
             ORDER BY acq_spend DESC LIMIT 25"

And for a quick read without writing SQL at all, the definition layer answers directly:

terminal
tablebi metrics --metric cpa --group-by campaign --period 2026-06-01:2026-08-31

Every answer arrives with a trust block: how fresh each source is, and the caveats that stop an agent from over-reading the result — that Search Console lags a few days and anonymizes rare queries, that GSC clicks and GA4 sessions are different populations. A lifecycle export you last refreshed three weeks ago is reported as three weeks old, rather than silently dragging the last three weeks of every chart toward zero.

Making it stop being a monthly task

The point of the exercise is that you do it once. Pin the queries and the report maintains itself for the sources that refresh on their own:

claude code → tablebi
tablebi pin --title "Acquisition + lifecycle" \
  --widget "Spend by platform (30d)=SELECT platform, SUM(cost) AS spend FROM facts …" \
  --widget "Daily conversions::line=SELECT date, SUM(conversions) AS conv FROM facts …"
✓ published → https://dk.tablebi.com/d/dsh_…  (public, read-only, self-refreshing)

Here's a live one, pinned exactly that way — real Search Console data across a portfolio of sites, refreshing as its sources sync:

What this does not tell you

Worth stating plainly, because this is exactly where reporting tools oversell:

  • It is not attribution. Putting lifecycle and paid on one spine gives you an honest denominator — total spend against total outcome over a window. It does not establish that a particular email caused a particular purchase, and no amount of SQL over aggregate data can.
  • Campaign-level joins are approximate. A contact acquired in March who converts through an August sequence appears in two periods on two platforms. The 90-day window above blunts that; it doesn't remove it.
  • The lifecycle source is only as fresh as your last export, until the platform is supported over OAuth. The freshness block makes that visible rather than solving it.

Those limits are the price of a number you can actually audit. A blended figure you understand the seams of beats a per-touch attribution model you can't inspect — we made that argument at length in blended ROAS: one honest number across Google Ads, Meta and GA4.

If your problem is more the monthly grind than the blend itself, automated marketing reports covers building it once and not rebuilding it. The mechanics of the CSV route are in CSV analysis with Claude Code, and choosing which numbers deserve a slot at all is the marketing KPI dashboard piece.

FAQ

Why can't my automation platform report across channels?

Because it only observes itself. It knows every send, open and click in a sequence, and has no idea what was paid to acquire the address. Its reporting is complete for its own funnel and structurally blind to the acquisition side — which is where most of the money went.

How do I get automation data into a cross-channel report?

Export the campaign-level performance report as CSV and load it with a platform label: tablebi connect csv --file lifecycle-aug.csv --platform lifecycle. Those rows land in the same table as Search Console, GA4, Meta Ads and Google Ads, joinable on a shared date and campaign spine.

What granularity should I export?

Date by platform by campaign — the finest grain both sides genuinely report. Contact-level event logs have nothing to join to, because ad platforms never report individuals. Export the campaign performance report, not the contact activity export.

Does this give me true email ROI?

No. It gives you an honest denominator — spend against outcome over a period — not per-touch causal attribution. It answers "is the blended picture healthy" and "did this sequence coincide with a change", not "this email caused that purchase". Anything claiming the latter from aggregate data is overselling.

Try it

Put lifecycle and acquisition on the same spine.

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