# B24jssdk Filtering

> Build filter, order, and select parameters for Bitrix24 REST methods called via b24jssdk's actions.v{2,3}.* API. Covers the v2 prefix dialect (>=, <=, !, %, =%), the v3 array-triple dialect ([['field', 'op', value]]) and the typed FilterV3 builder (eq/in/between/and/or/not/build), NOT, multi-value (IN), dates via Text.toB24Format, and the order-stripping rule of callList. Load when building filtered queries.

- Skill: `bitrix24/b24jssdk-filtering` (Agent Skill)
- Install (CLI): `npx skillmds@latest add bitrix24/b24jssdk-filtering`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bitrix24/b24jssdk-filtering/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: bitrix24 (https://skillmd.com/u/bitrix24)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bitrix24/b24jssdk-filtering

---


# b24jssdk filtering

Bitrix24 has **two filter dialects**. They are not interchangeable — each API version accepts only its own. Pick the dialect that matches the action surface you're using.

## v2 — prefix-keyed object

Used by `$b24.actions.v2.{call,callList,fetchList}.make({ params: { filter: ... }})`. The operator is a prefix on the field name.

```ts
const params = {
  filter: {
    '>=opportunity': 50000,
    '<=opportunity': 200000,
    '!stageId': 'LOST',
    '=%title': 'A%'
  }
}
```

### v2 operators

| Prefix | Meaning | Example |
| --- | --- | --- |
| (none) | exact match | `{ stageId: 'NEW' }` |
| `>=` | greater or equal | `{ '>=opportunity': 50000 }` |
| `>` | greater | `{ '>id': 100 }` |
| `<=` | less or equal | `{ '<=opportunity': 200000 }` |
| `<` | less | `{ '<closeDate': '2026-01-01' }` |
| `!` | not equal | `{ '!stageId': 'LOST' }` |
| `!=` | not equal (alternate) | `{ '!=stageId': 'LOST' }` |
| `%` | LIKE (substring, case-insensitive) | `{ '%title': 'поставка' }` |
| `=%` | LIKE with explicit pattern (`%`/`_`) | `{ '=%title': 'A%' }` |
| `!%` | NOT LIKE | `{ '!%title': 'тест' }` |

Multiple keys are combined with AND. Two operators on the same field need two separate keys: `{ '>=opportunity': 50000, '<=opportunity': 200000 }`.

### v2 IN / multi-value

Plain array means `IN`:

```ts
const params = { filter: { stageId: ['NEW', 'PREPARATION', 'EXECUTING'] } }
```

For "not in":

```ts
const params = { filter: { '!stageId': ['LOST', 'WON'] } }
```

## v3 — array of triples

Used by `$b24.actions.v3.{call,callList,fetchList}.make({ params: { filter: ... }})`. The filter is a JSON **array**, each element is a condition.

```ts
filter: [
  ['stageId', '=', 'NEW'],
  ['createdTime', '>=', '2026-01-01T00:00:00+03:00'],
  ['responsible', 'in', [1, 2, 3]]
]
```

### v3 operators — exhaustive list (only these 8)

```text
=   !=   >   >=   <   <=   in   between
```

> **No `%`, `like`, `~`, or substring operator** at the v3 protocol level — `Filtering/Operator.php` does not define one. Substring search is currently a v2-only feature.

`between` value must be a 2-element array: `[min, max]`.
`in` value must be an array.

### v3 condition forms

The two-arg form is sugar:

```ts
const equals = ['id', 42]              // same as ['id', '=', 42]
const oneOf = ['stageId', ['A', 'B']]  // same as ['stageId', 'in', ['A', 'B']]
```

The long struct form supports nested groups with `or` logic and negation (rarely needed in user code):

```ts
filter: [
  ['status', '=', 'OPEN'],
  {
    type: 'filter',
    logic: 'or',
    negative: false,
    conditions: [
      ['priority', '=', 'HIGH'],
      ['responsible', '=', 42]
    ]
  }
]
```

The top-level array is implicitly `logic: 'and'`. The `type: 'filter'` key above is optional — the server infers it — so the `FilterV3` builder below emits the shorter `{ logic, conditions }` form (verified live), and both are accepted.

### v3 — typed builder (`FilterV3`)

For anything beyond a flat list of triples, prefer the `FilterV3` builder over hand-writing the structs — it validates operators and `in`/`between` shapes on the client (a typo fails fast instead of as a server `UNKNOWNFILTEROPERATOREXCEPTION`):

```ts
import { Text } from '@bitrix24/b24jssdk'
import { FilterV3 as F } from '@bitrix24/b24jssdk'

// status = NEW  AND  (id in [1,2]  OR  id > 100)
const filter = F.build(
  F.eq('status', 'NEW'),
  F.or(
    F.in('id', [1, 2]),
    F.gt('id', 100)
  )
)
await $b24.actions.v3.call.make({ method: 'tasks.task.list', params: { filter } })
```

- Leaves: `F.eq` / `F.ne` / `F.gt` / `F.ge` / `F.lt` / `F.le` / `F.in(field, array)` / `F.between(field, from, to)`.
- Groups: `F.and(...)`, `F.or(...)`, `F.not(node)` (negates a condition or group).
- `F.build(...nodes)` returns the top-level (AND-joined) array for `params.filter`; falsy nodes are skipped, so `F.build(F.eq('a', 1), flag && F.gt('b', 2))` inlines conditionals.

> Inside a **v3 batch**, a filter value can reference an earlier command's output via a `$ref`/`$refArray` marker — build them with `BatchRefV3` (`import { BatchRefV3 } from '@bitrix24/b24jssdk'`), e.g. `['id', 'in', BatchRefV3.refArray('tasks.id')]`. The server does the substitution; see the `b24jssdk-rest` skill / the v3 batch docs.

## `order` rule for callList / fetchList

Both `actions.v{2,3}.callList.make` and `fetchList.make` **strip user-supplied `order`** and force `{ [cursorIdKey]: 'ASC' }` (where `cursorIdKey` defaults to `idKey`) because the action uses keyset cursor pagination. If you pass an `order`, the SDK logs a warning (`callList.make: user-provided 'order' parameter is ignored…`) and discards it (see the `order` warning in `packages/jssdk/src/core/actions/v2/call-list.ts` and the v3 equivalent).

If you need a specific sort order, drop down to `call.make` and page manually — but you almost certainly want to filter more narrowly instead.

## `order` for single `call.make`

- **v2**: object with values `'asc' | 'desc' | 'ASC' | 'DESC'` — `order: { id: 'asc', amount: 'desc' }`.
- **v3**: object form **only** (`{ field: 'asc' | 'desc' }`). Arrays throw `InvalidOrderException`. The DTO field must carry the server-side `#[Sortable]` attribute, or the request is **refused**: HTTP 400 with `BITRIX_REST_V3_EXCEPTION_VALIDATION_REQUESTVALIDATIONEXCEPTION` and the field name in `validation[].field`. (The specific PHP class is `DtoFieldRequiredAttributeException`, but it inherits that code and gets none of its own, so match on the code and the field — never on the message, which is localised.) `<entity>.field.list` tells you which fields are `sortable` before you send anything.

## Dates

Use the SDK helper `Text.toB24Format(date)` — it produces the Bitrix24 format `yyyy-MM-dd'T'HH:mm:ssZZ` and handles `Date | DateTime | string` inputs (see `Text.toB24Format` in `packages/jssdk/src/tools/text.ts`).

```ts
import { Text } from '@bitrix24/b24jssdk'

const sixMonthsAgo = new Date()
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)

// v2
const paramsV2 = { filter: { '>=createdTime': Text.toB24Format(sixMonthsAgo) } }

// v3
const paramsV3 = { filter: [['createdTime', '>=', Text.toB24Format(sixMonthsAgo)]] }
```

## Field naming — two casings inside v2

The same logical field has different names depending on the method:

| Logical | UPPER_SNAKE methods (`crm.deal.list`, …) | camelCase methods (`crm.item.list`) |
| --- | --- | --- |
| ID | `ID` | `id` |
| Title | `TITLE` | `title` |
| Stage | `STAGE_ID` | `stageId` |
| Amount | `OPPORTUNITY` | `opportunity` |
| Currency | `CURRENCY_ID` | `currencyId` |
| Created | `DATE_CREATE` | `createdTime` |
| Modified | `DATE_MODIFY` | `updatedTime` |
| Assigned to | `ASSIGNED_BY_ID` | `assignedById` |
| Custom field | `UF_CRM_INN` | `ufCrmInn` |

> Both shapes are v2-API, and neither is "v3-style": the casing here is per-method, not per-API-version. `crm.item.*` is camelCase for reasons of its own and is **not** a v3 method — v3 publishes no `crm.item.*` at all. Under `restApi:v3` camelCase *is* the rule, without exception across the 108 field descriptors measured; ask `<entity>.field.list` when unsure. Use the same casing across `filter`, `select`, and (where applicable) `order`. Mixing styles silently breaks paging.

Full guide: [Discovering entity fields](https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/discovering-entity-fields/) — how to ask an entity for its own field
list, and why a `filterable: false` field is refused rather than ignored.

## Select

Always pass `select` to limit response size:

```ts
select: ['id', 'title', 'stageId', 'opportunity', 'currencyId']
```

For custom fields, list them explicitly. For v3, dot syntax expands relations:

```ts
// v3 only
select: ['id', 'title', 'responsible.name', 'responsible.email']
```

## Examples

### v2 — open deals with amount range

```ts
import { EnumCrmEntityTypeId } from '@bitrix24/b24jssdk'
const response = await $b24.actions.v2.callList.make<CrmItem>({
  method: 'crm.item.list',
  params: {
    entityTypeId: EnumCrmEntityTypeId.deal,
    filter: {
      '!stageId': ['WON', 'LOSE'],
      '>=opportunity': 50000,
      '<=opportunity': 200000
    },
    select: ['id', 'title', 'stageId', 'opportunity']
  },
  idKey: 'id',
  customKeyForResult: 'items'
})
```

### v2 — contacts by phone substring

```ts
import { EnumCrmEntityTypeId } from '@bitrix24/b24jssdk'
const response = await $b24.actions.v2.callList.make<CrmItem>({
  method: 'crm.item.list',
  params: {
    entityTypeId: EnumCrmEntityTypeId.contact,
    filter: { '%phone': '+7916' },
    select: ['id', 'name', 'lastName', 'phone']
  },
  idKey: 'id',
  customKeyForResult: 'items'
})
```

### v2 — tasks not closed

```ts
const response = await $b24.actions.v2.callList.make<TaskItem>({
  method: 'tasks.task.list',
  params: {
    filter: { '!REAL_STATUS': 5 }, // 5 = COMPLETED
    select: ['ID', 'TITLE', 'STATUS', 'RESPONSIBLE_ID']
  },
  idKey: 'id',                   // tasks.task.list returns lowercase id
  cursorIdKey: 'ID',             // ...but sorts / filters by uppercase ID
  customKeyForResult: 'tasks'
})
```

### v3 — eventlog last 6 months

```ts
import { Text } from '@bitrix24/b24jssdk'
const sixMonthsAgo = new Date()
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)
sixMonthsAgo.setHours(0, 0, 0, 0)

const response = await $b24.actions.v3.callList.make<EventItem>({
  method: 'main.eventlog.list',
  params: {
    filter: [['timestampX', '>=', Text.toB24Format(sixMonthsAgo)]],
    select: ['id', 'userId']
  },
  idKey: 'id',
  customKeyForResult: 'items',
  limit: 200
})
```

### v3 — IN / between

```ts
import { Text } from '@bitrix24/b24jssdk'
filter: [
  ['responsibleId', 'in', [1, 2, 3]],
  ['createdTime', 'between', ['2026-01-01T00:00:00+03:00', '2026-03-31T23:59:59+03:00']]
]
```

## Multi-funnel pipelines (CRM)

On portals with multiple funnels, stage IDs come prefixed: `C2:WON`, `C4:LOSE`. Strategies:

- Filter by category ID and base stage: `filter: { categoryId: 2, stageId: 'NEW' }`.
- Enumerate full list explicitly: `filter: { stageId: ['C2:WON', 'C4:WON'] }`.
- Don't trust a bare `'NEW'` to match across funnels — read the live stage list from `crm.dealcategory.stage.list`.

## Anti-patterns

- ❌ `filter: { stageId: { $gte: 100 } }` — MongoDB-style. Not understood by Bitrix24, will 400.
- ❌ `filter: [['title', 'like', 'A%']]` — `like` is not in the v3 operator set. Use v2 + `=%` for substring search.
- ❌ Hand-assembling nested v3 groups (`{ logic, conditions }`) by hand — use the `FilterV3` builder, which validates operators and `in`/`between` shapes client-side.
- ❌ Passing a single condition as `params.filter` (`filter: F.eq('a', 1)`) — `filter` must be an **array**; wrap it: `filter: F.build(F.eq('a', 1))` (or `[F.eq('a', 1)]`).
- ⚠️ The array requirement is `callList` / `fetchList` only. They append the page condition to `filter`, so only an array can be extended (`JSSDK_ACTION_V3_LIST_FILTER_NOT_ARRAY`). `callTail` / `fetchTail` forward `filter` untouched and the portal takes a bare logic group as the whole filter, so `filter: F.or(...)` is accepted there — only the v2 object dialect is refused (`JSSDK_ACTION_V3_TAIL_FILTER_INVALID`). Wrapping in an array works everywhere, so wrap if you want one habit.
- ❌ Passing `order` to `callList.make` / `fetchList.make` — silently discarded with a warning.
- ❌ Mixing `STAGE_ID` and `stageId` across `filter` and `select` — they're different fields per method.
- ❌ Forgetting timezone in date strings — Bitrix24 portals are configured in a portal timezone. Use `Text.toB24Format()` to stay consistent.
- ❌ Using v3 array-of-triples filter for `actions.v2.*` — silently misparsed and returns wrong results.
- ❌ Using v2 prefix-keyed filter for `actions.v3.*` — returns `UnknownFilterOperatorException`.

