b24jssdk recipes
Twelve end-to-end programs. Every recipe runs on B24Hook (Node.js), but each function body takes $b24: TypeB24 so the same code works in-frame too — just swap the boot for initializeB24Frame().
All recipes use the canonical $b24.actions.v{2,3}.*.make() surface. The legacy callMethod / callBatch / callListMethod / fetchListMethod was removed in 3.0.0 — do not generate code against it.
| # |
File |
Stack |
Scopes |
What it does |
| 1 |
examples/01-crm-analytics.ts |
Node |
crm |
Stream all deals via actions.v2.fetchList.make, group by stage, print a funnel report (counts, conversion %, avg ticket, win rate) |
| 2 |
examples/02-mass-messaging.ts |
Node |
crm, im |
Filter contacts via actions.v2.call.make, send im.notify to assigned managers |
| 3 |
examples/03-task-automation.ts |
Node, setInterval |
crm, task |
Poll deal stages with actions.v2.fetchList.make; on watched transition create a task via actions.v3.call.make('tasks.task.add', …) |
| 4 |
examples/04-erp-sync.ts |
Node, node-cron |
crm |
Two-way contact sync between Bitrix24 (via actions.v2.*) and a mock ERP |
| 5 |
examples/05-disk-files.ts |
Node |
disk |
Storages → root → create folder → list files, with a actions.v2.batch.make round-trip |
| 6 |
examples/06-telegram-bot.ts |
Node, grammy, node-cron |
crm |
Poll new deals via actions.v2.call.make, notify a Telegram chat |
| 7 |
examples/07-webhook-handler.ts |
Node, express |
crm, task |
Express server that receives Bitrix24 outbound events; loads details with actions.v2.call.make, then creates a follow-up task with an idempotencyKey so a redelivered event does not create it twice |
| 8 |
examples/08-ai-assistant.ts |
Node, openai |
crm, task |
Deal + activity timeline → GPT prompt → actions.v3.call.make('tasks.task.add') follow-up |
| 9 |
examples/09-web-search-llm.ts |
Node, BYOC |
crm |
Two-step RAG; SDK posts the answer to a deal's timeline via actions.v2.call.make('crm.timeline.comment.add') |
| 10 |
examples/10-error-handling.ts |
Node |
any |
Error-handling cookbook: AjaxError vs SdkError taxonomy; hardErrorCodes / softErrorCodes / retryOnNetworkError knobs via setRestrictionManagerParams; non-idempotent-call safety, with idempotencyKey as the v3 answer |
| 11 |
examples/11-event-registration.ts |
Node |
crm |
CLI tool — list / bind / unbind outbound webhook events (event.get, event.bind, event.unbind). Pairs with recipe 7. |
| 12 |
examples/12-oauth-install.ts |
Node, express |
OAuth app |
OAuth install handshake: handle ONAPPINSTALL / ONAPPUPDATE / ONAPPUNINSTALL events, persist tokens per portal, build B24OAuth on demand, refresh callback writes new tokens back to storage |
Shared library (lib/)
Pure, I/O-free helpers extracted from recipes so they can be unit-tested without a live portal.
lib/ is for helpers where a second hand-written copy is a defect — a security
primitive, or logic with edge cases a test pins. It is not for shared plumbing: these
are recipes, and every extraction costs a reader the ability to lift one file and run
it. When in doubt, leave the code in the recipe.
Worked example of the rule: bootB24() is byte-identical in eleven of the twelve
recipes and stays that way on purpose. It has no edge cases to pin and no security
weight, and it is the first thing a reader needs to see when they open a recipe —
hiding it behind an import would cost more than the duplication does.
Recipes 06 and 12 end with a guard instead of a bare main() call:
import { pathToFileURL } from 'node:url'
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((e: unknown) => {
// Raw console.error, so structured-logger formatting cannot hide the trace.
console.error('\n[recipe failed]', e instanceof Error ? `${e.name}: ${e.message}` : String(e))
process.exitCode = 1
})
}
npx tsx 06-telegram-bot.ts still runs exactly as before — the guard is only false
when the file is imported, which is what the unit tests do to exercise tick's
cursor logic and the uninstall token check without opening a Telegram connection or
binding a port. Keep it when copying those two recipes. Before adding it to a third, prefer the
lib/ route above: extract the logic and test it there. Reach for the guard only when
the function cannot reasonably leave the recipe — tick closes over a live bot and the
poll cursor, handleUninstall is an Express handler — because two shapes of recipe is
already one more than ideal, and the guard spreading to all twelve would quietly retire
the lib/ convention. The exported functions in those files are exported for the same
reason, and export is inert when the file runs directly.
One deployment caveat: the guard compares import.meta.url against process.argv[1].
Under an exotic launcher that rewrites either — a wrapper CLI, some loader shims — the
comparison can come out false, and the process would exit 0 having started nothing.
Check for the recipe's startup log line rather than trusting the exit code.
| File |
Exports |
Used by |
lib/funnel.ts |
baseStage, analyseFunnel, DealRow, StageStat |
recipes 01, 03, 06 |
lib/crypto.ts |
safeEqual |
recipes 07, 12 |
lib/portal-url.ts |
checkPortalUrls |
recipe 12 |
baseStage(s) strips the multi-funnel category prefix ("C2:WON" → "WON"), falling
back to the original string when the part after the colon is empty ("C2:" → "C2:").
Import it from lib/funnel rather than re-declaring it in a recipe: 03 and 06 each
carried their own copy that returned "" for that case. No recipe's control flow
actually differed — neither "" nor "C2:" matches a stage key — but the copies had
drifted from the tested one unnoticed, and the next divergence might not be harmless.
analyseFunnel(deals) groups deals by raw stageId key, summing counts and opportunity amounts.
safeEqual(a, b) is a constant-time compare for secrets and tokens. It had the same
two-copy problem as baseStage, in recipes 07 and 12. Never re-declare this one: a
constant-time compare that drifts in one of two forks is a vulnerability that reads
like a refactor, and === on a token is the bug it exists to prevent.
Boot snippet (shared by all recipes)
import { B24Hook, ConsoleV2Handler, LogLevel, Logger, type TypeB24 } from '@bitrix24/b24jssdk'
export function bootB24(): TypeB24 {
const url = process.env.B24_HOOK
if (!url) throw new Error('B24_HOOK env var is required (incoming webhook URL)')
const $b24 = B24Hook.fromWebhookUrl(url)
return $b24
}
// Recipes run under Node, so the console handler is created explicitly with
// `useStyles: false` — the browser factory emits ANSI-free CSS styling that a
// terminal prints as literal noise.
export const logger = Logger.create('Recipe')
logger.pushHandler(new ConsoleV2Handler(
process.env.NODE_ENV === 'production' ? LogLevel.ERROR : LogLevel.INFO,
{ useStyles: false }
))
The snippet is inlined into each recipe file for copy-paste convenience.
API-version split — quick reference
Inside the recipes the split is:
| Method |
Action surface |
Why |
crm.item.{get,list,add,update,delete} |
actions.v2.* |
Classic API used via v2 here |
crm.activity.list, crm.timeline.comment.add |
actions.v2.* |
Classic API, v2 only |
tasks.task.{add,get,update,delete,list} |
actions.v3.* |
Used via v3 (camelCase, result.items) |
disk.*, im.*, profile, user.* |
actions.v2.* |
Classic API, v2 only |
main.eventlog.{list,get,tail} |
actions.v3.* |
v3, incl. native tail |
The SDK no longer gates v3 by a hardcoded allowlist: actions.v3.* sends any method to the v3 endpoint and the server validates it (an unknown method returns METHODNOTFOUNDEXCEPTION as a soft error on the AjaxResult, not an SDK throw). The recipes pick a surface per method by what reads cleanest for that data, not by a whitelist.
Field-naming choice
The newer CRM methods (crm.item.list) use camelCase fields: stageId, assignedById, opportunity, createdTime. The classic ones (tasks.task.add, crm.activity.list, crm.timeline.comment.add) use uppercase: TITLE, DESCRIPTION, OWNER_TYPE_ID. Recipes follow this split. Both are restApi:v2 — crm.item.* is camelCase for reasons of its own, not because it is v3; the v3 endpoint publishes no crm.item.*. Under restApi:v3 camelCase is the rule everywhere, and <entity>.field.list answers it per entity — see Discovering entity fields.
Running
# 1. Install: at the repo root or in a fresh project
pnpm add @bitrix24/b24jssdk
# Copying a single recipe out of this repo? Some recipes import a shared
# helper and need it copied alongside, keeping the ../lib/ path:
# recipes 1, 3, 6: lib/funnel.ts (baseStage)
# recipes 7, 12: lib/crypto.ts (safeEqual)
# Every other recipe is standalone.
# Recipe-specific deps as needed:
# recipe 4: pnpm add node-cron
# recipe 6: pnpm add grammy node-cron
# recipe 7: pnpm add express
# recipe 8: pnpm add openai
# recipe 9: pnpm add openai
# recipe 12: pnpm add express
# Recipe 12 also honours B24_OAUTH_STORE — where to write the OAuth token
# store. Defaults to .oauth-store.json in the working directory, which is
# rarely where portal credentials should live on a real server.
# 2. Set env
export B24_HOOK='https://YOUR_PORTAL.bitrix24.com/rest/1/k32t88gf3azpmwv3'
# 3. Run with tsx (no build step)
npx tsx examples/01-crm-analytics.ts
Caveats applied across recipes
- Multi-funnel pipelines: stage IDs may carry a category prefix (
C2:WON, C4:LOSE). Recipes 1, 3, and 6 strip the prefix when checking the base stage.
order in callList.make: silently dropped (the action forces cursorIdKey ASC, defaulting to idKey, for cursor stability). Use filter to narrow.
customKeyForResult: 'items' for crm.item.list, omit or 'tasks' for classic methods. Wrong value → silent empty array.
idKey / cursorIdKey: idKey: 'id' for crm.item.list; 'ID' (default) for classic methods. tasks.task.list is the exception on v2 — it returns lowercase id but sorts by ID, so use idKey: 'id', cursorIdKey: 'ID' (and customKeyForResult: 'tasks'); on v3 it is all-lowercase: idKey: 'id' (default), no cursorIdKey, customKeyForResult: 'items'.
- Error handling: failed calls throw
AjaxError for REST errors; recipes log and continue where it makes sense. Tune via setRestrictionManagerParams if you need different retry behaviour (see b24jssdk-core).
- Webhook events (recipe 7): registration of the outbound webhooks themselves (which events go where) is a one-off setup. Recipe 11 (
11-event-registration.ts) is a small CLI for that: list / bind / unbind via event.get / event.bind / event.unbind.
Cross-reference
For v3 wire-level details (filter grammar, batch $ref/$refArray, cursor pagination), use the b24jssdk-rest and b24jssdk-filtering skills.
1---2name: b24jssdk-recipes3description: End-to-end mini-apps built on the canonical b24jssdk actions.v{2,3}.* surface — CRM analytics, ERP sync, Telegram bot, mass mailing, task automation, AI assistant, web search + LLM, Disk files, webhook handler, error-handling cookbook, event registration, OAuth install handshake. Each recipe is a single TypeScript program using B24Hook on the server side. Load when the user asks for a working example or a starting template.4---56# b24jssdk recipes78Twelve end-to-end programs. Every recipe runs on `B24Hook` (Node.js), but each function body takes `$b24: TypeB24` so the same code works in-frame too — just swap the boot for `initializeB24Frame()`.910All recipes use the canonical **`$b24.actions.v{2,3}.*.make()`** surface. The legacy `callMethod` / `callBatch` / `callListMethod` / `fetchListMethod` was removed in 3.0.0 — do not generate code against it.1112| # | File | Stack | Scopes | What it does |13| --- | --- | --- | --- | --- |14| 1 | `examples/01-crm-analytics.ts` | Node | `crm` | Stream all deals via `actions.v2.fetchList.make`, group by stage, print a funnel report (counts, conversion %, avg ticket, win rate) |15| 2 | `examples/02-mass-messaging.ts` | Node | `crm`, `im` | Filter contacts via `actions.v2.call.make`, send `im.notify` to assigned managers |16| 3 | `examples/03-task-automation.ts` | Node, `setInterval` | `crm`, `task` | Poll deal stages with `actions.v2.fetchList.make`; on watched transition create a task via `actions.v3.call.make('tasks.task.add', …)` |17| 4 | `examples/04-erp-sync.ts` | Node, `node-cron` | `crm` | Two-way contact sync between Bitrix24 (via `actions.v2.*`) and a mock ERP |18| 5 | `examples/05-disk-files.ts` | Node | `disk` | Storages → root → create folder → list files, with a `actions.v2.batch.make` round-trip |19| 6 | `examples/06-telegram-bot.ts` | Node, `grammy`, `node-cron` | `crm` | Poll new deals via `actions.v2.call.make`, notify a Telegram chat |20| 7 | `examples/07-webhook-handler.ts` | Node, `express` | `crm`, `task` | Express server that receives Bitrix24 outbound events; loads details with `actions.v2.call.make`, then creates a follow-up task with an `idempotencyKey` so a redelivered event does not create it twice |21| 8 | `examples/08-ai-assistant.ts` | Node, `openai` | `crm`, `task` | Deal + activity timeline → GPT prompt → `actions.v3.call.make('tasks.task.add')` follow-up |22| 9 | `examples/09-web-search-llm.ts` | Node, BYOC | `crm` | Two-step RAG; SDK posts the answer to a deal's timeline via `actions.v2.call.make('crm.timeline.comment.add')` |23| 10 | `examples/10-error-handling.ts` | Node | any | Error-handling cookbook: AjaxError vs SdkError taxonomy; `hardErrorCodes` / `softErrorCodes` / `retryOnNetworkError` knobs via `setRestrictionManagerParams`; non-idempotent-call safety, with `idempotencyKey` as the v3 answer |24| 11 | `examples/11-event-registration.ts` | Node | `crm` | CLI tool — list / bind / unbind outbound webhook events (`event.get`, `event.bind`, `event.unbind`). Pairs with recipe 7. |25| 12 | `examples/12-oauth-install.ts` | Node, `express` | OAuth app | OAuth install handshake: handle `ONAPPINSTALL` / `ONAPPUPDATE` / `ONAPPUNINSTALL` events, persist tokens per portal, build `B24OAuth` on demand, refresh callback writes new tokens back to storage |2627## Shared library (`lib/`)2829Pure, I/O-free helpers extracted from recipes so they can be unit-tested without a live portal.3031`lib/` is for helpers where a second hand-written copy is a *defect* — a security32primitive, or logic with edge cases a test pins. It is not for shared plumbing: these33are recipes, and every extraction costs a reader the ability to lift one file and run34it. When in doubt, leave the code in the recipe.3536Worked example of the rule: `bootB24()` is byte-identical in eleven of the twelve37recipes and stays that way on purpose. It has no edge cases to pin and no security38weight, and it is the first thing a reader needs to see when they open a recipe —39hiding it behind an import would cost more than the duplication does.4041Recipes 06 and 12 end with a guard instead of a bare `main()` call:4243```ts44import { pathToFileURL } from 'node:url'4546if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {47 main().catch((e: unknown) => {48 // Raw console.error, so structured-logger formatting cannot hide the trace.49 console.error('\n[recipe failed]', e instanceof Error ? `${e.name}: ${e.message}` : String(e))50 process.exitCode = 151 })52}53```5455`npx tsx 06-telegram-bot.ts` still runs exactly as before — the guard is only false56when the file is *imported*, which is what the unit tests do to exercise `tick`'s57cursor logic and the uninstall token check without opening a Telegram connection or58binding a port. Keep it when copying those two recipes. Before adding it to a third, prefer the59`lib/` route above: extract the logic and test it there. Reach for the guard only when60the function cannot reasonably leave the recipe — `tick` closes over a live bot and the61poll cursor, `handleUninstall` is an Express handler — because two shapes of recipe is62already one more than ideal, and the guard spreading to all twelve would quietly retire63the `lib/` convention. The exported `function`s in those files are exported for the same64reason, and `export` is inert when the file runs directly.6566One deployment caveat: the guard compares `import.meta.url` against `process.argv[1]`.67Under an exotic launcher that rewrites either — a wrapper CLI, some loader shims — the68comparison can come out false, and the process would exit 0 having started nothing.69Check for the recipe's startup log line rather than trusting the exit code.7071| File | Exports | Used by |72| --- | --- | --- |73| `lib/funnel.ts` | `baseStage`, `analyseFunnel`, `DealRow`, `StageStat` | recipes 01, 03, 06 |74| `lib/crypto.ts` | `safeEqual` | recipes 07, 12 |75| `lib/portal-url.ts` | `checkPortalUrls` | recipe 12 |7677`baseStage(s)` strips the multi-funnel category prefix (`"C2:WON"` → `"WON"`), falling78back to the original string when the part after the colon is empty (`"C2:"` → `"C2:"`).79Import it from `lib/funnel` rather than re-declaring it in a recipe: 03 and 06 each80carried their own copy that returned `""` for that case. No recipe's control flow81actually differed — neither `""` nor `"C2:"` matches a stage key — but the copies had82drifted from the tested one unnoticed, and the next divergence might not be harmless. 83`analyseFunnel(deals)` groups deals by raw `stageId` key, summing counts and opportunity amounts.8485`safeEqual(a, b)` is a constant-time compare for secrets and tokens. It had the same86two-copy problem as `baseStage`, in recipes 07 and 12. Never re-declare this one: a87constant-time compare that drifts in one of two forks is a vulnerability that reads88like a refactor, and `===` on a token is the bug it exists to prevent.8990## Boot snippet (shared by all recipes)9192```ts93import { B24Hook, ConsoleV2Handler, LogLevel, Logger, type TypeB24 } from '@bitrix24/b24jssdk'9495export function bootB24(): TypeB24 {96 const url = process.env.B24_HOOK97 if (!url) throw new Error('B24_HOOK env var is required (incoming webhook URL)')98 const $b24 = B24Hook.fromWebhookUrl(url)99 return $b24100}101102// Recipes run under Node, so the console handler is created explicitly with103// `useStyles: false` — the browser factory emits ANSI-free CSS styling that a104// terminal prints as literal noise.105export const logger = Logger.create('Recipe')106logger.pushHandler(new ConsoleV2Handler(107 process.env.NODE_ENV === 'production' ? LogLevel.ERROR : LogLevel.INFO,108 { useStyles: false }109))110```111112The snippet is inlined into each recipe file for copy-paste convenience.113114## API-version split — quick reference115116Inside the recipes the split is:117118| Method | Action surface | Why |119| --- | --- | --- |120| `crm.item.{get,list,add,update,delete}` | `actions.v2.*` | Classic API used via v2 here |121| `crm.activity.list`, `crm.timeline.comment.add` | `actions.v2.*` | Classic API, v2 only |122| `tasks.task.{add,get,update,delete,list}` | **`actions.v3.*`** | Used via v3 (camelCase, `result.items`) |123| `disk.*`, `im.*`, `profile`, `user.*` | `actions.v2.*` | Classic API, v2 only |124| `main.eventlog.{list,get,tail}` | **`actions.v3.*`** | v3, incl. native `tail` |125126> The SDK no longer gates v3 by a hardcoded allowlist: `actions.v3.*` sends any method to the v3 endpoint and the server validates it (an unknown method returns `METHODNOTFOUNDEXCEPTION` as a soft error on the `AjaxResult`, not an SDK throw). The recipes pick a surface per method by what reads cleanest for that data, not by a whitelist.127128## Field-naming choice129130The newer CRM methods (`crm.item.list`) use camelCase fields: `stageId`, `assignedById`, `opportunity`, `createdTime`. The classic ones (`tasks.task.add`, `crm.activity.list`, `crm.timeline.comment.add`) use uppercase: `TITLE`, `DESCRIPTION`, `OWNER_TYPE_ID`. Recipes follow this split. Both are `restApi:v2` — `crm.item.*` is camelCase for reasons of its own, not because it is v3; the v3 endpoint publishes no `crm.item.*`. Under `restApi:v3` camelCase is the rule everywhere, and `<entity>.field.list` answers it per entity — see [Discovering entity fields](https://bitrix24.github.io/b24jssdk/docs/working-with-the-rest-api/discovering-entity-fields/).131132## Running133134```bash135# 1. Install: at the repo root or in a fresh project136pnpm add @bitrix24/b24jssdk137# Copying a single recipe out of this repo? Some recipes import a shared138# helper and need it copied alongside, keeping the ../lib/ path:139# recipes 1, 3, 6: lib/funnel.ts (baseStage)140# recipes 7, 12: lib/crypto.ts (safeEqual)141# Every other recipe is standalone.142# Recipe-specific deps as needed:143# recipe 4: pnpm add node-cron144# recipe 6: pnpm add grammy node-cron145# recipe 7: pnpm add express146# recipe 8: pnpm add openai147# recipe 9: pnpm add openai148# recipe 12: pnpm add express149# Recipe 12 also honours B24_OAUTH_STORE — where to write the OAuth token150# store. Defaults to .oauth-store.json in the working directory, which is151# rarely where portal credentials should live on a real server.152153# 2. Set env154export B24_HOOK='https://YOUR_PORTAL.bitrix24.com/rest/1/k32t88gf3azpmwv3'155156# 3. Run with tsx (no build step)157npx tsx examples/01-crm-analytics.ts158```159160## Caveats applied across recipes161162- **Multi-funnel pipelines**: stage IDs may carry a category prefix (`C2:WON`, `C4:LOSE`). Recipes 1, 3, and 6 strip the prefix when checking the base stage.163- **`order` in `callList.make`**: silently dropped (the action forces `cursorIdKey ASC`, defaulting to `idKey`, for cursor stability). Use `filter` to narrow.164- **`customKeyForResult`**: `'items'` for `crm.item.list`, omit or `'tasks'` for classic methods. Wrong value → silent empty array.165- **`idKey` / `cursorIdKey`**: `idKey: 'id'` for `crm.item.list`; `'ID'` (default) for classic methods. `tasks.task.list` is the exception **on v2** — it returns lowercase `id` but sorts by `ID`, so use `idKey: 'id', cursorIdKey: 'ID'` (and `customKeyForResult: 'tasks'`); on **v3** it is all-lowercase: `idKey: 'id'` (default), no `cursorIdKey`, `customKeyForResult: 'items'`.166- **Error handling**: failed calls throw `AjaxError` for REST errors; recipes log and continue where it makes sense. Tune via `setRestrictionManagerParams` if you need different retry behaviour (see `b24jssdk-core`).167- **Webhook events** (recipe 7): registration of the outbound webhooks themselves (which events go where) is a one-off setup. Recipe 11 (`11-event-registration.ts`) is a small CLI for that: `list` / `bind` / `unbind` via `event.get` / `event.bind` / `event.unbind`.168169## Cross-reference170171For v3 wire-level details (filter grammar, batch `$ref`/`$refArray`, cursor pagination), use the `b24jssdk-rest` and `b24jssdk-filtering` skills.