What Tempio's numbers are and are not
Base counts are stored and ratios are computed at read time, a zero denominator gives null rather than nought, every result states the scope it covers, and the warnings arrive before the numbers. This note is how to read a Tempio metrics answer honestly.
Last reviewed 2026-09-13.
Tempio stores counts and computes ratios. Everything in this note follows from that one choice, and an agent that reports a Tempio number without it will eventually say something false and cheerful.
What is stored
ad_metrics_daily holds one row per entity per day. The grain is the
workspace, the connection, the platform, the ad account, the entity level, the
entity, the date and the breakdown — so a campaign's Tuesday is exactly one
row, and re-syncing Tuesday rewrites that row rather than adding a second.
The columns are spend, impressions, reach, clicks, conversions and
conversionValue, with currency stamped beside them. Money is integer minor
units. There is no stored CTR, no stored ROAS, no stored blended anything, and
storing one would let it drift from the counts beneath it.
Reach is the column that is not a plain count: platforms report it de-duplicated, so one person who saw an ad on Monday and again on Tuesday is one person across the week and two daily rows. Adding reach over a range adds daily reach, and saying so is part of quoting it. Where a platform does not report reach the column is empty rather than zero — "did not report" and "reached nobody" are different facts.
Measurement
Five ratios are worked out when a page is drawn or a tool answers: CTR is clicks ÷ impressions, CPC is spend ÷ clicks, CPM is (spend ÷ impressions) × 1000, CPA is spend ÷ conversions, and ROAS is conversion value ÷ spend. The same function computes them everywhere, so the dashboard tile, the campaign table, the account brief and the MCP result cannot disagree about what CPA means.
A zero denominator gives null, never zero. An account with no conversions
yet has no cost per conversion. The value is null, it renders as an em dash,
and a result carrying any of them also carries undefined_ratio_present, whose
sentence is the one to repeat: null is undefined, never nought. Reporting a
null CPA as "$0" claims conversions were free.
Levels are uniform. account, campaign, ad_group and ad mean the same
thing on every platform, so a cross-platform comparison at one level is a
comparison of like things — that normalisation is the point of the mirror.
The envelope, and warnings first
Every tool result is { data, report?, warnings }. warnings is always
present and is [] when there is nothing wrong, because a consumer that has to
test whether a field exists learns nothing from its absence. report states,
once and authoritatively, what the scope covers: the currency (null when the
scope spans several), the IANA time zone the days are cut in, the date range,
the level, and whether the rows are all of them or only those returned.
State a degraded warning before the number it travels with, not after it.
A blocking warning means the value it names is unavailable — do not infer it,
do not substitute a neighbouring figure, and do not retry the call hoping for a
different answer; say what is blocked and stop. An info warning is context a
careful answer mentions once.
Today is always partly counted. A range ending today includes a day the
platform has not finished filling in, which is what today_incomplete says;
comparisons default to complete days for that reason.
Money across currencies
Minor units are not comparable across currencies: ¥1,500,000 has more digits than $12,000 and is less money. Tempio handles that in one direction only — it says so.
With one currency in scope nothing is converted, the answer is exact, and it is
stated in that currency. With several, totals are converted through published
rates into whichever currency carries the most spend — compared in USD rather
than by digit count — and the result is labelled an estimate carrying the date
of the oldest rate it leaned on, so a stale leg cannot hide behind a fresh
one. That label is fx_estimate_applied, and a converted figure quoted without
it is a converted figure passed off as exact.
With several currencies and a missing rate, nothing is invented. A blended
total that cannot be reached is refused rather than estimated, and a list that
cannot be ranked comparably comes back in raw order with ranking_raw
attached: a raw order across currencies is not a ranking, and the top row is not
the biggest spender.
Rates feed a labelled estimate and nothing else. No guardrail converts, no budget converts, and no cap is compared across currencies.
Sources
- ratios, safeRatio, the FX blend and its receipt, rankByComparableSpend —
repo://apps/app/lib/ads/metrics.service.ts(read 2026-09-13) - ToolEnvelope and ToolReport — the envelope every tool result is wrapped in —
repo://apps/app/lib/ads/tools/run.ts(read 2026-09-13) - WARNING_REGISTRY: today_incomplete, undefined_ratio_present, fx_estimate_applied —
repo://apps/app/lib/ads/warnings.ts(read 2026-09-13) - ad_metrics_daily and its grain —
repo://packages/db/schema.ts(read 2026-09-13) - Metrics — the product page for the same rules —
repo://apps/docs/content/docs/metrics/index.mdx(read 2026-09-13)