---
title: "Recipe: Bulk update deals"
description: "Migrate thousands of CRM deals to a new stage using BatchByChunkV2 — automatic chunking, partial-error handling, and a single progress line."
canonical_url: "https://bitrix24.github.io/b24jssdk/docs/examples/bulk-update-deals"
last_updated: "2026-06-02"
---
# 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.

> [!WARNING]
> 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`:```csv
dealId,newStageId
1234,WON
1235,WON
1240,LOSE
```

```bash
pnpm add @bitrix24/b24jssdk
export B24_HOOK="https://your-portal.bitrix24.com/rest/1/xxxxxxxxxxxxxxxx/"
```

## Full Example

`scripts/migrate-deals.mjs`:

```js
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

```bash
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

- [`BatchByChunkV2.make()`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/batch-by-chunk-rest-api-ver2.md) — full method reference.
- [`BatchV2.make()`](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/batch-rest-api-ver2.md) — when you have ≤ 50 calls and want named results.
- [Restrictions System](https://bitrix24.github.io/b24jssdk/raw/docs/working-with-the-rest-api/limiters.md) — choosing a `restrictionParams` preset.

## Sitemap

See the full [sitemap](/b24jssdk/sitemap.md) for all pages.
