Marketing automation reporting: getting lifecycle data out of its silo
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:
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:
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:
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:
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:
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:
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: