
Every operator eventually asks the same shape of question: totals and averages broken down by category. Revenue by region. Open tickets by status. Units on hand by warehouse. Answering it used to mean exporting records to a spreadsheet and building the pivot table by hand, or filing a request with whoever owns the BI tooling — a rollup that’s stale the moment someone asks a follow-up question. A new dashboard block computes it directly against live records instead: pick a field to group by, pick a reduce, and the number updates as the underlying data does, no export and no separate tool in the loop.
What a rollup gets you that a table doesn’t
- The number, not the rows. A stat tile or table shows records one at a time; a rollup answers “how many” and “how much” per category directly, which is the question a manager or a dashboard actually needs answered.
- Live, not exported. The same block that showed last week’s numbers shows this week’s automatically — there’s no CSV to regenerate and no pivot table to rebuild by hand.
- The same permissions as everywhere else. A manager and a rep looking at what looks like “the same” dashboard each see totals computed only from the records their role can read. Nobody maintains a second, restricted copy of the report.
- Composable with the same filters as any other view. “Revenue by region, this quarter, Enterprise tier only” is the rollup plus the same filter clauses that already narrow a search or an export — nothing new to learn to scope it down.
The aggregate endpoint
Grouping and reducing entities is one call: narrow the set with the same filter_groups grammar every search already uses, group by up to three fields, and compute up to ten reduces per group — count, sum, avg, min, or max.
curl -X POST "https://api.omnismith.io/v1/entities/aggregate/deal" \
-H "Authorization: Bearer omni_live_secret_key_..." \
-H "X-Omnismith-Project-Id: $PROJECT_ID" \
-H "Content-Type: application/json" \
-d '{
"filter_groups": [[{ "field": "status", "operator": "eq", "value": "018b2f1b-…-open" }]],
"group_by": ["region"],
"aggregations": [
{ "op": "count" },
{ "op": "sum", "field": "deal_value" }
],
"limit": 50
}'
{
"data": [
{
"key": [{ "field": "region", "value": "018b2f1b-…-emea", "custom_value": "EMEA" }],
"aggregates": [
{ "op": "count", "field": null, "value": 42 },
{ "op": "sum", "field": "deal_value", "value": 318400.5 }
]
}
],
"limit": 50,
"truncated": false
}
count takes no field; sum and avg need a number attribute; min and max accept number, date, or datetime attributes. A group’s key reports both the stored value (a list item or reference id, ready to filter on again) and its label — the value your code compares against, and the label your UI shows, in the same response.
This is a different kind of reduce from the one already available on a single record’s own metric readings — averaging a temperature sensor’s last hour of observations is a time-series rollup on one entity. Grouping and reducing across many entities at once, by a shared field, is what this endpoint adds.
The Aggregate dashboard block
aggregate joins stat, chart, gauge, and list as a dashboard block type — the same grid canvas, the same per-block filters and layout, backed by the same endpoint above. Configuring one is the same group_by / aggregations shape as the API call, saved once and resolved live every time the dashboard loads.
The same access rules as every other view
Every block type — including aggregate — resolves through the same row-scope check a direct search already goes through: the caller’s role needs view access to the block’s template, and any row-level access scope on that template narrows exactly which records the block is allowed to count, sum, or list. A rollup can’t become a side channel for seeing totals over records a table view on the same template would have hidden — the dashboard and the underlying data share one access model, not two.
The reduce itself isn’t new — count, sum, average, min, and max are the obvious ones. What’s new is having them live on the dashboard canvas, computed directly from the same records and the same permissions as every other block, instead of behind a separate export step that’s already stale by the time it lands in someone’s inbox.