## Run a report query

`client.reports.query(ReportQueryParamsparams, RequestOptionsoptions?): ReportQueryResponse`

**post** `/v2/reports/query`

Executes an ad-hoc reporting query: one metric + aggregation over a date range, with optional filters, grouping, segmentation and period-over-period comparison.

### Building a query

1. Pick a `metric` and `aggregation` from the datasets catalog (`GET /v2/reports/datasets`). Counts use `count`; duration metrics support `sum`, `avg`, `median`, `min`, `max`, `range` and `percentile` (pass `percentile: 95` for p95); rate metrics use `value`.
1. Set the reporting window with `startDate` / `endDate` (ISO 8601) and a `granularity` (`hour`, `day`, `week`, `month`) for the returned time series. Reporting data is available from **July 12, 2026**. Windows ending earlier are rejected; crossing windows are clamped to that boundary in the requested timezone.
1. Optionally narrow with `filters` — a rule (`{ "kind": "rule", "fieldId": "...", "operator": "in", "value": [...] }`) or an `and`/`or` group of rules. Attribute IDs and allowed operators come from the catalog.
1. Optionally break results down with `groupBy` (primary dimension) and `segmentBy` (secondary dimension).
1. Optionally pass `compareStartDate` / `compareEndDate` to get `previousValue` / `deltaPercent` alongside every data point.

### Top-N and Show Other

For high-cardinality breakdowns, cap the number of returned series:

- `topValuesLimit` keeps only the top **N** `groupBy` values; `segmentTopValuesLimit` does the same for `segmentBy`. Allowed values: **5, 7, 10, 15, 20**. `topValuesLimit` requires `groupBy`; `segmentTopValuesLimit` requires `segmentBy`.
- Members are ranked by the selected metric/aggregation over the **full filtered primary range**, so the ranked set is **stable across every time bucket** (a value that spikes in one bucket but is small overall does not enter the set). A comparison period reuses the **primary period's** ranked set — it is never re-ranked, so the legend stays identical period-over-period.
- `showOther` / `segmentShowOther` append a single synthetic bucket with id `__other__` and label `Other` that sums every value outside the top set. Because the values are summed, Show Other is only supported for **additive aggregations** (`count`, `sum`, `value`); it is rejected for `avg`, `median`, `min`, `max`, `range`, `percentile`, and for percentage metrics (whose ratios cannot be summed). `showOther` also requires `topValuesLimit` (and `segmentShowOther` requires `segmentTopValuesLimit`). Plain `topValuesLimit` (without Show Other) works with any aggregation.
- The `__other__` group/segment aggregates rows outside the selected top groups and **cannot be drilled into** (`POST /v2/reports/drill-in` rejects `dataPointFilters.groupValue` / `segmentValue` equal to `__other__`).

### Example

```json
{
  "metric": "new_conversations",
  "aggregation": "count",
  "startDate": "2026-07-12",
  "endDate": "2026-07-15",
  "granularity": "day",
  "groupBy": "conversation.channel",
  "filters": {
    "kind": "rule",
    "fieldId": "conversation.state",
    "operator": "is",
    "value": "closed"
  }
}
```

### Response shape

- `value` — the aggregate across the whole window
- `timeSeries` — one datum per granularity bucket
- `groupedData` / `segmentData` — present when `groupBy` / `segmentBy` were requested
- `flowData` — present for `view: "sankey"` (Overview conversation-flow metrics)
- `meta` — echo of the resolved metric, dataset, unit and aggregation

### Special views

- `view: "hourly_heatmap"` buckets by day-of-week × hour-of-day (use with volume metrics)
- `view: "sankey"` returns conversation flow edges (Overview metrics only)

### Version Availability

This endpoint is only available in API version 2026-01-01.nova and newer, and only for workspaces with the Reports product enabled (404 otherwise).

### Parameters

- `params: ReportQueryParams`

  - `aggregation: "count" | "sum" | "avg" | 6 more`

    Body param: Aggregation function applied to the metric. Each metric supports a subset of aggregations — see the `allowedAggregations` field in the `GET /v2/reports/datasets` catalog.

    - `"count"`

    - `"sum"`

    - `"avg"`

    - `"median"`

    - `"min"`

    - `"max"`

    - `"range"`

    - `"percentile"`

    - `"value"`

  - `endDate: string`

    Body param: End of the reporting window (ISO 8601 date or datetime, inclusive). Windows ending before 2026-07-12 are rejected.

  - `granularity: "hour" | "day" | "week" | "month"`

    Body param: Time bucket size for the returned time series.

    - `"hour"`

    - `"day"`

    - `"week"`

    - `"month"`

  - `metric: string`

    Body param: Metric ID to query (e.g. `new_conversations`). Discover metric IDs via `GET /v2/reports/datasets`.

  - `startDate: string`

    Body param: Start of the reporting window (ISO 8601 date or datetime, inclusive). Reporting data is available from 2026-07-12; crossing windows are clamped to that boundary.

  - `compareEndDate?: string`

    Body param: End of the comparison window. Must be paired with `compareStartDate`.

  - `compareStartDate?: string`

    Body param: Start of the comparison window for period-over-period deltas. Must be paired with `compareEndDate`.

  - `filters?: ReportFilterRule | ReportFilterExpression`

    Body param: Filter expression: a single rule, or an `and`/`or` group combining rules and nested groups. Attribute IDs and their allowed operators come from `GET /v2/reports/datasets`.

    - `ReportFilterRule`

      - `fieldId: string`

        Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

      - `kind: "rule"`

        - `"rule"`

      - `operator: "is" | "is_not" | "in" | 10 more`

        Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

        - `"is"`

        - `"is_not"`

        - `"in"`

        - `"not_in"`

        - `"contains"`

        - `"not_contains"`

        - `"gte"`

        - `"lte"`

        - `"between"`

        - `"exists"`

        - `"not_exists"`

        - `"is_member_of"`

        - `"is_not_member_of"`

      - `value?: string | number | boolean | Array<string | number | boolean>`

        Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

        - `string`

        - `number`

        - `boolean`

        - `Array<string | number | boolean>`

          - `string`

          - `number`

          - `boolean`

    - `ReportFilterExpression`

      - `children: Array<ReportFilterRule | ReportFilterGroup>`

        Rules and/or nested groups of rules. Groups may nest up to 6 levels at runtime; 100 rules max per expression.

        - `ReportFilterRule`

        - `ReportFilterGroup`

          - `children: Array<ReportFilterRule>`

            - `fieldId: string`

              Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

            - `kind: "rule"`

            - `operator: "is" | "is_not" | "in" | 10 more`

              Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

            - `value?: string | number | boolean | Array<string | number | boolean>`

              Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

          - `kind: "group"`

            - `"group"`

          - `op: "and" | "or"`

            Group operator: `and` (all match) or `or` (any match).

            - `"and"`

            - `"or"`

      - `kind: "group"`

        - `"group"`

      - `op: "and" | "or"`

        Group operator: `and` (all match) or `or` (any match).

        - `"and"`

        - `"or"`

  - `groupBy?: string`

    Body param: Attribute ID to group results by (e.g. `conversation.channel`). See `supportsGroupBy` in the catalog.

  - `officeHoursOnly?: boolean`

    Body param: Restrict time-based metrics to configured office hours. Only supported by some metrics (see `supportsOfficeHours` in the catalog).

  - `percentile?: number`

    Body param: Percentile (1-100) — required when `aggregation` is `percentile`.

  - `segmentBy?: string`

    Body param: Attribute ID for secondary segmentation within each group or time bucket.

  - `segmentShowOther?: boolean`

    Body param: When `true`, fold `segmentBy` values outside the top set into a synthetic `__other__` / `Other` segment. Only supported for additive aggregations (`count`, `sum`, `value`) and not for percentage metrics. The `__other__` segment cannot be drilled into. Requires `segmentBy` and `segmentTopValuesLimit`.

  - `segmentTopValuesLimit?: 5 | 7 | 10 | 2 more`

    Body param: Keep only the top N `segmentBy` values per series, ranked by the selected metric over the full filtered primary range. Under a time View-by the ranked segment set is identical across every bucket; under a dimension View-by the ranking is applied within each retained parent group. Allowed values: `5`, `7`, `10`, `15`, `20`. Requires `segmentBy`.

    - `5`

    - `7`

    - `10`

    - `15`

    - `20`

  - `showOther?: boolean`

    Body param: When `true`, append a single synthetic group with id `__other__` and label `Other` that sums the values of every `groupBy` value outside the top set. Only supported for additive aggregations (`count`, `sum`, `value`) and not for percentage metrics. The `__other__` group cannot be drilled into. Requires `groupBy` and `topValuesLimit`.

  - `timezone?: string`

    Body param: IANA timezone for date bucketing (e.g. `America/New_York`). Defaults to UTC.

  - `topValuesLimit?: 5 | 7 | 10 | 2 more`

    Body param: Keep only the top N `groupBy` (View-by) values, ranked by the selected metric/aggregation over the full filtered primary range. Allowed values: `5`, `7`, `10`, `15`, `20`. The ranked set is stable across every time bucket, and a comparison period reuses the primary period's ranked set (never re-ranked). Requires `groupBy`.

    - `5`

    - `7`

    - `10`

    - `15`

    - `20`

  - `view?: "standard" | "hourly_heatmap" | "sankey"`

    Body param: Result shape. `standard` returns a time series (plus grouped/segment data when requested), `hourly_heatmap` buckets by day-of-week × hour-of-day, `sankey` returns conversation flow data. Each metric lists its `supportedViews` in the `GET /v2/reports/datasets` catalog.

    - `"standard"`

    - `"hourly_heatmap"`

    - `"sankey"`

  - `featurebaseVersion?: "2026-01-01.nova" | "2025-12-12.clover"`

    Header param: API version for this request. Defaults to your organization's configured API version if not specified.

    - `"2026-01-01.nova"`

    - `"2025-12-12.clover"`

### Returns

- `ReportQueryResponse`

  - `meta: Meta`

    - `aggregation: "count" | "sum" | "avg" | 6 more`

      Aggregation function applied to the metric. Each metric supports a subset of aggregations — see the `allowedAggregations` field in the `GET /v2/reports/datasets` catalog.

      - `"count"`

      - `"sum"`

      - `"avg"`

      - `"median"`

      - `"min"`

      - `"max"`

      - `"range"`

      - `"percentile"`

      - `"value"`

    - `datasetId: string`

    - `granularity: string`

    - `metric: string`

    - `officeHoursOnly: boolean`

    - `unit: "count" | "percentage" | "duration_ms" | "number"`

      - `"count"`

      - `"percentage"`

      - `"duration_ms"`

      - `"number"`

    - `groupBy?: string`

    - `segmentBy?: string`

  - `object: "report_query_result"`

    - `"report_query_result"`

  - `value: number`

    Aggregated value across the whole reporting window.

  - `deltaPercent?: number`

    Percentage change vs the comparison window.

  - `flowData?: Array<FlowData>`

    - `metricId: string`

    - `pathId: string`

    - `source: string`

    - `sourceLabel: string`

    - `target: string`

    - `targetLabel: string`

    - `value: number`

    - `colorKey?: string`

    - `percentage?: number`

    - `sortOrder?: number`

  - `groupedData?: Array<ReportGroupedDatum>`

    - `group: string`

      Raw group value (e.g. an ID). Use `groupLabel` for display.

    - `value: number`

    - `groupLabel?: string`

    - `previousValue?: number`

    - `segments?: Array<ReportSegmentDatum>`

      - `segment: string`

      - `value: number`

      - `previousValue?: number`

      - `segmentLabel?: string`

  - `previousValue?: number`

    Aggregated value for the comparison window, when requested.

  - `segmentData?: Array<ReportSegmentDatum>`

    - `segment: string`

    - `value: number`

    - `previousValue?: number`

    - `segmentLabel?: string`

  - `table?: Table`

    - `columns: Array<Column>`

      - `id: string`

      - `kind: "dimension" | "metric" | "record_attribute"`

        - `"dimension"`

        - `"metric"`

        - `"record_attribute"`

      - `label: string`

      - `sortable: boolean`

      - `unit: "count" | "percentage" | "duration_ms" | 3 more`

        - `"count"`

        - `"percentage"`

        - `"duration_ms"`

        - `"number"`

        - `"text"`

        - `"datetime"`

      - `align?: "left" | "right"`

        - `"left"`

        - `"right"`

      - `sticky?: boolean`

      - `summary?: "sum" | "weighted_rate" | "none"`

        - `"sum"`

        - `"weighted_rate"`

        - `"none"`

      - `valueType?: string`

    - `key: string`

    - `mode: "aggregate" | "records"`

      - `"aggregate"`

      - `"records"`

    - `page: number`

    - `pageSize: number`

    - `rows: Array<Row>`

      - `id: string`

      - `cells: Record<string, Cells>`

        - `display: string`

        - `denominator?: number`

        - `numerator?: number`

        - `raw?: unknown`

        - `sortValue?: string | number | null`

          - `string`

          - `number`

        - `value?: unknown`

    - `totalRows: number`

    - `sort?: Sort`

      - `columnId: string`

      - `direction: "asc" | "desc"`

        - `"asc"`

        - `"desc"`

    - `summaryRows?: Array<SummaryRow>`

      - `id: string`

      - `cells: Record<string, Cells>`

        - `display: string`

        - `denominator?: number`

        - `numerator?: number`

        - `raw?: unknown`

        - `sortValue?: string | number | null`

          - `string`

          - `number`

        - `value?: unknown`

  - `timeSeries?: Array<ReportTimeSeriesDatum>`

    - `date: string`

      Time bucket start (ISO 8601).

    - `value: number`

    - `previousValue?: number`

      Value for the same bucket in the comparison window, when requested.

    - `segments?: Array<ReportSegmentDatum>`

      - `segment: string`

      - `value: number`

      - `previousValue?: number`

      - `segmentLabel?: string`

### Example

```typescript
import Featurebase from 'featurebase-node';

const client = new Featurebase({
  apiKey: process.env['FEATUREBASE_API_KEY'], // This is the default and can be omitted
});

const response = await client.reports.query({
  aggregation: 'count',
  endDate: '2026-07-15',
  granularity: 'day',
  metric: 'new_conversations',
  startDate: '2026-07-12',
});

console.log(response.meta);
```

#### Response

```json
{
  "meta": {
    "aggregation": "count",
    "datasetId": "datasetId",
    "granularity": "granularity",
    "metric": "metric",
    "officeHoursOnly": true,
    "unit": "count",
    "groupBy": "groupBy",
    "segmentBy": "segmentBy"
  },
  "object": "report_query_result",
  "value": 0,
  "deltaPercent": 0,
  "flowData": [
    {
      "metricId": "metricId",
      "pathId": "pathId",
      "source": "source",
      "sourceLabel": "sourceLabel",
      "target": "target",
      "targetLabel": "targetLabel",
      "value": 0,
      "colorKey": "colorKey",
      "percentage": 0,
      "sortOrder": 0
    }
  ],
  "groupedData": [
    {
      "group": "group",
      "value": 0,
      "groupLabel": "groupLabel",
      "previousValue": 0,
      "segments": [
        {
          "segment": "segment",
          "value": 0,
          "previousValue": 0,
          "segmentLabel": "segmentLabel"
        }
      ]
    }
  ],
  "previousValue": 0,
  "segmentData": [
    {
      "segment": "segment",
      "value": 0,
      "previousValue": 0,
      "segmentLabel": "segmentLabel"
    }
  ],
  "table": {
    "columns": [
      {
        "id": "id",
        "kind": "dimension",
        "label": "label",
        "sortable": true,
        "unit": "count",
        "align": "left",
        "sticky": true,
        "summary": "sum",
        "valueType": "valueType"
      }
    ],
    "key": "key",
    "mode": "aggregate",
    "page": 0,
    "pageSize": 0,
    "rows": [
      {
        "id": "id",
        "cells": {
          "foo": {
            "display": "display",
            "denominator": 0,
            "numerator": 0,
            "raw": {},
            "sortValue": "string",
            "value": {}
          }
        }
      }
    ],
    "totalRows": 0,
    "sort": {
      "columnId": "columnId",
      "direction": "asc"
    },
    "summaryRows": [
      {
        "id": "id",
        "cells": {
          "foo": {
            "display": "display",
            "denominator": 0,
            "numerator": 0,
            "raw": {},
            "sortValue": "string",
            "value": {}
          }
        }
      }
    ]
  },
  "timeSeries": [
    {
      "date": "2026-07-15",
      "value": 0,
      "previousValue": 0,
      "segments": [
        {
          "segment": "segment",
          "value": 0,
          "previousValue": 0,
          "segmentLabel": "segmentLabel"
        }
      ]
    }
  ]
}
```
