> ## Documentation Index
> Fetch the complete documentation index at: https://help.ezyid.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Dynamic Filters

> One filter language for querying assets, tasks, job cards and inventory.

***

Four endpoints accept the same request body, so once you can query one of them you can query all four:

| Data      | Query endpoint              | Fields endpoint                   | Time range applies to |
| --------- | --------------------------- | --------------------------------- | --------------------- |
| Assets    | `POST /api/assets/query`    | `GET /api/assets/query/fields`    | `createdOn`           |
| Tasks     | `POST /api/tasks/query`     | `GET /api/tasks/query/fields`     | `dueDate`             |
| Job cards | `POST /api/jobcards/query`  | `GET /api/jobcards/query/fields`  | `createdOn`           |
| Inventory | `POST /api/inventory/query` | `GET /api/inventory/query/fields` | none                  |

Each request returns only the records belonging to the subscriber the API key was issued to. There is no way to widen a query past your own data.

### Start with the fields endpoint

Every dataset publishes its own catalog of fields. Read it once and use it to build your filters, rather than guessing field names:

```bash theme={null}
GET https://api-external.ezyid.io/api/tasks/query/fields?apiKey=your_api_key
```

```json theme={null}
{
  "datasetId": "tasks",
  "name": "Tasks",
  "defaultDateFieldId": "dueDate",
  "timeRangePresets": ["today", "last7Days", "last30Days", "..."],
  "fields": [
    {
      "id": "taskStatus",
      "displayName": "Status",
      "type": "enum",
      "role": "dimension",
      "operators": ["eq", "neq", "in", "notIn", "isEmpty", "isNotEmpty"],
      "values": [
        { "value": 0, "label": "Open" },
        { "value": 1, "label": "Completed" },
        { "value": 2, "label": "Pending" },
        { "value": 3, "label": "In Progress" }
      ]
    }
  ]
}
```

The `id` is what you name in `filters`, `sort` and `fields`. The `operators` list is exactly what that field accepts — anything else is rejected with a `400`.

## Building a filter

A filter is a group of clauses joined by `and` (the default) or `or`. Each clause names a field, an operator and a value:

```json theme={null}
{
  "filters": {
    "logic": "and",
    "clauses": [
      { "fieldId": "taskStatus", "operator": "notIn", "value": ["Completed"] },
      { "fieldId": "priority", "operator": "eq", "value": "High" }
    ]
  }
}
```

Groups can nest one level to mix `and` with `or`. This matches tasks that are either overdue or unassigned, in both cases only on the Northern site:

```json theme={null}
{
  "filters": {
    "logic": "and",
    "clauses": [{ "fieldId": "siteName", "operator": "eq", "value": "Northern" }],
    "groups": [
      {
        "logic": "or",
        "clauses": [
          { "fieldId": "isOverdue", "operator": "eq", "value": true },
          { "fieldId": "assigneeCount", "operator": "eq", "value": 0 }
        ]
      }
    ]
  }
}
```

Omit `filters` entirely to list everything the key's subscriber owns.

### Operators

Which operators a field accepts depends on its `type`:

| Type      | Operators                                                                                |
| --------- | ---------------------------------------------------------------------------------------- |
| `string`  | `eq`, `neq`, `contains`, `startsWith`, `in`, `notIn`, `isEmpty`, `isNotEmpty`            |
| `int`     | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between`, `in`, `notIn`, `isEmpty`, `isNotEmpty` |
| `decimal` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between`, `isEmpty`, `isNotEmpty`                |
| `date`    | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between`, `isEmpty`, `isNotEmpty`                |
| `enum`    | `eq`, `neq`, `in`, `notIn`, `isEmpty`, `isNotEmpty`                                      |
| `bool`    | `eq`, `neq`, `isEmpty`, `isNotEmpty`                                                     |

`in` and `notIn` take an array. `between` needs an `endValue` and includes both ends. `isEmpty` and `isNotEmpty` take no value at all.

### Values

* **Dates** are strings, either a plain date (`"2026-01-31"`) or a full timestamp. A plain date covers the whole day, so `eq` on `"2026-01-31"` matches anything that day. Dates with no time zone are read as UTC.
* **Status fields** (`type: "enum"`) accept either the numeric code or the label from the catalog, so `1` and `"Completed"` are the same filter. Responses always return the code.
* **Text** matches without regard to case. In `contains` and `startsWith`, a `%` or `_` in your value matches itself rather than acting as a wildcard.
* **Booleans** are `true` or `false`.

<Note>
  `isEmpty` on a text field also matches an empty string, not just a missing value.

  Some older records store an unset date as a placeholder such as `1901-01-01` rather than leaving it empty, so treat a date at or before 1901 as "not set" rather than relying on `isEmpty`.
</Note>

### Time range

`timeRange` is a shorthand for filtering on the dataset's `defaultDateFieldId`, so you do not have to know which date field that is. Use a preset:

```json theme={null}
{ "timeRange": { "preset": "last30Days" } }
```

Or supply your own window, which includes `from` and excludes `to`:

```json theme={null}
{ "timeRange": { "from": "2026-01-01", "to": "2026-02-01" } }
```

Supply one or the other, never both. Presets are resolved in UTC. Inventory has no date field, so it rejects `timeRange` — filter on a field instead.

### Sorting and paging

```json theme={null}
{
  "sort": [{ "fieldId": "dueDate", "direction": "asc" }],
  "start": 0,
  "rows": 50
}
```

Up to two sort rules are allowed, and the record's own id always breaks ties, so paging never repeats or skips a row. Read the next page by advancing `start` by `rows`. `rows` defaults to 100 and caps at 1000; `count` in the response is the total number of matches, so you know when to stop.

## Reading the response

`items` holds one object per record, keyed by the same field ids you filtered on. Ask for `fields` to keep the payload small; omit it to get every field in the catalog. A field with no value is `null`.

```json theme={null}
{
  "count": 128,
  "start": 0,
  "rows": 2,
  "items": [
    {
      "id": 90412,
      "title": "Monthly harness inspection",
      "taskStatus": 0,
      "priority": 3,
      "dueDate": "2026-08-24T00:00:00",
      "siteName": "Northern",
      "assigneeCount": 2
    },
    {
      "id": 90408,
      "title": "Quarterly generator service",
      "taskStatus": 3,
      "priority": 2,
      "dueDate": "2026-08-26T00:00:00",
      "siteName": "Depot 4",
      "assigneeCount": 1
    }
  ]
}
```

### Errors

| Status | Meaning                                                                                                                                                |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | The `apiKey` is missing, or the filter, sort or paging window was rejected. The body carries an `error` message naming the field or operator at fault. |
| `401`  | The API key has been revoked or has expired.                                                                                                           |
| `404`  | The API key does not exist.                                                                                                                            |

A rejected filter tells you what to change:

```json theme={null}
{ "error": "Operator 'contains' is not valid for enum field 'taskStatus'." }
```

### Limits

| Limit               | Value |
| ------------------- | ----- |
| Clauses per request | 20    |
| Group nesting depth | 2     |
| Sort rules          | 2     |
| Rows per page       | 1000  |
