v1.2.0

Recipe: Bulk update deals

Migrate thousands of CRM deals to a new stage using BatchByChunkV2 — automatic chunking, partial-error handling, and a single progress line.
We are still updating this page. Some data may be missing here — we will complete it shortly.

Goal

Read a CSV mapping of dealId,newStageId and apply the stage change to every row. Could be hundreds of deals or tens of thousands — BatchByChunkV2.make() chunks the calls into 50-per-batch blocks server-side, no manual paging.

Stack & Prerequisites

  • Node.js 20+, ESM
  • @bitrix24/b24jssdk@^1.0.0
  • A webhook with the crm scope, set as B24_HOOK.
  • A CSV file migrations.csv:
    dealId,newStageId
    1234,WON
    1235,WON
    1240,LOSE
    
pnpm add @bitrix24/b24jssdk
export B24_HOOK="https://your-portal.bitrix24.com/rest/1/xxxxxxxxxxxxxxxx/"

Full Example

scripts/migrate-deals.mjs:

import { readFile } from 'node:fs/promises'
import {
  B24Hook,
  EnumCrmEntityTypeId,
  ParamsFactory
} from '@bitrix24/b24jssdk'

const hookUrl = process.env.B24_HOOK
if (!hookUrl) {
  console.error('B24_HOOK is not set')
  process.exit(2)
}

const csv = await readFile(process.argv[2] ?? 'migrations.csv', 'utf8')
const rows = csv
  .split(/\r?\n/)
  .slice(1) // header
  .filter(Boolean)
  .map((line) => {
    const [dealId, newStageId] = line.split(',')
    return { dealId: Number(dealId), newStageId }
  })

console.log(`planned updates: ${rows.length}`)

const b24 = B24Hook.fromWebhookUrl(hookUrl, {
  restrictionParams: ParamsFactory.getBatchProcessing()
})

const calls = rows.map(({ dealId, newStageId }) => [
  'crm.item.update',
  {
    entityTypeId: EnumCrmEntityTypeId.deal,
    id: dealId,
    fields: { stageId: newStageId }
  }
])

const response = await b24.actions.v2.batchByChunk.make({
  calls,
  options: {
    isHaltOnError: false,
    requestId: `deal-stage-migration-${Date.now()}`
  }
})

if (!response.isSuccess) {
  console.error('partial failure:')
  for (const message of response.getErrorMessages()) {
    console.error(`  - ${message}`)
  }
  process.exit(1)
}

console.log(`updated: ${response.getData()?.length ?? 0} of ${rows.length}`)

Run It

node scripts/migrate-deals.mjs migrations.csv
# planned updates: 3450
# updated: 3450 of 3450

If a few rows fail (deleted deal, missing scope, invalid stage), isHaltOnError: false lets the rest of the batch finish and the script exits with code 1 after listing the failures.

How It Works

  • BatchByChunkV2.make({ calls }) accepts an arbitrary number of commands and slices them internally into batches of 50, the per-request hard limit. Output order matches input order.
  • Result.isSuccess flips to false if any chunk had an error. getErrorMessages() returns one message per failed call. Successful calls are still in getData() — partial progress is preserved.
  • ParamsFactory.getBatchProcessing() raises the operating-time threshold and lowers the request rate, which keeps the portal queue responsive while a long migration runs alongside live UI traffic.
  • Naming the request via options.requestId makes it easy to filter portal-side logs and to deduplicate accidental re-runs.

Limitations

  • The two-tuple calls form is required: BatchByChunkV2 deliberately rejects named-object commands because chunk boundaries would split the names apart.
  • Each command counts against the daily method-call budget. For tens of thousands of updates, schedule the script to run during off-peak hours and consider splitting by responsible user.
  • Bitrix24's crm.item.update validates stageId against the deal's category. A typo in newStageId returns a per-call error — visible in getErrorMessages() — but the whole script does not abort thanks to isHaltOnError: false.
  • Atomic rollback is not supported. If you need transactional behavior, dump current values into a separate CSV first and run a second migration to restore.

See also