Aggregate
Description
Run a grouped aggregation query over records of a single record type — counts, sums, averages, and more, optionally grouped by up to two fields — without retrieving individual records.
This endpoint supersedes the v1 Query record endpoint. Prefer it over paging through the Search records API and aggregating client-side: it is faster.
Despite the POST verb, Aggregate is a read — no records are created. The query travels in the request body.
To discover the record_type_id and field IDs to reference, use the List record types API.
Request
HTTP Method: POST
URL: https://<tenant-domain>/api/v2/records/aggregate
The request body is a JSON object with the following fields:
| Body parameter | Required | Description |
|---|---|---|
| record_type_id | Yes* | Numeric record type ID. Provide this or record_type_name. |
| record_type_name | Yes* | Record type name or slug. An alternative to record_type_id; if the name matches multiple types, use record_type_id. |
| aggregations | Yes | An array of 1–5 aggregation functions to compute (see Aggregations). |
| group_by | No | An array of 0–2 field references to group by (see Group-by). When omitted, you get a single overall row. |
| filter | No | A filter tree that narrows records before aggregation (see Filters). |
| time_range | No | An array of up to two timestamp windows restricting which records are aggregated (see Time range). |
| order_by | No | Sort the output rows (see Order-by). |
| parent_ids | No | An array of parent record IDs (max 100). Restricts the query to first-degree children of those records. |
| timezone | No | IANA timezone name applied to date_trunc buckets and time-range parsing. Defaults to UTC. |
| limit | No | Maximum rows to return, between 1 and 500. When omitted, the server returns up to 1000 rows and sets meta.truncated if more matched. |
*Provide at least one of record_type_id or record_type_name. If you send both, record_type_id takes precedence and record_type_name is ignored. If record_type_name matches more than one record type, the request fails with a 400; supply record_type_id to disambiguate.
Field identification
Every field reference in aggregations, group_by, filter, and order_by accepts one of field_id (integer, stable across renames — recommended for automation) or field_name (the exact, case-sensitive field name or canonical snake-case slug). COUNT aggregations may omit both; when a field is supplied to COUNT, the function still counts rows rather than only non-null values in that field.
Aggregations
Each entry in aggregations has:
| Key | Required | Description |
|---|---|---|
| fn | Yes | The aggregation function: COUNT, SUM, AVG, MIN, MAX, or MEDIAN. |
| field_id or field_name | Depends | Required for all functions except COUNT. SUM, AVG, MIN, MAX, and MEDIAN only accept NUMBER fields. |
| alias | No | A name for this value in the response. Auto-generated if omitted (for example count for a bare COUNT). |
At most five aggregations per query.
Group-by
Each entry in group_by has:
| Key | Required | Description |
|---|---|---|
| field_id or field_name | Yes | The field to group by. |
| date_trunc | No | For TIMESTAMP fields only: hour, day, week, month, or year. Rolls values into calendar buckets in the query timezone (UTC by default). |
At most two group-by entries per query.
JSON and ARTIFACT fields cannot be used in group_by.
Filters
The optional filter field narrows records before they are aggregated. It is a filter tree — either a single condition:
{ "field_name": "Status", "operator": "EQUAL", "value": "open" }
or an AND group of conditions:
{
"op": "AND",
"nodes": [
{ "field_name": "Status", "operator": "EQUAL", "value": "open" },
{ "field_id": 102, "operator": "IS_ANY_OF", "value": ["high", "critical"] }
]
}
Each condition takes a field reference (field_id or field_name), an operator, and — for most operators — a value. Only AND is supported. Groups may nest to a maximum depth of 4, with up to 50 total nodes and 50 filter conditions. The available operators depend on the field's type; see the Search records API for the full operator matrix.
Time range
The optional time_range restricts which records are aggregated by timestamp. It is an array of up to two windows, ANDed together, with at most one window per field. Each window is an object:
| Key | Description |
|---|---|
| field | Timestamp column to filter: created_at (default) or updated_at. |
| rolling_date_range | A rolling preset: TODAY, YESTERDAY, LAST_7_DAYS, LAST_31_DAYS, LAST_365_DAYS, or ALL_TIME (ALL_TIME applies no bound). |
| range_start | Inclusive ISO 8601 lower bound of a custom range. |
| range_end | Inclusive ISO 8601 upper bound of a custom range. |
Within a window, supply either a rolling_date_range preset or range_start/range_end bounds, not both, and range_start must not be after range_end — otherwise the request returns a 400. Equal bounds select that single instant. A second window on the same field also returns a 400.
Order-by
The order_by array sorts the output rows. It holds at most one entry, naming a group-by field (by field_id or field_name) or an aggregation alias. When omitted, grouped results are ordered by every group-by field in ascending order, with null values last.
| Key | Required | Description |
|---|---|---|
| field_id or field_name or alias | Yes | The column to sort by: a group-by field, or an aggregation alias. |
| direction | No | ASC or DESC (case-insensitive). Defaults to ASC. |
order_by shares its key shape and validation with the Search records API's sort: an invalid direction, a non-array, more than one key, or an unknown key member each return a 400 (unknown members carry a Did you mean? hint). Naming both a field and an alias in one key also returns a 400.
Example request
curl --proto '=https' --tlsv1.2 \
-X POST \
"https://<tenant-domain>/api/v2/records/aggregate" \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <<CREDENTIAL.tines_api_key>>' \
-d '{
"record_type_id": 42,
"aggregations": [
{ "fn": "COUNT", "alias": "total" }
],
"filter": {
"field_name": "Severity",
"operator": "IS_ANY_OF",
"value": ["high", "critical"]
},
"group_by": [
{ "field_name": "Status" }
],
"order_by": [
{ "alias": "total", "direction": "DESC" }
],
"limit": 100
}'
Response
A successful request returns a JSON object containing the aggregation rows and result meta.
Field description
| Field | Description |
|---|---|
| rows | Array of result rows, each a flat object (see below). |
| meta | Metadata about the query result. |
Each object in rows is a flat map of column names to values:
- Group-by columns are keyed by the field's name.
TIMESTAMPvalues bucketed withdate_truncare ISO 8601 strings whose offset reflects the querytimezone. - Aggregation columns are keyed by the
aliasyou specified (or the auto-generated name).COUNTvalues are integers. Numeric aggregates (SUM,AVG,MIN,MAX,MEDIAN) are returned as strings to preserve precision.
The meta object contains:
| Field | Description |
|---|---|
| row_count | The number of rows returned. |
| truncated | true only when limit was omitted and the query matched more than the 1000-row default cap. Always false when you supply limit — a supplied limit is applied silently, and Aggregate does not support pagination. Use order_by with limit for a top-N query, or narrow the query when you need every row. |
| columns.group_by | Array of column names in rows that come from the group_by fields. |
| columns.aggregations | Array of column names in rows that come from aggregations. |
Sample response
{
"rows": [
{
"Status": "open",
"total": 2
},
{
"Status": "closed",
"total": 1
}
],
"meta": {
"row_count": 2,
"truncated": false,
"columns": {
"group_by": ["Status"],
"aggregations": ["total"]
}
}
}
Errors
Errors are returned as a JSON object with an error field, along with an appropriate HTTP status code:
{
"error": {
"type": "bad_request",
"message": "aggregations is required and must be an array",
"field": "aggregations"
}
}
| Status | Meaning |
|---|---|
| 400 | Invalid query — malformed body, unknown parameter, invalid timezone, disallowed operator, or validation failure. |
| 403 | The tenant does not have access to records. |
| 404 | Record type not found, or the calling token does not have access to it. |
| 504 | Query timeout — the query exceeded its time limit. Narrow your filters or the time range. |
The error object always carries type and message; validation errors add a field pointer, and some add a hint suggesting a correction.