---
title: nexyron.temporal_bucket_aggregate
url: https://nexyron.tech/docs/cypher/procedures/temporal-and-streaming/nexyron-temporal-bucket-aggregate
description: Aggregate one declared temporal property across all subjects, or across a selected subject list, into calendar or fixed width time buckets without first materializing every tempora
---

# nexyron.temporal_bucket_aggregate

## Generic Description

Aggregate one declared temporal property across all subjects, or across a selected subject list, into calendar or fixed-width time buckets without first materializing every temporal point as a Cypher row.

Simple example:

```cypher
CALL nexyron.temporal_bucket_aggregate(
  'User',
  'payments',
  'month',
  'sum',
  'amount',
  null,
  null,
  null
)
YIELD bucket_start, bucket_end, value
RETURN bucket_start, bucket_end, value
ORDER BY bucket_start
```

Arguments:

-   `label`: owning node label for the declared temporal property.
-   `property`: declared temporal property name.
-   `bucket`: `year`, `month`, `week`, `day`, or a positive fixed bucket width in milliseconds.
-   `aggregate`: `count`, `sum`, `min`, `max`, `avg`, `first`, or `last`.
-   `value_path`: optional dot path inside object values, such as `amount` or `payment.amount`; use `null` for scalar temporal values.
-   `from_ts`: optional inclusive lower bound in epoch milliseconds.
-   `to_ts`: optional inclusive upper bound in epoch milliseconds.
-   `owners`: optional list of internal owner node IDs; use `null` to aggregate every subject that has the declared label/property stream.

Returned columns:

-   `bucket_start`: epoch-millisecond bucket start.
-   `bucket_end`: exclusive epoch-millisecond bucket end.
-   `value`: aggregate result for the bucket.
-   `sample_count`: number of samples included. For numeric aggregates this means numeric samples; for `count` this means records where `value_path` exists, or non-null scalar records when `value_path` is `null`.
-   `subject_count`: number of distinct owner nodes that contributed to the bucket.

## Consumer-Level Explanation

Use this procedure when the business question is naturally phrased as "sum/count/average this temporal metric per year, month, week, or day" and the data is stored as a declared temporal property.

The important difference from `nexyron.temporal_history` or `nexyron.temporal_rollup` is scope. `temporal_history(node_id, ...)` and `temporal_rollup(node_id, ...)` read one owner at a time. That is useful for one account, one device, or one user. It is not the clean shape for "all users by month" because the caller would have to discover owners, loop over them, and then aggregate outside the temporal store.

`nexyron.temporal_bucket_aggregate` keeps that work inside the temporal store. The caller names the label, property, bucket, aggregate, and optional value path. If `owners` is `null`, Nexyron aggregates all subjects that have that declared temporal property. If `owners` is supplied, Nexyron aggregates only those subjects.

For business users, this means:

-   "monthly payment amount across all users" does not require passing every user ID.
-   "weekly payment amount for this segment" can pass only the selected owner IDs.
-   missing `from_ts` and `to_ts` means "use the stored data range", not "guess from wall-clock now".
-   the result is already one row per bucket, so normal Cypher math can adjust it.

## Conceptual Explanation

This procedure fills the gap between one-subject temporal rollups and general Cypher aggregation.

Normal Cypher aggregation is excellent after a query has already produced rows. One-subject temporal rollups are excellent when a known owner already has materialized buckets. Cross-owner business reporting needs a different shape: the engine should read the temporal store directly, group by the requested time bucket, and only return the small bucket result.

The procedure keeps the row grain explicit:

```text
one returned row = one populated time bucket for one declared temporal property
```

That grain is useful for reports, charts, feature generation, assistant-authored analytics, and downstream calculations such as dividing every bucket by a denominator from another query.

Calendar buckets are UTC calendar buckets. `month` means real calendar months such as January and February, not a fixed 30-day duration. `week` uses ISO-style Monday-start weeks.

## More Detailed Explanation

The procedure scans the temporal-property store directly and maintains one compact aggregate state per output bucket. It does not build a list of all matching history rows, and it does not keep per-subject row buffers.

The memory shape is:

```text
bucket_start -> aggregate_state
```

For a ten-year monthly report this means roughly 120 states, plus a distinct-subject set per populated bucket for `subject_count`. It does not mean one in-memory object per payment row in the final aggregation layer.

The execution path uses the existing declared temporal-property storage:

1.  Resolve `label.property` in the temporal property store.
2.  Iterate only retained subjects for that property, or only subjects in the optional owner filter.
3.  Use sorted per-subject history vectors to skip records outside `from_ts` and `to_ts`.
4.  Extract the scalar value directly from the temporal value or from `value_path`.
5.  Put the timestamp into a calendar bucket (`year`, `month`, `week`, `day`) or fixed millisecond bucket.
6.  Update the aggregate state in place.
7.  Return finalized bucket rows ordered by `bucket_start`.

## Advanced Example

This example calculates three monthly payment totals and then divides each monthly total by four. The division happens after the bucket aggregate, so the procedure still returns a compact bucket series first.

```cypher
WITH datetime('2026-01-01T00:00:00Z') AS from_ts,
     datetime('2026-04-01T00:00:00Z') AS to_ts
CALL nexyron.temporal_bucket_aggregate(
  'User',
  'payments',
  'month',
  'sum',
  'amount',
  from_ts,
  to_ts,
  null
)
YIELD bucket_start, bucket_end, value, sample_count, subject_count
RETURN bucket_start,
       bucket_end,
       value AS monthly_payment_sum,
       value / 4.0 AS monthly_payment_sum_divided_by_4,
       sample_count,
       subject_count
ORDER BY bucket_start
```

This example applies a denominator from another graph query. The denominator is computed once, then applied to every returned bucket.

```cypher
MATCH (:Company {id: $company_id})<-[:BELONGS_TO]-(u:User)
WITH collect(id(u)) AS owners
CALL {
  MATCH (p:Plan {id: $plan_id})
  RETURN coalesce(p.normalization_divisor, 1.0) AS divisor
}
WITH owners, divisor
CALL nexyron.temporal_bucket_aggregate(
  'User',
  'payments',
  'month',
  'sum',
  'amount',
  null,
  null,
  owners
)
YIELD bucket_start, value, sample_count, subject_count
RETURN bucket_start,
       value AS selected_users_monthly_sum,
       value / divisor AS normalized_monthly_sum,
       sample_count,
       subject_count
ORDER BY bucket_start
```

This example uses scalar temporal values instead of object values. Because the temporal value itself is numeric, `value_path` is `null`.

```cypher
CALL nexyron.temporal_bucket_aggregate(
  'Account',
  'balance_delta',
  'week',
  'avg',
  null,
  datetime('2026-01-01T00:00:00Z'),
  datetime('2026-03-01T00:00:00Z'),
  null
)
YIELD bucket_start, value, sample_count, subject_count
RETURN bucket_start,
       value AS avg_weekly_balance_delta,
       sample_count,
       subject_count
ORDER BY bucket_start
```

## Real Use Cases

-   Revenue and payment reporting: aggregate `User.payments.amount` by month across all users without first returning every payment row to Cypher.
-   Subscription cohorts: match a selected customer segment, pass `collect(id(u))` as `owners`, and return weekly or monthly totals for only that segment.
-   Account balance monitoring: aggregate scalar balance deltas by day or week to detect cash-flow changes without modeling every delta as a visible graph node.
-   Operational event volumes: count numeric event flags or quantities by day across all devices, stores, or accounts that own a declared temporal property.
-   Normalized business metrics: compute bucket totals first, then divide each bucket by a fixed value, a plan property, or a scalar produced by another query.

## Real Limitations And Tradeoffs

-   The procedure currently targets declared temporal properties. It does not aggregate arbitrary relationship timestamp properties, hidden event streams, or graph-visible event nodes unless those facts are also stored as temporal properties.
-   For `sum`, `min`, `max`, `avg`, `first`, and `last`, `value_path` must point to a numeric scalar. Non-numeric values are skipped for numeric aggregates, so an incorrect path can return empty or undercounted buckets. For `count`, the path only needs to exist.
-   `owners` uses internal owner node IDs, usually produced by `id(u)` in the same query. Passing `null` is the intended shape for all-subject aggregation.
-   Calendar buckets use UTC. If a business needs region-specific wall-clock calendars, that should be added explicitly rather than silently applying local machine time.
-   `subject_count` keeps distinct owner IDs per bucket. That is useful for business coverage reporting, but it is the one part of the result state that grows with the number of contributing subjects per bucket.
-   Existing fixed-width temporal rollups still serve one-owner rollup reads. This procedure is for cross-owner bucket aggregation and does not replace `nexyron.temporal_rollup` where the query is about one subject's stored rollup buckets.
