Time grains and asymmetric aggregates
Time grain is part of the metric contract. Some aggregates roll cleanly across grains. Others do not, and agents need to know the difference.
The datahashi team3 min read

Time is where metric definitions quietly break. Daily active users are not the sum of hourly active users. A monthly average is not twelve times a weekly average. Agents that treat every metric as freely rollable will invent confident wrong answers. Time grain belongs in the contract.
Grain is not decoration
A metric without a declared grain is incomplete. “Revenue” might be valid at day, week, and month. “Active users” might be valid at day and month but not by summing days into a month. The semantic layer must say which grains are supported and how aggregation behaves when the caller asks for a coarser window.
That is what we mean by asymmetric aggregates. Addition works for some measures. Distinct counts, ratios, and unique-user style metrics do not compose the same way. The layer should prevent callers from assuming symmetry that does not exist.
What agents get wrong
Language models often default to sum and average. Faced with daily series, they will sum to “monthly” without checking whether the metric allows it. If your tools accept free-form SQL, nothing stops that mistake. If your tools require a metric name and a declared grain, the contract can reject invalid combinations.
This is not about being pedantic. It is about protecting the same-number test across time windows. Two agents that both claim “monthly active users” must mean the same thing, including how the month is bounded and how uniqueness is computed.
Encoding grain in the request
A practical request shape includes the metric, the time range, and the grain. Filters stay orthogonal when possible. The engine resolves the metric at the requested grain according to the contract. Callers do not pick arbitrary group-by expressions against raw event timestamps unless the metric allows it.
In datahashi, agents inspect grain through describe_metric and request it through run_semantic_query. list_catalog helps them find metrics before they commit to a plan. Metering exists so you can see which grains are actually used. Rollups have shipped, including sketches for approximate distinct counts and percentiles. There is still no p99 latency promise.
Design rules that help
Prefer explicit grains on every metric. Document whether values can be summed across periods. For unique-user and ratio metrics, state the computation at each supported grain. Avoid publishing a daily unique metric and silently treating monthly as a sum of dailies.
When a new grain is needed, add it deliberately. Do not let agents invent a grain by rewriting SQL. If month-to-date is a common ask, define how month-to-date is computed and expose it as part of the contract or as a related metric.
Testing across grains
Extend the same-number test across time. Compare day and month results where the relationship is known. For additive metrics, check that days in a month reconcile to the monthly total. For non-additive metrics, check that the monthly value is computed from the base grain correctly, not by illegal composition.
Keep time boring and explicit
Time logic is easy to romanticize and hard to maintain. Put grain in the contract. Make invalid grain requests fail loudly. Teach agents to read describe_metric before they invent calendar math. Your warehouse can stay flexible. Your published metrics should not.
Asymmetric aggregates are not an edge case. They are the normal state of product analytics. A semantic layer that ignores them will look fine in demos and fail in production agent runs.
Product notes
An occasional email when we ship something or write something worth reading.