Skip to content
Dashboard

Run a report query

client.reports.query(ReportQueryParams { aggregation, endDate, granularity, 16 more } params, RequestOptionsoptions?): ReportQueryResponse { meta, object, value, 7 more }
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.
  2. 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.
  3. 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.
  4. Optionally break results down with groupBy (primary dimension) and segmentBy (secondary dimension).
  5. 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

{
  "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).

ParametersExpand Collapse
params: ReportQueryParams { aggregation, endDate, granularity, 16 more }
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.

One of the following:
"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.

minLength10
maxLength64
granularity: "hour" | "day" | "week" | "month"

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

One of the following:
"hour"
"day"
"week"
"month"
metric: string

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

minLength1
maxLength128
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.

minLength10
maxLength64
compareEndDate?: string

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

minLength10
maxLength64
compareStartDate?: string

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

minLength10
maxLength64
filters?: ReportFilterRule { fieldId, kind, operator, value } | ReportFilterExpression { children, kind, op }

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.

One of the following:
ReportFilterRule { fieldId, kind, operator, value }
fieldId: string

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

minLength1
maxLength512
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.

One of the following:
"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.

One of the following:
string
number
boolean
Array<string | number | boolean>
string
number
boolean
ReportFilterExpression { children, kind, op }
children: Array<ReportFilterRule { fieldId, kind, operator, value } | ReportFilterGroup { children, kind, op } >

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

One of the following:
ReportFilterRule { fieldId, kind, operator, value }
fieldId: string

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

minLength1
maxLength512
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.

One of the following:
"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.

One of the following:
string
number
boolean
Array<string | number | boolean>
string
number
boolean
ReportFilterGroup { children, kind, op }
children: Array<ReportFilterRule { fieldId, kind, operator, value } >
fieldId: string

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

minLength1
maxLength512
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.

One of the following:
"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.

One of the following:
string
number
boolean
Array<string | number | boolean>
string
number
boolean
kind: "group"
op: "and" | "or"

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

One of the following:
"and"
"or"
kind: "group"
op: "and" | "or"

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

One of the following:
"and"
"or"
groupBy?: string

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

maxLength512
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.

minimum1
maximum100
segmentBy?: string

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

maxLength512
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.

One of the following:
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.

maxLength64
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.

One of the following:
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.

One of the following:
"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.

One of the following:
"2026-01-01.nova"
"2025-12-12.clover"
ReturnsExpand Collapse
ReportQueryResponse { meta, object, value, 7 more }
meta: Meta { aggregation, datasetId, granularity, 5 more }
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.

One of the following:
"count"
"sum"
"avg"
"median"
"min"
"max"
"range"
"percentile"
"value"
datasetId: string
granularity: string
metric: string
officeHoursOnly: boolean
unit: "count" | "percentage" | "duration_ms" | "number"
One of the following:
"count"
"percentage"
"duration_ms"
"number"
groupBy?: string
segmentBy?: string
object: "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, value, groupLabel, 2 more } >
group: string

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

value: number
groupLabel?: string
previousValue?: number
segments?: Array<ReportSegmentDatum { segment, value, previousValue, segmentLabel } >
segment: string
value: number
previousValue?: number
segmentLabel?: string
previousValue?: number

Aggregated value for the comparison window, when requested.

segmentData?: Array<ReportSegmentDatum { segment, value, previousValue, segmentLabel } >
segment: string
value: number
previousValue?: number
segmentLabel?: string
table?: Table { columns, key, mode, 6 more }
columns: Array<Column>
id: string
kind: "dimension" | "metric" | "record_attribute"
One of the following:
"dimension"
"metric"
"record_attribute"
label: string
sortable: boolean
unit: "count" | "percentage" | "duration_ms" | 3 more
One of the following:
"count"
"percentage"
"duration_ms"
"number"
"text"
"datetime"
align?: "left" | "right"
One of the following:
"left"
"right"
sticky?: boolean
summary?: "sum" | "weighted_rate" | "none"
One of the following:
"sum"
"weighted_rate"
"none"
valueType?: string
key: string
mode: "aggregate" | "records"
One of the following:
"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
One of the following:
string
number
value?: unknown
totalRows: number
sort?: Sort { columnId, direction }
columnId: string
direction: "asc" | "desc"
One of the following:
"asc"
"desc"
summaryRows?: Array<SummaryRow>
id: string
cells: Record<string, Cells>
display: string
denominator?: number
numerator?: number
raw?: unknown
sortValue?: string | number | null
One of the following:
string
number
value?: unknown
timeSeries?: Array<ReportTimeSeriesDatum { date, value, previousValue, segments } >
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, value, previousValue, segmentLabel } >
segment: string
value: number
previousValue?: number
segmentLabel?: string

Run a report query

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);
{
  "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"
        }
      ]
    }
  ]
}
Returns Examples
{
  "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"
        }
      ]
    }
  ]
}