Skip to main content
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:
CategoryAggregatesDescription
Metriccount, avg, sum, min, maxCompute a single numeric value from a set of records
BucketuniqueValues, numberHistogram, timeHistogram, filtersGroup records into buckets based on property values, numeric ranges, time intervals, or filter conditions
PipelinemovingFunctionCompute 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.
PropertyTypeDescription
equipment_idtextEquipment identifier (e.g., PUMP-001, MOTOR-042)
locationtextFacility location (e.g., Building-A, Building-B)
temperaturefloat64Measured temperature in degrees Celsius
severitytextEvent severity level (LOW, MEDIUM, HIGH, CRITICAL)
priorityint64Priority score from 1 (lowest) to 100 (highest)
is_criticalbooleanWhether the event requires immediate attention
recorded_attimestampWhen the event was recorded
To learn how to create containers, set up streams, and ingest records, see Get started with Records.

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.
Example request structure
{
  "lastUpdatedTime": {
    "gt": "2025-10-01T00:00:00.000Z"
  },
  "filter": {
    "hasData": [
      {
        "type": "container",
        "space": "factory-data",
        "externalId": "equipment_events"
      }
    ]
  },
  "aggregates": {
    "my_aggregate_name": {
      "<aggregate_type>": { ... }
    }
  }
}
Time-based filtering with lastUpdatedTimeThe 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 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 for details and examples.For mutable streams, lastUpdatedTime is optional, but using it improves query performance.

Property paths

Aggregations reference container properties using an array of three strings: the space, the container external ID, and the property name.
"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.
Null value handlingFor 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.

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.
curl
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"]
        }
      }
    }
  }'
Example response
{
  "aggregates": {
    "total_events": {
      "count": 1250
    },
    "events_with_temperature": {
      "count": 1183
    }
  }
}

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
curl
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"]
        }
      }
    }
  }'
Example response
{
  "aggregates": {
    "avg_temperature": {
      "avg": 72.45
    }
  }
}

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
curl
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"]
        }
      }
    }
  }'
Example response
{
  "aggregates": {
    "total_priority": {
      "sum": 58420
    }
  }
}

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
curl
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"]
        }
      }
    }
  }'
Example response
{
  "aggregates": {
    "coldest_reading": {
      "min": 18.3
    },
    "earliest_event": {
      "min": "2025-10-01T08:12:33.000Z"
    }
  }
}

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
curl
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"]
        }
      }
    }
  }'
Example response
{
  "aggregates": {
    "hottest_reading": {
      "max": 142.7
    },
    "latest_event": {
      "max": "2025-10-24T16:45:12.000Z"
    }
  }
}

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.
curl
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"] } }
    }
  }'
Example response
{
  "aggregates": {
    "total_events": { "count": 312 },
    "avg_temp": { "avg": 104.8 },
    "min_temp": { "min": 85.9 },
    "max_temp": { "max": 142.7 },
    "total_priority": { "sum": 15230 }
  }
}
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 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.
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:
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.

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

ParameterTypeRequiredDescription
propertyarrayYesThe property to group by. Supports text, numeric, boolean properties, and the top-level ["space"] path.
sizeintegerNoMaximum number of buckets to return. Range: 1-2000. Default: 10.
aggregatesobjectNoNested sub-aggregates to compute within each bucket.
Ordering and size behaviorBuckets 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 countsCounts 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 valuesRecords 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.

Example: severity distribution with nested average

curl
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"]
              }
            }
          }
        }
      }
    }
  }'
Example response
{
  "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 } }
        }
      ]
    }
  }
}

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).
numberHistogram is similar to binning a numeric column and grouping by the bins in SQL or pandas/Polars:
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().

Parameters

ParameterTypeRequiredDescription
propertyarrayYesThe numeric property to bucket. Supports int32, int64, float32, float64.
intervalnumberYesThe width of each bucket. Must be a positive number.
hardBoundsobjectNoLimits the range of buckets. Has optional min and max fields. Buckets outside these bounds are excluded from the response, even if matching records exist.
aggregatesobjectNoNested sub-aggregates to compute within each bucket.
Empty bucketsBy 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. filtersUse 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).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 valuesRecords that do not have the specified property (or have a null value) are excluded from the histogram. They do not contribute to any bucket.

Example: temperature distribution

curl
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 }
        }
      }
    }
  }'
Example response
{
  "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 }
      ]
    }
  }
}

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.
timeHistogram is similar to truncating timestamps and grouping by the truncated value in SQL or pandas/Polars:
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()).
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

ParameterTypeRequiredDescription
propertyarrayYesThe timestamp property to bucket. Also supports ["createdTime"] and ["lastUpdatedTime"].
calendarIntervalstringOne ofCalendar-aware interval. Allowed values: 1s (second), 1m (minute), 1h (hour), 1d (day), 1w (week), 1M (month), 1q (quarter), 1y (year).
fixedIntervalstringOne ofFixed-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).
hardBoundsobjectNoLimits the range of time buckets. Has optional min and max fields as ISO 8601 timestamps.
aggregatesobjectNoNested sub-aggregates to compute within each bucket.
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").
The following details describe how the timeHistogram handles edge cases such as empty intervals, bounded time ranges, and missing data.
Empty bucketsLike 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 rangesUse hardBounds with min and max (ISO 8601 timestamps) to limit which time buckets appear in the response. This is useful when you want a fixed time window regardless of where your data starts and ends. Like numberHistogram, hardBounds controls only which buckets are returned, it does not filter which records are aggregated.Missing valuesRecords 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.

Example: hourly event counts

curl
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"
        }
      }
    }
  }'
Example response
{
  "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 }
      ]
    }
  }
}

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.
The filters aggregate is similar to conditional counting in a single query in SQL or pandas/Polars:
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.
Each filter uses the same filter syntax available on the filter and sync endpoints.

Parameters

ParameterTypeRequiredDescription
filtersarrayYesAn array of 1-10 filter expressions. Each filter defines one bucket.
aggregatesobjectNoNested sub-aggregates to compute within each bucket.
Bucket orderingBuckets 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 filtersA 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).LimitsA 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.

Example: temperature severity bands

curl
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
              }
            }
          ]
        }
      }
    }
  }'
Example response
{
  "aggregates": {
    "temp_bands": {
      "filterBuckets": [
        { "count": 57 },
        { "count": 590 },
        { "count": 524 },
        { "count": 79 }
      ]
    }
  }
}
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.
movingFunction is similar to SQL window functions with a frame clause, .rolling() in pandas, or .rolling_mean() in Polars:
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)).
The movingFunction aggregate must be nested inside a numberHistogram or timeHistogram. It cannot be used as a standalone aggregate.

Parameters

ParameterTypeRequiredDescription
bucketsPathstringYesPath to the metric to use as input. Use _count for the bucket record count, or the name of a sibling metric aggregate.
windowintegerYesSize of the sliding window in number of buckets. Must be at least 1.
functionstringYesThe function to apply over the window.

Available functions

FunctionDescription
MovingFunctions.unweightedAvgSimple average of all values in the window. The most common choice for trend smoothing.
MovingFunctions.linearWeightedAvgWeighted average where more recent values have higher weight. Gives a trend line that responds faster to recent changes.
MovingFunctions.sumSum of all values in the window. Useful for calculating rolling totals.
MovingFunctions.minMinimum value in the window.
MovingFunctions.maxMaximum value in the window.
Window warm-upThe 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 dataWhen 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-aggregatePipeline 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.

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:
PathDescription
_countThe record count of each bucket. Use this when you want to apply the moving function to the number of records per bucket.
my_metricThe 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_metricA 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]>metricA 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

curl
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"
              }
            }
          }
        }
      }
    }
  }'
Example response
{
  "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 } }
        }
      ]
    }
  }
}
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.
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:
SELECT location, severity, COUNT(*) AS count, AVG(temperature) AS avg_temp
FROM equipment_events
GROUP BY location, severity
ORDER BY location, count DESC
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.
While nesting is powerful, deep nesting with high-cardinality data can be expensive. Keep these performance considerations in mind.
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.

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?”
curl
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"]
              }
            }
          }
        }
      }
    }
  }'
Example response
{
  "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 }
              ]
            }
          }
        }
      ]
    }
  }
}

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.
curl
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"
              }
            }
          }
        }
      }
    }
  }'
Example response
{
  "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 }
          }
        }
      ]
    }
  }
}
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

ConstraintValue
Max aggregates per level5
Max aggregate tree depth5
Max total aggregates in request16
Max total buckets per request10,000
Aggregate name length1-255 characters
Aggregate name patternCannot contain [, ], or >

Aggregate-specific limits

AggregateConstraintValue
uniqueValuessize range1-2000 (default: 10)
filtersFilters per aggregate1-10
filtersTotal filter buckets across all filters aggregates30
movingFunctionMinimum window1
movingFunctionbucketsPath length1-1280 characters
timeHistogramcalendarInterval values1s (second), 1m (minute), 1h (hour), 1d (day), 1w (week), 1M (month), 1q (quarter), 1y (year)
timeHistogramfixedInterval unitsms, s, m, h, d (any positive multiplier, e.g., 30m, 6h)
timeHistogramfixedInterval length1-100 characters

General request limits

ConstraintValue
lastUpdatedTimeRequired for immutable streams, optional for mutable
maxFilteringIntervalMaximum time range between gt and lt in lastUpdatedTime. Defined by the stream template (e.g., P1Y for one year). Retrieve from stream settings.
Rate limitsAggregate requests have stricter rate limits than filter or sync requests. See resource throttling.
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.

Common patterns

Trend analysis

Combine timeHistogram with movingFunction to smooth noisy time-series data and identify trends:
{
  "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:
{
  "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:
{
  "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

Last modified on February 19, 2026