---
title: Aggregate
url: https://www.tines.com/stories/docs/api/records/aggregate/
updated: 2026-09-23T08:49:49+00:00
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.
---

*[tines.com](https://www.tines.com/llms.txt) › [Docs](https://www.tines.com/stories/docs/llms.txt) › [Tines API](https://www.tines.com/stories/docs/api/llms.txt) › [Records](https://www.tines.com/llm/stories/docs/api/records.md)*

# Aggregate

*[View on tines.com](https://www.tines.com/stories/docs/api/records/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](/api/records/query) record endpoint. Prefer it over paging through the [Search records API](/api/records/search) 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](/api/records/record_types/list).

## 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](#aggregations)).                                                  |
| group_by         | No       | An array of 0–2 field references to group by (see [Group-by](#group-by)). When omitted, you get a single overall row.                  |
| filter           | No       | A filter tree that narrows records before aggregation (see [Filters](#filters)).                                                       |
| time_range       | No       | An array of up to two timestamp windows restricting which records are aggregated (see [Time range](#time-range)).                      |
| order_by         | No       | Sort the output rows (see [Order-by](#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:

```json
{ "field_name": "Status", "operator": "EQUAL", "value": "open" }
```

or an `AND` group of conditions:

```json
{
  "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](/api/records/search#filters) 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, `AND`ed 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](/api/records/search#sort)'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

```bash
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. `TIMESTAMP` values bucketed with `date_trunc` are ISO 8601 strings whose offset reflects the query `timezone`.
- **Aggregation columns** are keyed by the `alias` you specified (or the auto-generated name). `COUNT` values 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

```json
{
  "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:

```json
{
  "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.
