Your template looks right on a demo account with ten units. Then a customer with 120 trucks runs it over six months. The app refuses the PDF, the total row shows dashes, and a borrowed figure repeats down every month.
None of that is random. A template is a query plan. Its grouping sets the row count, and the row count decides which totals and exports the app accepts.
Calculators turn device messages into stored intervals, such as trips, parkings and sensor sessions, plus daily aggregates. A template reads those figures; it never changes them. How report data works covers the computing side.
Three rules shape everything below. Nothing is cut silently: a result is complete, visibly summarised, or refused with the limit and the actual number. The grid re-runs the query for each page, so viewing a large report costs one page. The total row and every export cover the whole report, not the page on screen.
A template is a JSON document: a name, one or more tables and optional charts. Each table reads one source and compiles to one query. The builder writes the JSON for you, and you see it when you export a template as JSON.
The table keys that decide scale:
| Key | What it decides |
|---|---|
source |
Which intervals the table reads, for example interval:trip |
group_by |
One row per what: unit, driver, day, month and others, depending on the source. Several keys nest. Empty means one row per record. |
columns[].agg |
How a column folds: sum, avg, min, max, count, first or last. Required when the table groups, refused when it does not. |
totals |
The footer row: enabled, plus optional per-column ops |
merges |
Columns borrowed from another table in the same template |
Charts, sorts, filters and totals address a column by its result alias, not its catalogue key. The alias is the aggregate, an underscore, then the key with dots replaced: a summed metrics.distance_km is sum_metrics_distance_km.
Harbor Fresh Foods runs 120 refrigerated trucks with fuel sensors. Management wants one row per truck per month. Each row shows trips, distance, driving time, top speed, opening and closing fuel, and time parked.
The saved definition reads like this:
{
"name": "Monthly utilisation",
"tables": [
{
"id": "trips",
"source": "interval:trip",
"group_by": ["unit", "month"],
"columns": [
{ "key": "unit_id", "resolve": "unit_name" },
{ "key": "duration_s", "agg": "count", "label": "Trips" },
{ "key": "metrics.distance_km", "agg": "sum", "uom": "distance" },
{ "key": "duration_s", "agg": "sum", "uom": "duration" },
{ "key": "metrics.max_speed", "agg": "max", "uom": "speed" },
{ "key": "metrics.fuel_begin", "agg": "first", "uom": "volume" },
{ "key": "metrics.fuel_end", "agg": "last", "uom": "volume" }
],
"merges": [
{ "table_id": "parked", "on": "unit_id", "period": "month", "cardinality": "aggregate" }
],
"totals": { "enabled": true }
},
{
"id": "parked",
"source": "interval:parking",
"group_by": ["unit", "month"],
"columns": [
{ "key": "unit_id" },
{ "key": "duration_s", "agg": "sum", "uom": "duration" }
]
}
],
"charts": [
{ "id": "km_by_month", "table": "trips", "type": "bar", "x": "month", "y": ["sum_metrics_distance_km"] }
]
}
Do the row arithmetic before you publish. Six months of this template is at most 720 rows. The same months as a per-trip listing, at about 12 trips a truck a day, is roughly 260,000 rows. That pages fine on screen but is too large for a PDF, and the Generate panel says so before you run: "too large for PDF".
Publishing to the whole account needs the Owner role. Where a shipped template already fits, start the customer from it; the report template catalogue lists them.
A grouped table shows groups as Nested by default, with subtotals on each parent row. Flat repeats the group values on every row. Above 5,000 groups at one level, the grid falls back to flat rows.
The total row comes from its own query over the whole report. Each column's total defaults to the column's own aggregate. Ids, names and timestamps get no total and show a dash. Every figure that is not a sum carries a small marker, such as avg or max, and its tooltip names the population.
The population matters. When the total's operation matches the column's aggregate, the app computes it over the records. Average speed then covers every trip, not 720 monthly averages. A different operation, set in ops, runs over the group rows instead, under the same 5,000-group bound. "ops": { "sum_metrics_distance_km": "avg" } gives the average truck-month's distance.
Begin and End take the value of the earliest or latest record, ordered by each interval's start, so every run agrees. The worked template's fuel columns fold with first and last, so their totals default to Begin and End. Each is one truck's reading from the earliest or latest trip; if that misleads, set it to No total. For other columns, choose Begin or End yourself. Earlier design notes kept both off everywhere; the code now defaults them for first and last columns.
A grouped table filtered on its own aggregate, for example trucks over 1,000 km a month, shows a dash for Begin and End. Its records still include the rejected groups.
Each limit is a deployment setting; these are the shipped defaults. The app checks the row estimate before any work starts and refuses rather than truncates.
| Limit | Default | Past the limit |
|---|---|---|
| Grid page | 50 rows, up to 500 | The app clamps larger requests and shrinks a page to fit a 512 KB reply |
| Range, a table with no grouping | 184 days | Run and export refused; group by day or narrow the range |
| Range, grouped tables only | 366 days | Run and export refused |
| 20,000 rows | Refused before the query runs | |
| Excel | 300,000 rows | Refused, with a suggestion to use CSV |
| CSV | 5,000,000 rows, one table per file | Refused. The report engine also caps one table query at 1,000,000 rows. |
| Excel sheets | 255 | Excel (a sheet per unit) refused when it needs more sheets |
| Chart | 400 points a series, 20 series | Bucketed by time, with a minimum-maximum band per bucket |
| Exports | 2 running per user; 20,000,000 rows per account per 24 hours | Refused until one finishes or the window moves |
| Addresses | 5,000 geocoder lookups per export | Address cells stay blank past the budget; geofence and cached answers are free |
Scheduled reports follow the same size caps and account budget, without the per-user cap. Above 15 MB of files, the email carries download links instead, valid for 7 days. See scheduled reports for delivery settings.