> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Aggregate records

> Use aggregations to compute statistics, group data into buckets, and analyze trends across your records without retrieving individual items.

Aggregations let you compute statistics, group data into buckets, and analyze trends across large volumes of records without retrieving individual items. Instead of fetching thousands of records and processing them client-side, a single aggregation request can return counts, averages, distributions, time-series breakdowns, and more, directly from the Records service.

Common use cases include:

* **Dashboards and reporting**: Power real-time dashboards with summary statistics, distributions, and time-histogram charts from a single API call.
* **Trend analysis**: Track how metrics change over time using time-based histograms and moving window functions.
* **Distribution analysis**: Understand how values are spread across categories, severity levels, or numeric ranges.
* **Threshold monitoring**: Count records that fall into specific value ranges to detect anomalies or trigger alerts.

Records supports three categories of aggregations:

| Category     | Aggregates                                                    | Description                                                                                               |
| ------------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Metric**   | `count`, `avg`, `sum`, `min`, `max`                           | Compute a single numeric value from a set of records                                                      |
| **Bucket**   | `uniqueValues`, `numberHistogram`, `timeHistogram`, `filters` | Group records into buckets based on property values, numeric ranges, time intervals, or filter conditions |
| **Pipeline** | `movingFunction`                                              | Compute derived values from the output of bucket aggregations, such as moving averages over time          |

## Example schema

The examples on this page use records stored in a stream with the following container schema. The container `equipment_events` in space `factory-data` tracks equipment monitoring events across an industrial facility.

| Property       | Type      | Description                                                |
| -------------- | --------- | ---------------------------------------------------------- |
| `equipment_id` | text      | Equipment identifier (e.g., `PUMP-001`, `MOTOR-042`)       |
| `location`     | text      | Facility location (e.g., `Building-A`, `Building-B`)       |
| `temperature`  | float64   | Measured temperature in degrees Celsius                    |
| `severity`     | text      | Event severity level (`LOW`, `MEDIUM`, `HIGH`, `CRITICAL`) |
| `priority`     | int64     | Priority score from 1 (lowest) to 100 (highest)            |
| `is_critical`  | boolean   | Whether the event requires immediate attention             |
| `recorded_at`  | timestamp | When the event was recorded                                |

<Tip>
  To learn how to create containers, set up streams, and ingest records, see [Get started with Records](/cdf/dm/records/guides/records_getting_started).
</Tip>

## Request structure

Send aggregation requests to the aggregate endpoint:

```
POST /api/v1/projects/{project}/streams/{streamId}/records/aggregate
```

Every request requires an `aggregates` object that maps your chosen names to aggregate definitions. You can optionally include a `filter` to narrow the dataset and a `lastUpdatedTime` range.

```json Example request structure theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
{
  "lastUpdatedTime": {
    "gt": "2025-10-01T00:00:00.000Z"
  },
  "filter": {
    "hasData": [
      {
        "type": "container",
        "space": "factory-data",
        "externalId": "equipment_events"
      }
    ]
  },
  "aggregates": {
    "my_aggregate_name": {
      "<aggregate_type>": { ... }
    }
  }
}
```

<Info>
  **Time-based filtering with lastUpdatedTime**

  The `lastUpdatedTime` filter is **mandatory** for aggregate queries on immutable streams. Immutable streams are optimized for high-volume, append-only data and are internally organized by time. The service uses `lastUpdatedTime` to efficiently target only the relevant data partitions, avoiding costly full-stream scans across potentially billions of records. Without this constraint, aggregate queries on large immutable streams would be prohibitively slow.

  The filter defines the time range for the aggregation:

  * **`gt` (greater than)** / **`gte` (greater than or equal to)**: The start of the time range (required for immutable streams).
  * **`lt` (less than)** / **`lte` (less than or equal to)**: The end of the time range (optional, defaults to current time).

  The maximum time range you can query **in a single request** is determined by the stream's `maxFilteringInterval` setting (an ISO 8601 duration, for example `P1Y` for one year). This limit is defined by the [stream template](/cdf/dm/records/concepts/records_and_streams#stream-templates) selected when the stream was created, and it applies to the span between `gt` and `lt`, not to how far back in time you can reach — you can query any historical period as long as each request stays within the interval. If the difference between `gt` and `lt` exceeds this interval, the API returns a validation error.

  To query data spanning more than this interval, split your requests into adjacent time windows. See [Query time range limits](/cdf/dm/records/concepts/records_and_streams#query-time-range-limits) for details and examples.

  For mutable streams, `lastUpdatedTime` is optional, but using it improves query performance.
</Info>

### Property paths

Aggregations reference container properties using an array of three strings: the space, the container external ID, and the property name.

```json theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
"property": ["factory-data", "equipment_events", "temperature"]
```

Some aggregations also support top-level record properties with a single-element array:

* `["createdTime"]` — when the record was created
* `["lastUpdatedTime"]` — when the record was last updated
* `["space"]` — the space the record belongs to (only for `uniqueValues`)

### Naming rules

You choose the name for each aggregate in the `aggregates` map. Names must be 1-255 characters and cannot contain `[`, `]`, or `>`, since these characters are reserved for referencing aggregate paths in pipeline aggregations.

## Metric aggregates

Metric aggregates compute a single numeric value from all records that match your filter. You can combine multiple metric aggregates in a single request, similar to running `SELECT COUNT(*), AVG(temperature), SUM(priority), MIN(temperature), MAX(temperature) FROM equipment_events WHERE ...` in SQL.

<Note>
  **Null value handling**

  For all metric aggregates except `count` without a property, records with a `null` or missing value for the specified property are excluded from the calculation. They do not affect the result. For example, `avg` computes the average only over records that have a non-null value, so 10 records where 3 have null temperatures would compute the average from the remaining 7 records.

  The `count` aggregate without a `property` counts all matching records regardless of null values. With a `property` specified, `count` only counts records where that property has a non-null value.
</Note>

### count

Returns the number of records. Without a `property`, it counts all matching records. With a `property`, it counts only records that have a non-null value for that property.

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "total_events": {
          "count": {}
        },
        "events_with_temperature": {
          "count": {
            "property": ["factory-data", "equipment_events", "temperature"]
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  from cognite.client import CogniteClient

  client = CogniteClient()

  space_id = "factory-data"
  container_id = "equipment_events"
  stream_id = "your-stream-id"

  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "total_events": {"count": {}},
              "events_with_temperature": {
                  "count": {"property": [space_id, container_id, "temperature"]}
              },
          },
      },
  )

  result = response.json()
  aggs = result["aggregates"]
  print(f"Total events: {aggs['total_events']['count']}")
  print(f"Events with temperature: {aggs['events_with_temperature']['count']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  import { CogniteClient } from '@cognite/sdk';

  const client = new CogniteClient({ appId: 'records-tutorial' });

  const spaceId = 'factory-data';
  const containerId = 'equipment_events';
  const streamId = 'your-stream-id';

  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      total_events: { count: {} },
      events_with_temperature: {
        count: { property: [spaceId, containerId, 'temperature'] },
      },
    },
  });

  console.log(`Total events: ${result.total_events.count}`);
  console.log(`Events with temperature: ${result.events_with_temperature.count}`);
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "total_events": {
        "count": 1250
      },
      "events_with_temperature": {
        "count": 1183
      }
    }
  }
  ```
</CodeGroup>

### avg

Computes the arithmetic mean of a numeric property across all matching records. Records with null values for the specified property are excluded from the calculation.

**Supported property types**: `int32`, `int64`, `float32`, `float64`

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "avg_temperature": {
          "avg": {
            "property": ["factory-data", "equipment_events", "temperature"]
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "avg_temperature": {
                  "avg": {"property": [space_id, container_id, "temperature"]}
              }
          },
      },
  )

  result = response.json()
  print(f"Average temperature: {result['aggregates']['avg_temperature']['avg']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      avg_temperature: {
        avg: { property: [spaceId, containerId, 'temperature'] },
      },
    },
  });

  console.log(`Average temperature: ${result.avg_temperature.avg}`);
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "avg_temperature": {
        "avg": 72.45
      }
    }
  }
  ```
</CodeGroup>

### sum

Computes the total sum of values for a numeric property. Useful for calculating totals like cumulative priority scores or aggregated measurements.

**Supported property types**: `int32`, `int64`, `float32`, `float64`

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "total_priority": {
          "sum": {
            "property": ["factory-data", "equipment_events", "priority"]
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "total_priority": {
                  "sum": {"property": [space_id, container_id, "priority"]}
              }
          },
      },
  )

  result = response.json()
  print(f"Total priority: {result['aggregates']['total_priority']['sum']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      total_priority: {
        sum: { property: [spaceId, containerId, 'priority'] },
      },
    },
  });

  console.log(`Total priority: ${result.total_priority.sum}`);
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "total_priority": {
        "sum": 58420
      }
    }
  }
  ```
</CodeGroup>

### min

Returns the lowest value for a property. Works with numeric and timestamp properties, including the top-level `createdTime` and `lastUpdatedTime` fields.

**Supported property types**: `int32`, `int64`, `float32`, `float64`, `timestamp`, `date`

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "coldest_reading": {
          "min": {
            "property": ["factory-data", "equipment_events", "temperature"]
          }
        },
        "earliest_event": {
          "min": {
            "property": ["lastUpdatedTime"]
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "coldest_reading": {
                  "min": {"property": [space_id, container_id, "temperature"]}
              },
              "earliest_event": {
                  "min": {"property": ["lastUpdatedTime"]}
              },
          },
      },
  )

  result = response.json()
  aggs = result["aggregates"]
  print(f"Coldest reading: {aggs['coldest_reading']['min']}")
  print(f"Earliest event: {aggs['earliest_event']['min']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      coldest_reading: {
        min: { property: [spaceId, containerId, 'temperature'] },
      },
      earliest_event: {
        min: { property: ['lastUpdatedTime'] },
      },
    },
  });

  console.log(`Coldest reading: ${result.coldest_reading.min}`);
  console.log(`Earliest event: ${result.earliest_event.min}`);
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "coldest_reading": {
        "min": 18.3
      },
      "earliest_event": {
        "min": "2025-10-01T08:12:33.000Z"
      }
    }
  }
  ```
</CodeGroup>

### max

Returns the highest value for a property. Works with the same property types as `min`.

**Supported property types**: `int32`, `int64`, `float32`, `float64`, `timestamp`, `date`

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "hottest_reading": {
          "max": {
            "property": ["factory-data", "equipment_events", "temperature"]
          }
        },
        "latest_event": {
          "max": {
            "property": ["lastUpdatedTime"]
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "hottest_reading": {
                  "max": {"property": [space_id, container_id, "temperature"]}
              },
              "latest_event": {
                  "max": {"property": ["lastUpdatedTime"]}
              },
          },
      },
  )

  result = response.json()
  aggs = result["aggregates"]
  print(f"Hottest reading: {aggs['hottest_reading']['max']}")
  print(f"Latest event: {aggs['latest_event']['max']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      hottest_reading: {
        max: { property: [spaceId, containerId, 'temperature'] },
      },
      latest_event: {
        max: { property: ['lastUpdatedTime'] },
      },
    },
  });

  console.log(`Hottest reading: ${result.hottest_reading.max}`);
  console.log(`Latest event: ${result.latest_event.max}`);
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "hottest_reading": {
        "max": 142.7
      },
      "latest_event": {
        "max": "2025-10-24T16:45:12.000Z"
      }
    }
  }
  ```
</CodeGroup>

### Combined metrics example

You can request multiple metric aggregates in a single call to get a complete statistical summary without making separate requests. This example also demonstrates how to combine `hasData` with a `range` condition using `and` to compute metrics only for records where the temperature exceeds 85.5 degrees Celsius.

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "and": [
          {
            "hasData": [{
              "type": "container",
              "space": "factory-data",
              "externalId": "equipment_events"
            }]
          },
          {
            "range": {
              "property": ["factory-data", "equipment_events", "temperature"],
              "gt": 85.5
            }
          }
        ]
      },
      "aggregates": {
        "total_events": { "count": {} },
        "avg_temp": { "avg": { "property": ["factory-data", "equipment_events", "temperature"] } },
        "min_temp": { "min": { "property": ["factory-data", "equipment_events", "temperature"] } },
        "max_temp": { "max": { "property": ["factory-data", "equipment_events", "temperature"] } },
        "total_priority": { "sum": { "property": ["factory-data", "equipment_events", "priority"] } }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "and": [
                  {"hasData": [{"type": "container", "space": space_id, "externalId": container_id}]},
                  {"range": {"property": [space_id, container_id, "temperature"], "gt": 85.5}},
              ]
          },
          "aggregates": {
              "total_events": {"count": {}},
              "avg_temp": {"avg": {"property": [space_id, container_id, "temperature"]}},
              "min_temp": {"min": {"property": [space_id, container_id, "temperature"]}},
              "max_temp": {"max": {"property": [space_id, container_id, "temperature"]}},
              "total_priority": {"sum": {"property": [space_id, container_id, "priority"]}},
          },
      },
  )

  result = response.json()
  aggs = result["aggregates"]
  print(f"Total events: {aggs['total_events']['count']}")
  print(f"Avg temp: {aggs['avg_temp']['avg']}")
  print(f"Min temp: {aggs['min_temp']['min']}")
  print(f"Max temp: {aggs['max_temp']['max']}")
  print(f"Total priority: {aggs['total_priority']['sum']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      and: [
        {
          hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
        },
        {
          range: {
            property: [spaceId, containerId, 'temperature'],
            gt: 85.5,
          },
        },
      ],
    },
    aggregates: {
      total_events: { count: {} },
      avg_temp: { avg: { property: [spaceId, containerId, 'temperature'] } },
      min_temp: { min: { property: [spaceId, containerId, 'temperature'] } },
      max_temp: { max: { property: [spaceId, containerId, 'temperature'] } },
      total_priority: { sum: { property: [spaceId, containerId, 'priority'] } },
    },
  });

  console.log(`Total events: ${result.total_events.count}`);
  console.log(`Avg temp: ${result.avg_temp.avg}`);
  console.log(`Min temp: ${result.min_temp.min}`);
  console.log(`Max temp: ${result.max_temp.max}`);
  console.log(`Total priority: ${result.total_priority.sum}`);
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "total_events": { "count": 312 },
      "avg_temp": { "avg": 104.8 },
      "min_temp": { "min": 85.9 },
      "max_temp": { "max": 142.7 },
      "total_priority": { "sum": 15230 }
    }
  }
  ```
</CodeGroup>

The `and` filter narrows the dataset to only records from the `equipment_events` container where `temperature` is above 85.5 degrees Celsius, so all returned metrics reflect that subset. You can use any [filter expression](/cdf/dm/dm_concepts/dm_querying) here, including `or`, `not`, `equals`, `prefix`, and nested combinations.

## Bucket aggregates

Bucket aggregates group records into categories (called buckets) based on property values, numeric ranges, time intervals, or filter conditions. Each bucket includes a count of matching records and can contain nested sub-aggregates that compute additional metrics per bucket.

<Tip>
  Bucket aggregates with nested sub-aggregates work like `GROUP BY` with aggregate functions in SQL or pandas/Polars. A `uniqueValues` aggregate on `severity` with a nested `avg` on `temperature` is conceptually equivalent to:

  ```sql theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  SELECT severity, COUNT(*) AS count, AVG(temperature) AS avg_temp
  FROM equipment_events
  GROUP BY severity
  ```

  The key difference is that Records aggregates support multiple levels of nesting in a single request, similar to using `MultiIndex` grouping in pandas or chaining `.group_by()` operations in Polars.
</Tip>

### uniqueValues

Groups records by the distinct values of a property and returns one bucket per unique value, ordered by count (highest first). This is the equivalent of `GROUP BY` in SQL, `.groupby()` in pandas, or `.group_by()` in Polars, useful for understanding distributions, for example, how many events exist for each severity level or which equipment generates the most events.

#### Parameters

| Parameter    | Type    | Required | Description                                                                                                         |
| ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `property`   | array   | Yes      | The property to group by. Supports text, numeric, boolean, and enum properties, and the top-level `["space"]` path. |
| `size`       | integer | No       | Maximum number of buckets to return. Range: 1-2000. Default: 10.                                                    |
| `aggregates` | object  | No       | Nested sub-aggregates to compute within each bucket.                                                                |

<Note>
  **Ordering and size behavior**

  Buckets are returned in descending order by record count (most common values first). When your data contains more unique values than the requested `size`, the response returns only the top buckets by count. The remaining values are not included in the response. If you need to see all unique values, increase `size` up to the maximum of 2000.

  **Approximate counts**

  Counts may be approximate when the dataset is very large. Because records are distributed across multiple partitions for performance, each partition independently identifies its top values and these are merged to produce the final result. This means a value that is moderately common across many partitions might not appear in every partition's top list, leading to a slight undercount. For most use cases, the approximation is negligible and does not affect the relative ranking of top values.

  **Missing values**

  Records that do not have the specified property (or have a `null` value) are excluded from the aggregation. They are not counted and do not produce a bucket. Only records with a non-null value for the property contribute to the unique value buckets.
</Note>

#### Example: severity distribution with nested average

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "by_severity": {
          "uniqueValues": {
            "property": ["factory-data", "equipment_events", "severity"],
            "size": 10,
            "aggregates": {
              "avg_temp": {
                "avg": {
                  "property": ["factory-data", "equipment_events", "temperature"]
                }
              }
            }
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "by_severity": {
                  "uniqueValues": {
                      "property": [space_id, container_id, "severity"],
                      "size": 10,
                      "aggregates": {
                          "avg_temp": {
                              "avg": {"property": [space_id, container_id, "temperature"]}
                          }
                      },
                  }
              }
          },
      },
  )

  result = response.json()
  for bucket in result["aggregates"]["by_severity"]["uniqueValueBuckets"]:
      print(f"{bucket['value']}: {bucket['count']} events, avg temp {bucket['aggregates']['avg_temp']['avg']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      by_severity: {
        uniqueValues: {
          property: [spaceId, containerId, 'severity'],
          size: 10,
          aggregates: {
            avg_temp: {
              avg: { property: [spaceId, containerId, 'temperature'] },
            },
          },
        },
      },
    },
  });

  const buckets = result.by_severity.uniqueValueBuckets;
  buckets.forEach((bucket) => {
    console.log(`${bucket.value}: ${bucket.count} events, avg temp ${bucket.aggregates.avg_temp.avg}`);
  });
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "by_severity": {
        "uniqueValueBuckets": [
          {
            "value": "LOW",
            "count": 523,
            "aggregates": { "avg_temp": { "avg": 65.2 } }
          },
          {
            "value": "MEDIUM",
            "count": 412,
            "aggregates": { "avg_temp": { "avg": 78.1 } }
          },
          {
            "value": "HIGH",
            "count": 238,
            "aggregates": { "avg_temp": { "avg": 91.4 } }
          },
          {
            "value": "CRITICAL",
            "count": 77,
            "aggregates": { "avg_temp": { "avg": 118.6 } }
          }
        ]
      }
    }
  }
  ```
</CodeGroup>

### numberHistogram

Divides numeric values into fixed-width intervals (buckets) and counts how many records fall into each interval. Each bucket represents a range starting at the bucket key and extending to the next interval boundary. For example, with an interval of 10, a bucket with key `60.0` contains records with values in the range \[60, 70).

The bucket key for a value is calculated as:

```
bucket_key = floor(value / interval) * interval
```

This means each value is rounded down to its closest interval boundary. A record with a temperature of `73.2` and an interval of `20` would be assigned to the bucket with key `60.0` (since floor(73.2 / 20) \* 20 = 60), which covers the range \[60, 80).

<Tip>
  `numberHistogram` is similar to binning a numeric column and grouping by the bins in SQL or pandas/Polars:

  ```sql theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  SELECT FLOOR(temperature / 20) * 20 AS bin, COUNT(*)
  FROM equipment_events
  GROUP BY bin ORDER BY bin
  ```

  In pandas, this is `df.groupby(df["temperature"] // 20 * 20).size()`, and in Polars, `df.group_by(pl.col("temperature").floordiv(20) * 20).len()`.
</Tip>

#### Parameters

| Parameter    | Type   | Required | Description                                                                                                                                                                                                  |
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `property`   | array  | Yes      | The numeric property to bucket. Supports `int32`, `int64`, `float32`, `float64`.                                                                                                                             |
| `interval`   | number | Yes      | The width of each bucket. Must be a positive number.                                                                                                                                                         |
| `hardBounds` | object | No       | Limits the range of buckets. Has optional `min` (inclusive) and `max` (inclusive) fields. Only buckets whose keys fall within \[min, max] are returned, even if matching records exist outside these bounds. |
| `aggregates` | object | No       | Nested sub-aggregates to compute within each bucket.                                                                                                                                                         |

<Note>
  **Empty buckets**

  By default, the response includes empty buckets (buckets with a count of 0) between the lowest and highest values in the data. This provides a continuous, gap-free view of the data distribution. For example, if your data has values at 20 and 80 with an interval of 20, you will get buckets for 20, 40, 60, and 80, even if the 40 and 60 buckets have a count of 0. The first bucket is determined by the smallest value in the data, and the last bucket by the largest value.

  **Hard bounds vs. filters**

  Use `hardBounds` to limit which buckets appear in the response. This is purely a display-level control, it does not affect which records are aggregated, only which buckets are included in the output. Records outside the bounds are still counted if they fall into a bucket that starts within the bounds.

  This is different from using a `filter` in the request, which restricts which records are considered for the entire aggregation. For example, if you set `hardBounds: { "min": 40, "max": 120 }`, records with temperatures below 40 are still counted (in the bucket whose range starts below 40 if it overlaps with the bounds), but the response only includes buckets whose keys fall within \[40, 120] (both bounds inclusive).

  If you only need results for a specific range, prefer a `filter` over `hardBounds`. Filters reduce the number of records the service needs to aggregate, which improves performance. Reserve `hardBounds` for cases where you need multiple views in a single request, for example, a full-range metric aggregate alongside bucketed results for a narrower range.

  **Missing values**

  Records that do not have the specified property (or have a `null` value) are excluded from the histogram. They do not contribute to any bucket.
</Note>

#### Example: temperature distribution

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "temp_distribution": {
          "numberHistogram": {
            "property": ["factory-data", "equipment_events", "temperature"],
            "interval": 20,
            "hardBounds": { "min": 0, "max": 160 }
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "temp_distribution": {
                  "numberHistogram": {
                      "property": [space_id, container_id, "temperature"],
                      "interval": 20,
                      "hardBounds": {"min": 0, "max": 160},
                  }
              }
          },
      },
  )

  result = response.json()
  for bucket in result["aggregates"]["temp_distribution"]["numberHistogramBuckets"]:
      print(f"[{bucket['intervalStart']}, {bucket['intervalStart'] + 20}): {bucket['count']} events")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      temp_distribution: {
        numberHistogram: {
          property: [spaceId, containerId, 'temperature'],
          interval: 20,
          hardBounds: { min: 0, max: 160 },
        },
      },
    },
  });

  const buckets = result.temp_distribution.numberHistogramBuckets;
  buckets.forEach((bucket) => {
    console.log(`[${bucket.intervalStart}, ${bucket.intervalStart + 20}): ${bucket.count} events`);
  });
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "temp_distribution": {
        "numberHistogramBuckets": [
          { "intervalStart": 0, "count": 12 },
          { "intervalStart": 20, "count": 45 },
          { "intervalStart": 40, "count": 178 },
          { "intervalStart": 60, "count": 412 },
          { "intervalStart": 80, "count": 356 },
          { "intervalStart": 100, "count": 168 },
          { "intervalStart": 120, "count": 0 },
          { "intervalStart": 140, "count": 62 },
          { "intervalStart": 160, "count": 17 }
        ]
      }
    }
  }
  ```
</CodeGroup>

### timeHistogram

Divides timestamp values into time-based intervals and counts records per interval. This is the primary aggregation for building time-series charts, trend lines, and temporal analysis. Each bucket represents a time window starting at the `intervalStart` timestamp.

<Tip>
  `timeHistogram` is similar to truncating timestamps and grouping by the truncated value in SQL or pandas/Polars:

  ```sql theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  SELECT DATE_TRUNC('hour', recorded_at) AS hour, COUNT(*)
  FROM equipment_events
  GROUP BY hour ORDER BY hour
  ```

  In pandas, this is `df.resample("1h", on="recorded_at").size()`, and in Polars, `df.group_by_dynamic("recorded_at", every="1h").agg(pl.len())`.
</Tip>

You must specify exactly one of `calendarInterval` or `fixedInterval`:

* **calendarInterval**: Uses calendar-aware intervals that respect natural time boundaries. For example, `1M` produces monthly buckets where each month has its actual number of days (28, 29, 30, or 31), and `1d` respects daylight saving time transitions. Only single-unit values are allowed because calendar units vary in length (a "2-month" interval would be ambiguous since months have different numbers of days).
* **fixedInterval**: Uses fixed-duration intervals measured in consistent time units. Any multiplier is allowed (e.g., `30m`, `6h`, `2d`). Each bucket has exactly the same duration in absolute time. Use fixed intervals when you need precise, uniform bucket sizes. For example, `6h` always means exactly 21,600,000 milliseconds, regardless of daylight saving time or calendar boundaries.

Choose `calendarInterval` when you want human-readable time boundaries (start of each day, month, or year). Choose `fixedInterval` when you need precise, uniform durations (every 30 minutes, every 6 hours).

#### Parameters

| Parameter          | Type   | Required | Description                                                                                                                                                                                                                                                                                                                                            |
| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `property`         | array  | Yes      | The timestamp property to bucket. Also supports `["createdTime"]` and `["lastUpdatedTime"]`.                                                                                                                                                                                                                                                           |
| `calendarInterval` | string | One of   | Calendar-aware interval. Allowed values: `1s` (second), `1m` (minute), `1h` (hour), `1d` (day), `1w` (week), `1M` (month), `1q` (quarter), `1y` (year).                                                                                                                                                                                                |
| `fixedInterval`    | string | One of   | Fixed-duration interval as a duration string. Supported units: `ms` (milliseconds), `s` (seconds), `m` (minutes), `h` (hours), `d` (days). Any positive multiplier is allowed (e.g., `30m`, `6h`, `2d`).                                                                                                                                               |
| `hardBounds`       | object | No       | Limits the range of time buckets. Has optional `min` (inclusive) and `max` (exclusive) fields. Values must be ISO 8601 date-time strings with timezone (e.g., `2025-06-15T00:00:00Z`, `2025-06-15T00:00:00.000Z`, or `2025-06-15T02:00:00+02:00`). Epoch milliseconds are not supported. Only buckets whose keys fall within \[min, max) are returned. |
| `aggregates`       | object | No       | Nested sub-aggregates to compute within each bucket.                                                                                                                                                                                                                                                                                                   |

<Warning>
  You must specify **exactly one** of `calendarInterval` or `fixedInterval`. Providing both or neither causes a validation error.

  For `calendarInterval`, only single-unit multipliers are allowed (`1h`, `1d`, `1M`, etc.). Multiples like `2d` or `6h` are **not valid** for calendar intervals because calendar units have variable lengths. Use `fixedInterval` instead if you need non-unit intervals (e.g., `fixedInterval: "6h"`).
</Warning>

The following details describe how the `timeHistogram` handles edge cases such as empty intervals, bounded time ranges, and missing data.

<Note>
  **Empty buckets**

  Like `numberHistogram`, the `timeHistogram` returns empty buckets (count of 0) between the earliest and latest data points. This ensures a continuous time series with no gaps, which is important for charting and trend analysis. For example, if your data has records at 08:00 and 14:00 with a `calendarInterval` of `"1h"`, you will get buckets for every hour from 08:00 through 14:00, including hours with no records.

  **Hard bounds for time ranges**

  Use `hardBounds` with `min` (inclusive) and `max` (exclusive) as ISO 8601 date-time strings to limit which time buckets appear in the response. Only buckets whose keys fall within \[min, max) are returned. Supported formats include `2025-06-15T00:00:00Z`, `2025-06-15T00:00:00.000Z`, and `2025-06-15T02:00:00+02:00` (timezone offsets are converted to UTC). Epoch milliseconds are not supported; use ISO 8601 date-time strings instead.

  Note that `timeHistogram` uses a half-open range \[min, max) while `numberHistogram` uses a closed range \[min, max]. Like `numberHistogram`, `hardBounds` controls only which buckets are returned — it does not filter which records are aggregated.

  **Missing values**

  Records that do not have the specified timestamp property (or have a `null` value) are excluded from the time histogram. They do not contribute to any time bucket.
</Note>

#### Example: hourly event counts

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "hourly_events": {
          "timeHistogram": {
            "property": ["factory-data", "equipment_events", "recorded_at"],
            "calendarInterval": "1h"
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "hourly_events": {
                  "timeHistogram": {
                      "property": [space_id, container_id, "recorded_at"],
                      "calendarInterval": "1h",
                  }
              }
          },
      },
  )

  result = response.json()
  for bucket in result["aggregates"]["hourly_events"]["timeHistogramBuckets"]:
      print(f"{bucket['intervalStart']}: {bucket['count']} events")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      hourly_events: {
        timeHistogram: {
          property: [spaceId, containerId, 'recorded_at'],
          calendarInterval: '1h',
        },
      },
    },
  });

  const buckets = result.hourly_events.timeHistogramBuckets;
  buckets.forEach((bucket) => {
    console.log(`${bucket.intervalStart}: ${bucket.count} events`);
  });
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "hourly_events": {
        "timeHistogramBuckets": [
          { "intervalStart": "2025-10-24T08:00:00.000Z", "count": 45 },
          { "intervalStart": "2025-10-24T09:00:00.000Z", "count": 78 },
          { "intervalStart": "2025-10-24T10:00:00.000Z", "count": 112 },
          { "intervalStart": "2025-10-24T11:00:00.000Z", "count": 0 },
          { "intervalStart": "2025-10-24T12:00:00.000Z", "count": 63 },
          { "intervalStart": "2025-10-24T13:00:00.000Z", "count": 88 },
          { "intervalStart": "2025-10-24T14:00:00.000Z", "count": 102 },
          { "intervalStart": "2025-10-24T15:00:00.000Z", "count": 71 },
          { "intervalStart": "2025-10-24T16:00:00.000Z", "count": 34 }
        ]
      }
    }
  }
  ```
</CodeGroup>

### filters

Creates one bucket per filter expression, where each bucket contains the count of records matching that filter. Use this aggregation to segment records into categories defined by arbitrary conditions. For example, splitting events into severity bands based on numeric ranges, or comparing events across different equipment types.

<Tip>
  The `filters` aggregate is similar to conditional counting in a single query in SQL or pandas/Polars:

  ```sql theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  SELECT
    COUNT(*) FILTER (WHERE temperature < 50)           AS cold,
    COUNT(*) FILTER (WHERE temperature >= 50 AND temperature < 80)  AS normal,
    COUNT(*) FILTER (WHERE temperature >= 80 AND temperature < 110) AS warm,
    COUNT(*) FILTER (WHERE temperature >= 110)          AS hot
  FROM equipment_events
  ```

  In pandas or Polars, this is equivalent to applying multiple boolean masks and counting each subset. Note that unlike `GROUP BY`, a record can match multiple filters and appear in more than one bucket.
</Tip>

Each filter uses the same [filter syntax](/cdf/dm/dm_concepts/dm_querying) available on the filter and sync endpoints.

#### Parameters

| Parameter    | Type   | Required | Description                                                          |
| ------------ | ------ | -------- | -------------------------------------------------------------------- |
| `filters`    | array  | Yes      | An array of 1-10 filter expressions. Each filter defines one bucket. |
| `aggregates` | object | No       | Nested sub-aggregates to compute within each bucket.                 |

<Note>
  **Bucket ordering**

  Buckets are returned in the same order as the filters in your request. Since the filters are provided as an array (not a named map), you identify each bucket by its position. For example, the first bucket in the `filterBuckets` response array corresponds to the first filter in your `filters` array.

  **Overlapping filters**

  A record can match multiple filters and appear in more than one bucket. The filters are evaluated independently, they do not partition the data into mutually exclusive groups. If you need non-overlapping segments, design your filter conditions so they don't overlap (for example, use `lt` and `gte` to create adjacent ranges without gaps or overlaps).

  **Limits**

  A single request can have at most **30 filter buckets** across all `filters` aggregates combined. For example, if you have three `filters` aggregates with 10 filters each, that uses all 30.
</Note>

#### Example: temperature severity bands

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "temp_bands": {
          "filters": {
            "filters": [
              {
                "range": {
                  "property": ["factory-data", "equipment_events", "temperature"],
                  "lt": 50
                }
              },
              {
                "range": {
                  "property": ["factory-data", "equipment_events", "temperature"],
                  "gte": 50, "lt": 80
                }
              },
              {
                "range": {
                  "property": ["factory-data", "equipment_events", "temperature"],
                  "gte": 80, "lt": 110
                }
              },
              {
                "range": {
                  "property": ["factory-data", "equipment_events", "temperature"],
                  "gte": 110
                }
              }
            ]
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "temp_bands": {
                  "filters": {
                      "filters": [
                          {"range": {"property": [space_id, container_id, "temperature"], "lt": 50}},
                          {"range": {"property": [space_id, container_id, "temperature"], "gte": 50, "lt": 80}},
                          {"range": {"property": [space_id, container_id, "temperature"], "gte": 80, "lt": 110}},
                          {"range": {"property": [space_id, container_id, "temperature"], "gte": 110}},
                      ]
                  }
              }
          },
      },
  )

  result = response.json()
  labels = ["Cold (<50)", "Normal (50-80)", "Warm (80-110)", "Hot (>110)"]
  for i, bucket in enumerate(result["aggregates"]["temp_bands"]["filterBuckets"]):
      print(f"{labels[i]}: {bucket['count']} events")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      temp_bands: {
        filters: {
          filters: [
            { range: { property: [spaceId, containerId, 'temperature'], lt: 50 } },
            { range: { property: [spaceId, containerId, 'temperature'], gte: 50, lt: 80 } },
            { range: { property: [spaceId, containerId, 'temperature'], gte: 80, lt: 110 } },
            { range: { property: [spaceId, containerId, 'temperature'], gte: 110 } },
          ],
        },
      },
    },
  });

  const labels = ['Cold (<50)', 'Normal (50-80)', 'Warm (80-110)', 'Hot (>110)'];
  result.temp_bands.filterBuckets.forEach((bucket, i) => {
    console.log(`${labels[i]}: ${bucket.count} events`);
  });
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "temp_bands": {
        "filterBuckets": [
          { "count": 57 },
          { "count": 590 },
          { "count": 524 },
          { "count": 79 }
        ]
      }
    }
  }
  ```
</CodeGroup>

The four buckets correspond to the four filters in order: below 50 degrees Celsius, 50-80 degrees Celsius, 80-110 degrees Celsius, and above 110 degrees Celsius.

## Pipeline aggregates

Pipeline aggregates compute derived values from the output of bucket aggregations. Unlike metric and bucket aggregates that operate on records directly, pipeline aggregates process the results of other aggregates, making them ideal for trend smoothing, rate calculations, and comparative analysis.

### movingFunction

Slides a window across the buckets of a histogram and applies a function to the values within that window. This is commonly used to smooth noisy data by computing a moving average over time, or to calculate rolling sums and extremes.

<Tip>
  `movingFunction` is similar to SQL window functions with a frame clause, `.rolling()` in pandas, or `.rolling_mean()` in Polars:

  ```sql theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  SELECT hour, event_count,
    AVG(event_count) OVER (ORDER BY hour ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg
  FROM hourly_counts
  ```

  In pandas, this is `df["event_count"].rolling(window=3).mean()`, and in Polars, `df.select(pl.col("event_count").rolling_mean(window_size=3))`.
</Tip>

The `movingFunction` aggregate **must be nested inside** a `numberHistogram` or `timeHistogram`. It cannot be used as a standalone aggregate.

#### Parameters

| Parameter     | Type    | Required | Description                                                                                                              |
| ------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `bucketsPath` | string  | Yes      | Path to the metric to use as input. Use `_count` for the bucket record count, or the name of a sibling metric aggregate. |
| `window`      | integer | Yes      | Size of the sliding window in number of buckets. Must be at least 1.                                                     |
| `function`    | string  | Yes      | The function to apply over the window.                                                                                   |

#### Available functions

| Function                            | Description                                                                                                              |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `MovingFunctions.unweightedAvg`     | Simple average of all values in the window. The most common choice for trend smoothing.                                  |
| `MovingFunctions.linearWeightedAvg` | Weighted average where more recent values have higher weight. Gives a trend line that responds faster to recent changes. |
| `MovingFunctions.sum`               | Sum of all values in the window. Useful for calculating rolling totals.                                                  |
| `MovingFunctions.min`               | Minimum value in the window.                                                                                             |
| `MovingFunctions.max`               | Maximum value in the window.                                                                                             |

<Note>
  **Window warm-up**

  The first few buckets in the response will have a window smaller than the specified `window` size because there aren't enough preceding buckets yet. For example, with `window: 3`:

  * Bucket 1: the `movingFunction` receives only one value (the current bucket), so `MovingFunctions.unweightedAvg` returns 0 because it requires at least 2 values.
  * Bucket 2: the window contains 2 values (buckets 1 and 2), so the average is computed from those two.
  * Bucket 3 onward: the full 3-bucket window is used.

  This warm-up behavior means the first few results may not be representative. For `MovingFunctions.unweightedAvg`, the first bucket always returns `0` as the `fnValue`.

  **Handling gaps in data**

  When a bucket in the window has no data (a gap), the function skips that bucket and computes the result from the remaining values in the window. This prevents empty time periods from distorting the smoothed trend line. For example, if a 3-bucket window covers hours 10:00, 11:00, and 12:00, but 11:00 has no records, the moving average is computed from only the 10:00 and 12:00 values.

  **Pipeline vs. sub-aggregate**

  Pipeline aggregates like `movingFunction` are fundamentally different from sub-aggregates. Sub-aggregates (like nesting `avg` inside `timeHistogram`) operate on the records within each bucket. Pipeline aggregates operate on the computed results of other aggregates across buckets — they process bucket-level outputs, not individual records.
</Note>

#### bucketsPath syntax

The `bucketsPath` specifies which value from the parent histogram buckets to feed into the function. Paths are relative to the parent histogram aggregate, not absolute paths from the root of the request.

The formal syntax is:

```
AGG_SEPARATOR       =  `>` ;
AGG_NAME            =  <the name of the aggregation> ;
MULTIBUCKET_KEY     =  `[<KEY_NAME>]`
KEY_NAME            =  <the name of the bucket key in the multi-bucket aggregate result> ;
PATH                =  <AGG_NAME><MULTIBUCKET_KEY>?(<AGG_SEPARATOR><AGG_NAME>)* ;
```

Common path patterns:

| Path                        | Description                                                                                                                                                                      |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_count`                    | The record count of each bucket. Use this when you want to apply the moving function to the number of records per bucket.                                                        |
| `my_metric`                 | The result of a sibling metric aggregate named `my_metric`. For example, if you have a sibling `avg` aggregate named `avg_temp`, use `"bucketsPath": "avg_temp"`.                |
| `my_agg>nested_metric`      | A metric inside a nested aggregate. Use `>` to traverse levels. For example, `"by_severity>avg_temp"` accesses the `avg_temp` metric inside the `by_severity` bucket aggregate.  |
| `my_agg[bucket_key]>metric` | A metric inside a specific bucket of a multi-bucket aggregate. For example, `"by_severity[HIGH]>avg_temp"` accesses the average temperature for the "HIGH" severity bucket only. |

#### Example: 3-hour moving average of event counts

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "hourly_trend": {
          "timeHistogram": {
            "property": ["factory-data", "equipment_events", "recorded_at"],
            "calendarInterval": "1h",
            "aggregates": {
              "moving_avg_3h": {
                "movingFunction": {
                  "bucketsPath": "_count",
                  "window": 3,
                  "function": "MovingFunctions.unweightedAvg"
                }
              }
            }
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "hourly_trend": {
                  "timeHistogram": {
                      "property": [space_id, container_id, "recorded_at"],
                      "calendarInterval": "1h",
                      "aggregates": {
                          "moving_avg_3h": {
                              "movingFunction": {
                                  "bucketsPath": "_count",
                                  "window": 3,
                                  "function": "MovingFunctions.unweightedAvg",
                              }
                          }
                      },
                  }
              }
          },
      },
  )

  result = response.json()
  for bucket in result["aggregates"]["hourly_trend"]["timeHistogramBuckets"]:
      print(f"{bucket['intervalStart']}: {bucket['count']} events, moving avg: {bucket['aggregates']['moving_avg_3h']['fnValue']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      hourly_trend: {
        timeHistogram: {
          property: [spaceId, containerId, 'recorded_at'],
          calendarInterval: '1h',
          aggregates: {
            moving_avg_3h: {
              movingFunction: {
                bucketsPath: '_count',
                window: 3,
                function: 'MovingFunctions.unweightedAvg',
              },
            },
          },
        },
      },
    },
  });

  const buckets = result.hourly_trend.timeHistogramBuckets;
  buckets.forEach((bucket) => {
    console.log(`${bucket.intervalStart}: ${bucket.count} events, moving avg: ${bucket.aggregates.moving_avg_3h.fnValue}`);
  });
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "hourly_trend": {
        "timeHistogramBuckets": [
          {
            "intervalStart": "2025-10-24T08:00:00.000Z",
            "count": 45,
            "aggregates": { "moving_avg_3h": { "fnValue": 45.0 } }
          },
          {
            "intervalStart": "2025-10-24T09:00:00.000Z",
            "count": 78,
            "aggregates": { "moving_avg_3h": { "fnValue": 61.5 } }
          },
          {
            "intervalStart": "2025-10-24T10:00:00.000Z",
            "count": 112,
            "aggregates": { "moving_avg_3h": { "fnValue": 78.3 } }
          },
          {
            "intervalStart": "2025-10-24T11:00:00.000Z",
            "count": 95,
            "aggregates": { "moving_avg_3h": { "fnValue": 95.0 } }
          },
          {
            "intervalStart": "2025-10-24T12:00:00.000Z",
            "count": 63,
            "aggregates": { "moving_avg_3h": { "fnValue": 90.0 } }
          },
          {
            "intervalStart": "2025-10-24T13:00:00.000Z",
            "count": 88,
            "aggregates": { "moving_avg_3h": { "fnValue": 82.0 } }
          },
          {
            "intervalStart": "2025-10-24T14:00:00.000Z",
            "count": 102,
            "aggregates": { "moving_avg_3h": { "fnValue": 84.3 } }
          },
          {
            "intervalStart": "2025-10-24T15:00:00.000Z",
            "count": 71,
            "aggregates": { "moving_avg_3h": { "fnValue": 87.0 } }
          },
          {
            "intervalStart": "2025-10-24T16:00:00.000Z",
            "count": 34,
            "aggregates": { "moving_avg_3h": { "fnValue": 69.0 } }
          }
        ]
      }
    }
  }
  ```
</CodeGroup>

The bar chart shows raw hourly counts while the line shows the smoothed 3-hour moving average, making it easier to identify the underlying trend.

## Nesting aggregates

Bucket aggregates (`uniqueValues`, `numberHistogram`, `timeHistogram`, and `filters`) can contain nested sub-aggregates of any type — including other bucket aggregates. This lets you build multi-dimensional analyses in a single API call.

### How nesting works

When you add sub-aggregates to a bucket aggregate, the sub-aggregates are computed independently within each bucket. For example, nesting an `avg` inside a `uniqueValues` aggregate computes a separate average for each unique value group.

You can nest aggregates up to 5 levels deep, with up to 5 aggregates per level and 16 total aggregates in the request.

<Tip>
  Nesting maps directly to multi-column `GROUP BY` with aggregate functions in SQL or pandas/Polars. The example below groups by `location`, then by `severity` within each location, computing an average temperature per group:

  <CodeGroup>
    ```sql SQL theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    SELECT location, severity, COUNT(*) AS count, AVG(temperature) AS avg_temp
    FROM equipment_events
    GROUP BY location, severity
    ORDER BY location, count DESC
    ```

    ```python Polars theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    df.group_by("location", "severity").agg(
        pl.len().alias("count"),
        pl.col("temperature").mean().alias("avg_temp"),
    ).sort("location", "count", descending=[False, True])
    ```

    ```python pandas theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
    df.groupby(["location", "severity"]).agg(
        count=("temperature", "size"),
        avg_temp=("temperature", "mean"),
    ).sort_values(["location", "count"], ascending=[True, False])
    ```
  </CodeGroup>

  In the Records API, this becomes a `uniqueValues` on `location` with a nested `uniqueValues` on `severity` and a nested `avg` on `temperature`, all in a single request.
</Tip>

While nesting is powerful, deep nesting with high-cardinality data can be expensive. Keep these performance considerations in mind.

<Warning>
  Deep nesting with high-cardinality bucket aggregates can be expensive in terms of computation and memory. For example, nesting a `uniqueValues` with `size: 1000` inside another `uniqueValues` with `size: 1000` could produce up to 1,000,000 bucket combinations.

  A single aggregate request can produce a maximum of **10,000 buckets** across all aggregations. If the total number of buckets exceeds this limit, the API returns a **400 Bad Request** error. Note that even requests that stay under the limit can consume significant resources if the bucket count is high.

  To keep requests efficient:

  * Use smaller `size` values for outer bucket aggregates when possible.
  * Place the highest-cardinality groupings at the deepest level of nesting, not the outermost.
  * Prefer metric aggregates (like `count`, `avg`, `sum`) as leaf-level sub-aggregates rather than adding more bucket levels.
  * If you need more than 16 aggregates or deeper nesting, split the analysis across multiple requests.
</Warning>

### Example: equipment breakdown by location and severity

This example uses two levels of `uniqueValues` with a nested `avg` to answer: "For each location, what are the top severity levels and their average temperatures?"

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "by_location": {
          "uniqueValues": {
            "property": ["factory-data", "equipment_events", "location"],
            "size": 5,
            "aggregates": {
              "by_severity": {
                "uniqueValues": {
                  "property": ["factory-data", "equipment_events", "severity"]
                }
              },
              "avg_temp": {
                "avg": {
                  "property": ["factory-data", "equipment_events", "temperature"]
                }
              }
            }
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "by_location": {
                  "uniqueValues": {
                      "property": [space_id, container_id, "location"],
                      "size": 5,
                      "aggregates": {
                          "by_severity": {
                              "uniqueValues": {"property": [space_id, container_id, "severity"]}
                          },
                          "avg_temp": {
                              "avg": {"property": [space_id, container_id, "temperature"]}
                          },
                      },
                  }
              }
          },
      },
  )

  result = response.json()
  for loc in result["aggregates"]["by_location"]["uniqueValueBuckets"]:
      print(f"{loc['value']}: {loc['count']} events, avg temp {loc['aggregates']['avg_temp']['avg']}")
      for sev in loc["aggregates"]["by_severity"]["uniqueValueBuckets"]:
          print(f"  {sev['value']}: {sev['count']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      by_location: {
        uniqueValues: {
          property: [spaceId, containerId, 'location'],
          size: 5,
          aggregates: {
            by_severity: {
              uniqueValues: {
                property: [spaceId, containerId, 'severity'],
              },
            },
            avg_temp: {
              avg: { property: [spaceId, containerId, 'temperature'] },
            },
          },
        },
      },
    },
  });

  const locations = result.by_location.uniqueValueBuckets;
  locations.forEach((loc) => {
    console.log(`${loc.value}: ${loc.count} events, avg temp ${loc.aggregates.avg_temp.avg}`);
    loc.aggregates.by_severity.uniqueValueBuckets.forEach((sev) => {
      console.log(`  ${sev.value}: ${sev.count}`);
    });
  });
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "by_location": {
        "uniqueValueBuckets": [
          {
            "value": "Building-A",
            "count": 680,
            "aggregates": {
              "avg_temp": { "avg": 74.2 },
              "by_severity": {
                "uniqueValueBuckets": [
                  { "value": "LOW", "count": 290 },
                  { "value": "MEDIUM", "count": 215 },
                  { "value": "HIGH", "count": 130 },
                  { "value": "CRITICAL", "count": 45 }
                ]
              }
            }
          },
          {
            "value": "Building-B",
            "count": 570,
            "aggregates": {
              "avg_temp": { "avg": 70.1 },
              "by_severity": {
                "uniqueValueBuckets": [
                  { "value": "LOW", "count": 233 },
                  { "value": "MEDIUM", "count": 197 },
                  { "value": "HIGH", "count": 108 },
                  { "value": "CRITICAL", "count": 32 }
                ]
              }
            }
          }
        ]
      }
    }
  }
  ```
</CodeGroup>

### Example: time-series with severity bands and trend line

This example combines `timeHistogram`, `filters`, and `movingFunction` to create a stacked severity breakdown over time with a smoothed trend line — all in a single request. This is a common pattern for alarm monitoring dashboards.

<CodeGroup>
  ```bash curl theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  curl -X POST \
    "https://${CLUSTER}.cognitedata.com/api/v1/projects/${PROJECT}/streams/${STREAM_ID}/records/aggregate" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer ${TOKEN}" \
    -d '{
      "lastUpdatedTime": { "gt": "2025-10-01T00:00:00.000Z" },
      "filter": {
        "hasData": [{
          "type": "container",
          "space": "factory-data",
          "externalId": "equipment_events"
        }]
      },
      "aggregates": {
        "events_over_time": {
          "timeHistogram": {
            "property": ["factory-data", "equipment_events", "recorded_at"],
            "calendarInterval": "1d",
            "aggregates": {
              "by_severity_band": {
                "filters": {
                  "filters": [
                    {
                      "equals": {
                        "property": ["factory-data", "equipment_events", "severity"],
                        "value": "LOW"
                      }
                    },
                    {
                      "equals": {
                        "property": ["factory-data", "equipment_events", "severity"],
                        "value": "MEDIUM"
                      }
                    },
                    {
                      "or": [
                        {
                          "equals": {
                            "property": ["factory-data", "equipment_events", "severity"],
                            "value": "HIGH"
                          }
                        },
                        {
                          "equals": {
                            "property": ["factory-data", "equipment_events", "severity"],
                            "value": "CRITICAL"
                          }
                        }
                      ]
                    }
                  ]
                }
              },
              "trend": {
                "movingFunction": {
                  "bucketsPath": "_count",
                  "window": 3,
                  "function": "MovingFunctions.unweightedAvg"
                }
              }
            }
          }
        }
      }
    }'
  ```

  ```python Python theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  # Using client and variables from previous examples
  response = client.post(
      url=f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records/aggregate",
      json={
          "lastUpdatedTime": {"gt": "2025-10-01T00:00:00.000Z"},
          "filter": {
              "hasData": [{"type": "container", "space": space_id, "externalId": container_id}]
          },
          "aggregates": {
              "events_over_time": {
                  "timeHistogram": {
                      "property": [space_id, container_id, "recorded_at"],
                      "calendarInterval": "1d",
                      "aggregates": {
                          "by_severity_band": {
                              "filters": {
                                  "filters": [
                                      {"equals": {"property": [space_id, container_id, "severity"], "value": "LOW"}},
                                      {"equals": {"property": [space_id, container_id, "severity"], "value": "MEDIUM"}},
                                      {"or": [
                                          {"equals": {"property": [space_id, container_id, "severity"], "value": "HIGH"}},
                                          {"equals": {"property": [space_id, container_id, "severity"], "value": "CRITICAL"}},
                                      ]},
                                  ]
                              }
                          },
                          "trend": {
                              "movingFunction": {
                                  "bucketsPath": "_count",
                                  "window": 3,
                                  "function": "MovingFunctions.unweightedAvg",
                              }
                          },
                      },
                  }
              }
          },
      },
  )

  result = response.json()
  band_labels = ["LOW", "MEDIUM", "HIGH+CRITICAL"]
  for bucket in result["aggregates"]["events_over_time"]["timeHistogramBuckets"]:
      print(f"{bucket['intervalStart']}: {bucket['count']} events, trend: {bucket['aggregates']['trend']['fnValue']}")
      for i, band in enumerate(bucket["aggregates"]["by_severity_band"]["filterBuckets"]):
          print(f"  {band_labels[i]}: {band['count']}")
  ```

  ```javascript JavaScript/TypeScript theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  // Using client and variables from previous examples
  const result = await client.records.aggregate(streamId, {
    lastUpdatedTime: { gt: '2025-10-01T00:00:00.000Z' },
    filter: {
      hasData: [{ type: 'container' as const, space: spaceId, externalId: containerId }],
    },
    aggregates: {
      events_over_time: {
        timeHistogram: {
          property: [spaceId, containerId, 'recorded_at'],
          calendarInterval: '1d',
          aggregates: {
            by_severity_band: {
              filters: {
                filters: [
                  {
                    equals: {
                      property: [spaceId, containerId, 'severity'],
                      value: 'LOW',
                    },
                  },
                  {
                    equals: {
                      property: [spaceId, containerId, 'severity'],
                      value: 'MEDIUM',
                    },
                  },
                  {
                    or: [
                      {
                        equals: {
                          property: [spaceId, containerId, 'severity'],
                          value: 'HIGH',
                        },
                      },
                      {
                        equals: {
                          property: [spaceId, containerId, 'severity'],
                          value: 'CRITICAL',
                        },
                      },
                    ],
                  },
                ],
              },
            },
            trend: {
              movingFunction: {
                bucketsPath: '_count',
                window: 3,
                function: 'MovingFunctions.unweightedAvg',
              },
            },
          },
        },
      },
    },
  });

  const buckets = result.events_over_time.timeHistogramBuckets;
  const bandLabels = ['LOW', 'MEDIUM', 'HIGH+CRITICAL'];
  buckets.forEach((bucket) => {
    console.log(`${bucket.intervalStart}: ${bucket.count} events, trend: ${bucket.aggregates.trend.fnValue}`);
    bucket.aggregates.by_severity_band.filterBuckets.forEach((band, i) => {
      console.log(`  ${bandLabels[i]}: ${band.count}`);
    });
  });
  ```

  ```json Example response theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
  {
    "aggregates": {
      "events_over_time": {
        "timeHistogramBuckets": [
          {
            "intervalStart": "2025-10-21T00:00:00.000Z",
            "count": 180,
            "aggregates": {
              "by_severity_band": {
                "filterBuckets": [
                  { "count": 85 },
                  { "count": 62 },
                  { "count": 33 }
                ]
              },
              "trend": { "fnValue": 180.0 }
            }
          },
          {
            "intervalStart": "2025-10-22T00:00:00.000Z",
            "count": 210,
            "aggregates": {
              "by_severity_band": {
                "filterBuckets": [
                  { "count": 95 },
                  { "count": 72 },
                  { "count": 43 }
                ]
              },
              "trend": { "fnValue": 195.0 }
            }
          },
          {
            "intervalStart": "2025-10-23T00:00:00.000Z",
            "count": 165,
            "aggregates": {
              "by_severity_band": {
                "filterBuckets": [
                  { "count": 78 },
                  { "count": 58 },
                  { "count": 29 }
                ]
              },
              "trend": { "fnValue": 185.0 }
            }
          },
          {
            "intervalStart": "2025-10-24T00:00:00.000Z",
            "count": 195,
            "aggregates": {
              "by_severity_band": {
                "filterBuckets": [
                  { "count": 88 },
                  { "count": 68 },
                  { "count": 39 }
                ]
              },
              "trend": { "fnValue": 190.0 }
            }
          }
        ]
      }
    }
  }
  ```
</CodeGroup>

In this response, each day's bucket contains three severity bands (LOW, MEDIUM, HIGH+CRITICAL) and a 3-day moving average trend. The `filterBuckets` are ordered to match the filter array: index 0 is LOW, index 1 is MEDIUM, and index 2 is HIGH+CRITICAL.

## Limits and constraints

The aggregate endpoint enforces limits on request complexity to ensure consistent performance. These limits apply to the aggregate tree structure, individual aggregate parameters, and the overall request.

### Aggregate tree limits

| Constraint                      | Value                           |
| ------------------------------- | ------------------------------- |
| Max aggregates per level        | 5                               |
| Max aggregate tree depth        | 5                               |
| Max total aggregates in request | 16                              |
| Max total buckets per request   | 10,000                          |
| Aggregate name length           | 1-255 characters                |
| Aggregate name pattern          | Cannot contain `[`, `]`, or `>` |

### Aggregate-specific limits

| Aggregate        | Constraint                                           | Value                                                                                                         |
| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `uniqueValues`   | `size` range                                         | 1-2000 (default: 10)                                                                                          |
| `filters`        | Filters per aggregate                                | 1-10                                                                                                          |
| `filters`        | Total filter buckets across all `filters` aggregates | 30                                                                                                            |
| `movingFunction` | Minimum `window`                                     | 1                                                                                                             |
| `movingFunction` | `bucketsPath` length                                 | 1-1280 characters                                                                                             |
| `timeHistogram`  | `calendarInterval` values                            | `1s` (second), `1m` (minute), `1h` (hour), `1d` (day), `1w` (week), `1M` (month), `1q` (quarter), `1y` (year) |
| `timeHistogram`  | `fixedInterval` units                                | `ms`, `s`, `m`, `h`, `d` (any positive multiplier, e.g., `30m`, `6h`)                                         |
| `timeHistogram`  | `fixedInterval` length                               | 1-100 characters                                                                                              |

### General request limits

| Constraint             | Value                                                                                                                                                    |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lastUpdatedTime`      | Required for immutable streams, optional for mutable                                                                                                     |
| `maxFilteringInterval` | Maximum time range between `gt` and `lt` in `lastUpdatedTime`. Defined by the stream template (e.g., `P1Y` for one year). Retrieve from stream settings. |
| Rate limits            | Aggregate requests have stricter rate limits than filter or sync requests. See [API rate limits](/api-reference/concepts/20230101/rate-limits).          |

<Tip>
  If you need more than 16 aggregates or deeper nesting, split your analysis across multiple requests. Each request gets a consistent snapshot of the data, so results from separate requests made close together will be comparable.
</Tip>

## Common patterns

### Trend analysis

Combine `timeHistogram` with `movingFunction` to smooth noisy time-series data and identify trends:

```json theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
{
  "aggregates": {
    "daily_trend": {
      "timeHistogram": {
        "property": ["factory-data", "equipment_events", "recorded_at"],
        "calendarInterval": "1d",
        "aggregates": {
          "avg_temp": {
            "avg": { "property": ["factory-data", "equipment_events", "temperature"] }
          },
          "smoothed_temp": {
            "movingFunction": {
              "bucketsPath": "avg_temp",
              "window": 7,
              "function": "MovingFunctions.unweightedAvg"
            }
          }
        }
      }
    }
  }
}
```

This computes a 7-day moving average of daily temperatures, smoothing out day-to-day variation to reveal the underlying trend.

### Distribution analysis

Use `uniqueValues` with nested metrics to understand how a categorical dimension relates to numeric measures:

```json theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
{
  "aggregates": {
    "equipment_analysis": {
      "uniqueValues": {
        "property": ["factory-data", "equipment_events", "equipment_id"],
        "size": 50,
        "aggregates": {
          "event_count": { "count": {} },
          "avg_priority": {
            "avg": { "property": ["factory-data", "equipment_events", "priority"] }
          },
          "max_temp": {
            "max": { "property": ["factory-data", "equipment_events", "temperature"] }
          }
        }
      }
    }
  }
}
```

### Threshold monitoring

Use `filters` to count records in specific value ranges for threshold-based alerting:

```json theme={"languages":{"custom":["/_languages/kuiper.json","../_languages/kuiper.json"]}}
{
  "aggregates": {
    "status_check": {
      "filters": {
        "filters": [
          {
            "and": [
              { "equals": { "property": ["factory-data", "equipment_events", "is_critical"], "value": true } },
              { "equals": { "property": ["factory-data", "equipment_events", "severity"], "value": "CRITICAL" } }
            ]
          },
          {
            "range": {
              "property": ["factory-data", "equipment_events", "temperature"],
              "gte": 120
            }
          }
        ]
      }
    }
  }
}
```

## Further reading

* [About records and streams](/cdf/dm/records/concepts/records_and_streams) — Core concepts for records, streams, and containers
* [Get started with Records](/cdf/dm/records/guides/records_getting_started) — Hands-on tutorial for setting up schemas, streams, and queries
* [Filter syntax](/cdf/dm/dm_concepts/dm_querying) — Filter expressions used with the `filter` parameter and the `filters` aggregate
* [Limits and restrictions](/cdf/dm/dm_reference/dm_limits_and_restrictions) — Full list of Records API limits
