> ## Documentation Index
> Fetch the complete documentation index at: https://www.plain.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Filter and group reporting queries

> Narrow Reporting API queries with filters and AND, OR, and NOT, and split results by a dimension with groupBy and topN.

`filters` narrows a reporting query to matching threads, and `groupBy` splits the result into one series or value per label, tier, company, or other dimension. All three reporting queries accept `filters`. The [time series](/docs/graphql/reporting/time-series) and [single value](/docs/graphql/reporting/single-value) queries also accept `groupBy`.

## Filter fields

`filters` takes a `ThreadMetricFilters` object with these fields:

| Field | Matches threads where |
| - | - |
| `labelTypeIds` | The thread has any of these labels |
| `companyIdentifiers` | The customer's company is any of these, by `companyId` or `companyDomainName` |
| `tenantIdentifiers` | The thread's tenant is any of these, by `tenantId` or `externalId` |
| `tierIdentifiers` | The thread's tier is any of these, by `tierId` or `externalId` |
| `customerGroupIdentifiers` | The customer is in any of these groups, by `customerGroupId`, `customerGroupKey`, or `externalId` |
| `assignedToUser` | Any of these users is the thread's current primary or additional assignee. See [Filter by user](#filter-by-user) |
| `priorities` | The thread's priority is any of these: `0` Urgent, `1` High, `2` Normal, `3` Low |
| `messageSource` | The thread's first inbound message came from any of these channels: `EMAIL`, `SLACK`, `CHAT`, `API`, `MS_TEAMS`, `DISCORD`, `INTERNAL` |
| `threadFields` | Every entry matches a thread field, by `key` and `stringValue`, `stringValues`, or `booleanValue` |
| `tenantFields` | Every entry matches a field on the thread's tenant, by `externalFieldId` and `stringValue` or `stringValues` |
| `surveyResponse` | For CSAT metrics only: `rating` is required, and `surveyId` limits results to one survey |
| `userIds` | For `agent_` metrics only: any of these users gets the credit for the measured action. See [Filter by user](#filter-by-user) |

A company, tenant, tier, or customer group identifier that matches nothing fails the query with `thread_metric_filter_no_match`.

## Filter by user

`assignedToUser` and `userIds` both take user IDs, but they answer different questions:

* **`assignedToUser`**: who the thread is assigned to right now. It works on every metric and matches the current primary or additional assignee, not whoever was assigned when the measured event happened
* **`userIds`**: who gets the credit for the measured action. It only works on `agent_` metrics, and what credit means depends on the metric

For `userIds`, each `agent_` metric credits either the user who did the action or every user ever assigned to the thread.

**The user who did the action**, such as sending the reply or changing the status. For assignment transitions, that's the user who was assigned:

* `agent_threads_first_response_time`
* `agent_threads_time_between_follow_up_responses`
* `agent_messages_sent_count`
* `agent_threads_status_transitions_count__todo`, `__snoozed`, and `__done`
* `agent_threads_assignment_transitions_count`

Every other `agent_` metric credits **every user ever assigned to the thread**, even if the thread has since been reassigned. A thread that passed through 3 users counts toward all 3.

`userIds` is only allowed at the top level of `filters`, not inside `and`, `or`, or `not`. On a metric without the `agent_` prefix, it returns `input_validation`. Naming anyone other than yourself in either filter needs the `metricsAgent:read` permission.

## Filters match current values

Filters match the values a thread has now, not the values it had when the measured event happened. Remove a label from a thread and it drops out of every past bucket for that label. The same applies to assignee, tier, priority, company, tenant, customer group, and fields.

## Combine filters with AND, OR, and NOT

Fields in the same object combine with AND, and the values in one field combine with OR. For other combinations, nest filter objects:

* **`and`**: a list of filter objects that must all match
* **`or`**: a list of filter objects where at least one must match
* **`not`**: one filter object that must not match

This filter matches threads with the Bug label on the Free or Pro tier:

```json theme={null}
{
  "labelTypeIds": ["lt_01HB81HYXZ2B8QGYCH5YG1AGM8"],
  "or": [
    { "tierIdentifiers": [{ "externalId": "free" }] },
    { "tierIdentifiers": [{ "externalId": "pro" }] }
  ]
}
```

To require 2 labels at once, put each in its own object under `and`:

```json theme={null}
{
  "and": [
    { "labelTypeIds": ["lt_01HB81HYXZ2B8QGYCH5YG1AGM8"] },
    { "labelTypeIds": ["lt_01HB8BTN5S8NAJGSNC3NJZ7FV4"] }
  ]
}
```

Filters can nest `and` and `or` up to 2 levels deep, so `(A AND B) OR (C AND D)` works and a third level returns `input_validation`. `not` doesn't add a level. `userIds` is only allowed at the top level.

## Group results by a dimension

`groupBy` takes a list with one `ThreadMetricGroupByInput`. These dimensions are supported:

| Dimension | Groups by |
| - | - |
| `ASSIGNEE` | The thread's current primary or additional assignees. On `agent_` metrics, the user who gets the credit. Needs `metricsAgent:read` |
| `COMPANY` | The customer's company |
| `CUSTOMER_GROUP` | The customer's groups |
| `LABEL_TYPE` | The thread's labels |
| `MESSAGE_SOURCE` | The channel of the thread's first inbound message |
| `PRIORITY` | The thread's priority |
| `TENANT` | The thread's tenant |
| `TIER` | The thread's tier |
| `THREAD_FIELD` | A thread field's value. Set `subKey` to the field's key |
| `TENANT_FIELD` | A tenant field's value. Set `subKey` to the field's external ID |

Each group's `group.value` is a string: an ID such as a tier ID, a priority from `"0"` to `"3"`, or a field value. `group.label` holds the display name, such as the tier name. It's `null` for `MESSAGE_SOURCE`, `THREAD_FIELD`, and `TENANT_FIELD`, whose `value` is already readable, and for a deleted entity.

Grouped results follow two rules:

* **A thread with several values counts in each group.** A thread with 2 labels counts under both, and the same applies to customer groups, tenants, and assignees.
* **A thread with no value is left out.** Grouping by tier drops threads with no tier, so the groups don't add up to the ungrouped total.

If you filter and group by the same dimension, only the filtered values become groups.

## Limit the number of groups with topN

`topN` returns the largest groups, from 1 to 50, and pools the rest into one group whose `value` is `__OTHER__`. Without `topN`, the query returns every group. On a large workspace, grouping by a dimension with thousands of values can time out with `thread_metric_query_timed_out`.

`__OTHER__` follows these rules:

* **Groups are ranked by the metric's value over the whole range.** On a duration metric, the top 5 groups are the 5 slowest, not the 5 busiest.
* **`__OTHER__` isn't a real entity.** You can't filter by it or get its thread IDs.
* **On a duration time series, `__OTHER__` is the percentile of the remaining groups' values in each bucket**, not the percentile of their threads pooled together. Single value queries pool the remaining groups' threads and take the percentile of those.

This `groupBy` returns the 10 slowest tiers by first response time and pools the rest:

```json theme={null}
{
  "groupBy": [{ "dimension": "TIER", "topN": 10 }]
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.