---
title: Query syntax reference | Featurebase
description: The full reporting query grammar – metrics, aggregations, filters, grouping, comparison windows and drill-ins.
---

Every report query is a JSON body sent to `POST /v2/reports/query`. This page documents the complete grammar. The [datasets catalog](/guides/reports#quickstart/index.md) (`GET /v2/reports/datasets`) is the authoritative source for the metric and attribute IDs your workspace accepts.

## Anatomy of a query

```
{
  "metric": "new_conversations",
  "aggregation": "count",
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "compareStartDate": "2026-05-01",
  "compareEndDate": "2026-05-31",
  "granularity": "day",
  "timezone": "Europe/Tallinn",
  "officeHoursOnly": false,
  "filters": { "kind": "rule", "fieldId": "conversation.channel", "operator": "is", "value": "email" },
  "groupBy": "teammate.currently_assigned",
  "segmentBy": "conversation.priority",
  "view": "standard"
}
```

| Field                                 | Required                      | Description                                                                                |
| ------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------ |
| `metric`                              | yes                           | Metric ID from the catalog                                                                 |
| `aggregation`                         | yes                           | One of the metric’s `allowedAggregations`                                                  |
| `percentile`                          | with `percentile` aggregation | 1–100, e.g. `95` for p95                                                                   |
| `startDate` / `endDate`               | yes                           | ISO 8601 date or datetime, inclusive window                                                |
| `compareStartDate` / `compareEndDate` | no                            | Comparison window – adds `previousValue` and `deltaPercent` to every datum                 |
| `granularity`                         | yes                           | `hour`, `day`, `week` or `month` – the time-series bucket size                             |
| `timezone`                            | no                            | IANA timezone for bucketing (defaults to UTC)                                              |
| `officeHoursOnly`                     | no                            | Restrict time metrics to configured office hours (only metrics with `supportsOfficeHours`) |
| `filters`                             | no                            | Filter expression (see below)                                                              |
| `groupBy`                             | no                            | Attribute ID for the primary breakdown                                                     |
| `segmentBy`                           | no                            | Attribute ID for a secondary breakdown within each group/bucket                            |
| `view`                                | no                            | `standard` (default), `hourly_heatmap` or `sankey`                                         |

## Aggregations

| Aggregation                   | Use with                                                 |
| ----------------------------- | -------------------------------------------------------- |
| `count`                       | Count metrics (`new_conversations`, `replies_sent`, …)   |
| `avg`, `median`, `min`, `max` | Duration and number metrics                              |
| `percentile`                  | Duration metrics – pass `percentile: 95` alongside       |
| `sum`                         | Summable number metrics                                  |
| `value`                       | Rate/percentage metrics (`csat_score`, `reopen_rate`, …) |

Each metric lists its own `allowedAggregations` in the catalog; anything else returns `400 invalid_parameter`.

## Filter expressions

A filter is either a single **rule** or a **group** combining child expressions with `and` / `or`. Groups nest up to 6 levels; 100 rules max per expression.

```
FilterExpression := Rule | Group
Rule  := { "kind": "rule", "fieldId": string, "operator": Operator, "value"?: Value }
Group := { "kind": "group", "op": "and" | "or", "children": FilterExpression[] }
Value := string | number | boolean | Array<string | number | boolean>
```

Example – email or web-chat conversations that are closed:

```
{
  "kind": "group",
  "op": "and",
  "children": [
    { "kind": "rule", "fieldId": "conversation.channel", "operator": "in", "value": ["email", "desktop"] },
    { "kind": "rule", "fieldId": "conversation.state", "operator": "is", "value": "closed" }
  ]
}
```

### Operators

| Operator                            | Meaning                                    | Value shape                                             |
| ----------------------------------- | ------------------------------------------ | ------------------------------------------------------- |
| `is` / `is_not`                     | Equality                                   | scalar                                                  |
| `in` / `not_in`                     | Set membership                             | non-empty array                                         |
| `contains` / `not_contains`         | Substring / multi-value containment        | scalar                                                  |
| `gte` / `lte`                       | Greater/less than or equal                 | scalar (number, or ISO date string for date attributes) |
| `between`                           | Inclusive range                            | 2-element array                                         |
| `exists` / `not_exists`             | Attribute has any value                    | omit `value`                                            |
| `is_member_of` / `is_not_member_of` | Membership in a referenced set (e.g. team) | non-empty array                                         |

Wrong value shape for an operator returns `400` with a message naming the operator and field.

### Which attributes can I use?

Attributes are scoped to datasets: `filters`, `groupBy` and `segmentBy` must use attributes from the **same dataset as the metric**. Find the metric’s dataset in `GET /v2/reports/datasets` (the dataset whose `metrics` array contains it — also echoed as `meta.datasetId` in query responses) and pick from that dataset’s `attributes`. Each attribute lists its `allowedOperators`, `valueType` and `supportsFilter` / `supportsGroupBy` flags.

For valid values: attributes with `staticOptions` carry their full value set inline; for the rest use `POST /v2/reports/filter-values`:

```
{ "fieldId": "conversation.tags", "query": "bill", "limit": 20 }
```

## Grouping and segmentation

- `groupBy` breaks the aggregate down by one attribute → `groupedData` in the response
- `segmentBy` adds a second dimension → `segments` arrays nested inside each group and time-series datum

Attributes must have `supportsGroupBy: true` in the catalog. Group values come back as raw values (`group`) plus display labels (`groupLabel`) where the raw value is an ID.

## Period comparison

Pass `compareStartDate` / `compareEndDate` (both or neither). Every datum then carries `previousValue`, and the top level adds `deltaPercent`. Typical usage is the same-length window immediately before `startDate`.

## Views

Each metric lists its `supportedViews` in the catalog.

- `standard` – time series + optional groups/segments (default)
- `hourly_heatmap` – buckets by day-of-week × hour-of-day; drill in with `dayOfWeek` (1 = Monday) + `hourOfDay` (0–23)
- `sankey` – conversation flow edges in `flowData` (`overview_conversation_flow` metric only); drill in with `flowPathId` or `sourceNodeId` + `targetNodeId`

## Response shape

```
{
  "object": "report_query_result",
  "value": 1284,
  "previousValue": 1120,
  "deltaPercent": 14.6,
  "timeSeries": [ { "date": "2026-06-01", "value": 43, "previousValue": 39 } ],
  "groupedData": [ { "group": "email", "groupLabel": "Email", "value": 612 } ],
  "meta": {
    "metric": "new_conversations",
    "datasetId": "conversations",
    "unit": "count",
    "granularity": "day",
    "aggregation": "count",
    "officeHoursOnly": false
  }
}
```

Duration values are milliseconds (`unit: "duration_ms"`); percentages are 0–100 (`unit: "percentage"`).

## Drill-ins

`POST /v2/reports/drill-in` returns the paginated rows behind a metric. Send the **same** query context (`metric`, dates, `granularity`, `filters`, `groupBy`, `segmentBy`, `view`) plus `dataPointFilters` selecting the point:

| Data point      | `dataPointFilters`                                 |
| --------------- | -------------------------------------------------- |
| Whole window    | `{}`                                               |
| One time bucket | `{ "timeBucket": "2026-06-15" }`                   |
| One group       | `{ "groupValue": "email" }`                        |
| Heatmap cell    | `{ "dayOfWeek": 2, "hourOfDay": 14 }`              |
| Flow edge       | `{ "sourceNodeId": "...", "targetNodeId": "..." }` |

Pagination is `page` (1-based) + `pageSize` (max 200). The response lists `availableColumns` (all columns the dataset offers) and `defaultColumnIds`; rows are keyed by column ID. Cells holding people (teammates, contacts) are identity objects: `{ "kind": "identity", "id": "...", "label": "..." }`. Sort with `sort: [{ "fieldId": "metric.value", "direction": "desc" }]`.

## Errors

| Status | Code                 | Cause                                                                          |
| ------ | -------------------- | ------------------------------------------------------------------------------ |
| 400    | `invalid_parameter`  | Unknown metric/attribute, disallowed aggregation or operator, malformed filter |
| 404    | `resource_not_found` | Reports not enabled for the workspace                                          |
| 400    | validation message   | Body fails schema validation (unknown keys are rejected)                       |
