SuperAPI AI Directory

b24jssdk-filtering

Coding & development

View repository

Updated 10-15 20:06

About this skill

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.

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:

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

For "not in":

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.

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

v3 operators — exhaustive list (only these 8)

=   !=   >   >=   <   <=   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:

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):

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):

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.

A boolean condition needs a JSON body

A boolean in a filter survives only if the request body is JSON. Sent as application/x-www-form-urlencoded it arrives as the string "false", the condition is dropped, and the call answers with rows it should have excluded — no error on either side. Measured on user.get / ACTIVE: JSON { ACTIVE: false } → 0 rows, form-encoded → 1 row.

The SDK states Content-Type: application/json on every request, per request, so this is handled — with two ways to undo it:

  • a request interceptor on getHttpClient(v).ajaxClient that sets Content-Type runs after the SDK's config and changes the encoding;
  • building the same call by hand (curl, a webhook tester, your own client) and posting form data.
// ✅ through the SDK — JSON, so the condition holds
await $b24.actions.v2.call.make({ method: 'user.get', params: { filter: { ACTIVE: false } } })

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

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 — 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:

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

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

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

Examples

v2 — open deals with amount range

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

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

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

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

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). See CallTail and FetchTail — and note a *.tail endpoint is rare, so check the method has one first. 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.