﻿Source: https://chiselbase.com/guides/bulk-update-firestore-documents/

Published 2026-10-04

# Firestore Bulk Update: Change Many Documents with a Dry Run

Bulk update Firestore documents with a paginated Node script, a dry run and conflict checks. Learn when to use batches, how to rename fields and how Chiselbase previews changes.

By [Chiselbase](https://chiselbase.com/about/)  · Explore the Firestore GUI: [Firestore](https://chiselbase.com/firestore/)

## Change a field across a collection without replacing documents

Seven orders still say `shipped`, but they should say `delivered`. Or a CSV import left hundreds of products with the wrong category. You need to update matching documents, preserve their other fields and find out what will change before running a migration.

For Firestore Core operations, the practical approach is to query the documents and update their references. This guide uses a status correction to show pagination, a dry run, concurrent-change protection and partial failure reporting. It also covers field renames and the visual workflow.

> **Want to see the before and after values?** Chiselbase combines the query result with a bulk edit dialog and requires a dry run before execution. You can inspect a sample diff without writing a migration script. [See the Chiselbase workflow](https://chiselbase.com/guides/bulk-update-firestore-documents/#way-3-with-chiselbase).

## Way 1: pick the right Firestore write operation

Use `update()` to change individual fields. A plain `set()` replaces the document unless you request a merge. For nested values, a field path such as `shipping.status` updates that leaf; writing the whole `shipping` map can replace its other entries. See Firebase’s [field update examples](https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects).

| Operation | Good fit | What to account for |
| --- | --- | --- |
| Write batch | A bounded set that must succeed or fail together | Request size and index updates; no reads inside the batch |
| Transaction | An update that depends on current values | Reads before writes; callbacks can run again |
| BulkWriter | Many independent document updates | Partial success is possible; inspect each write result |

Firebase recommends a [server library with parallel writes or a bulk writer](https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes) for large workloads. Adding every document to one batch is not a solution for an arbitrarily large collection.

**Using Enterprise Pipeline operations?** Firestore also has [Pipeline update and delete stages](https://firebase.google.com/docs/firestore/pipelines/dml). Those require Enterprise edition. The script below uses Core operations and does not require an Enterprise database.

## Way 2: a paginated Node script with a dry run

This example changes only `orders` where `status == shipped`. It prints the first five document paths and the planned patch, counts all matches, and writes only with `--write`. Edit `FILTER` and `PATCH` for your task; set `FILTER` to `null` to update all documents in the chosen collection.

Use Node.js 22 or later. The example was checked with `firebase-admin` 14.5.0. Run these commands in a fresh folder. The credential command is Bash; in PowerShell use the alternative below. Replace the project ID and key path.

Terminal window

```bash
npm init -y
npm install firebase-admin
export GOOGLE_APPLICATION_CREDENTIALS="$HOME/keys/my-project-writer.json"
```

PowerShell: `$env:GOOGLE_APPLICATION_CREDENTIALS = "C:\keys\my-project-writer.json"`. The service account needs write access, for example `roles/datastore.user`. Admin SDK access uses IAM and bypasses Firestore Security Rules.

Save the script as `bulk-update-firestore.mjs`:

### Show bulk-update-firestore.mjs — complete script

bulk-update-firestore.mjs

```js
import { initializeApp, applicationDefault } from "firebase-admin/app";
import { getFirestore, FieldPath } from "firebase-admin/firestore";

const FILTER = ["status", "==", "shipped"]; // null = all documents
const PATCH = { status: "delivered" }; // update fields, not the entire document
const PAGE_SIZE = 200;

const [projectId, collectionPath, flag, ...extra] = process.argv.slice(2);
if (!projectId || !collectionPath || extra.length || (flag && flag !== "--write")) {
  console.error("Usage: node bulk-update-firestore.mjs <projectId> <collectionPath> [--write]");
  process.exit(1);
}
const write = flag === "--write";
initializeApp({ projectId, credential: applicationDefault() });
const db = getFirestore(); // (default) database
let query = db.collection(collectionPath).orderBy(FieldPath.documentId());
if (FILTER) query = query.where(...FILTER);
const writer = write ? db.bulkWriter() : null;
let cursor;
let matched = 0;
let updated = 0;
let failed = 0;

try {
  while (true) {
    const page = await (cursor ? query.startAfter(cursor) : query).limit(PAGE_SIZE).get();
    if (page.empty) break;
    const pending = [];
    for (const doc of page.docs) {
      if (matched < 5) {
        console.log(JSON.stringify({ path: doc.ref.path, patch: PATCH }));
      }
      matched += 1;
      if (writer) {
        pending.push(writer.update(doc.ref, PATCH, { lastUpdateTime: doc.updateTime })
          .then(() => { updated += 1; })
          .catch((error) => {
            failed += 1;
            console.error(`${doc.ref.path}: ${error.code} ${error.message}`);
          }));
      }
    }
    // Attach error handlers immediately; bound outstanding writes to one page.
    if (writer) await writer.flush();
    await Promise.all(pending);
    cursor = page.docs.at(-1);
  }
} finally {
  if (writer) await writer.close();
  await db.terminate();
}
console.log(JSON.stringify({ mode: write ? "write" : "dry-run", matched, updated, failed }));
if (failed) process.exitCode = 1;
```

First preview, then explicitly write:

Terminal window

```bash
node bulk-update-firestore.mjs PROJECT_ID orders
node bulk-update-firestore.mjs PROJECT_ID orders --write
```

The dry run reads documents but writes nothing. A seven-document preview ends with:

```json
{"mode":"dry-run","matched":7,"updated":0,"failed":0}
```

### Why the conflict check and pagination matter

`lastUpdateTime` makes a write fail if the document changed after the script read it. A conflict is reported rather than silently overwriting that newer version. BulkWriter retries some transient failures by default; the script catches each terminal failure because `close()` itself does not reject for individual write failures. These behaviors are documented in the [BulkWriter reference](https://docs.cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/bulkwriter).

Pages use document ID order rather than an offset or the field being changed. Updating `status` does not move a document’s ID cursor. Keep that ordering when adapting the example. Additional filters may require an index; Firestore returns an index creation link if one is missing.

The dry run and write run are separate reads. They can match different documents if the data changes between them. The update-time check protects the read-to-write interval within the write run; it does not bind execution to the earlier preview. Neither the scan nor the entire migration is one atomic snapshot. Coordinate with application writers if you need a stable migration window.

### Rename a Firestore field without losing its value

There is no field-rename operation in the Core document API. Copy the old value to the new field and delete the old field in the same document update. Decide what happens if the target already exists.

Here is a transaction for one document. It preserves the value’s Firestore type and skips a document if `displayName` already exists, including when it is `null`. Use it with an initialized Admin `db`; replace the document path. It writes when you execute it.

```js
// With the initialized Admin db from your application or migration:
const { FieldValue } = await import("firebase-admin/firestore");
const ref = db.doc("users/user1");
await db.runTransaction(async (tx) => {
  const doc = await tx.get(ref);
  if (!doc.exists) return;
  const oldValue = doc.get("display_name");
  if (oldValue === undefined || doc.get("displayName") !== undefined) return;
  tx.update(ref, {
    displayName: oldValue,
    display_name: FieldValue.delete(),
  });
});
```

For a live application, deploy readers that accept both names, migrate the data, verify it, and then remove the fallback and old writers. Renaming a data field does not update your queries, indexes, Security Rules or application code. To remove an obsolete field without renaming it, use `FieldValue.delete()` in an update; this does not delete the document or its subcollections.

## Way 3: with Chiselbase

For an occasional correction, the useful part is seeing the query, document values and planned changes together. [Chiselbase’s Firestore GUI](https://chiselbase.com/firestore/) keeps that workflow in the desktop app:

1.  Open the correct connection and query `orders` with `status == shipped`. Keep the ordering on document ID when changing the status field.
2.  Open **Bulk edit**. Mark individual rows first to restrict the operation to those documents. **With no rows marked, the operation uses all query matches and removes the result limit.** A visible page of seven rows does not necessarily mean seven targets.
3.  Add a **Set** action for `status`, choose a string value and enter `delivered`.
4.  Run **Dry run**. Check the target count, errors and sample before/after values. The sample is a preview, not a diff of every document.
5.  Execute after reviewing the connection and scope. Check the result for partial failures, then rerun the query to verify the data.

· bulk edit · orders · dry run

 

Screenshot: Bulk edit dialog setting the field status to delivered on all orders matching the query. The dry run shows 7 checked, 7 would change, and five sample rows with shipped before and delivered after.

Setting status from shipped to delivered, after the dry run. 7 checked, 7 would change. “Dry run, nothing written.”

The same dialog offers **Rename** and **Delete** field actions, conversions and transforms. A rename can overwrite an existing target value; inspect that target first if your migration requires preserving it. The transaction above implements a skip-on-collision policy instead.

Bulk edit is a [Pro feature](https://chiselbase.com/pricing/). Completed runs can be undone when a local snapshot was saved; metadata-only and none snapshot policies disable undo. Check your policy before a migration and keep a suitable [backup](https://chiselbase.com/guides/back-up-firestore-and-restore/) for recovery. A dry run does not lock the matching documents, and a bulk operation can partially succeed.

## Which way to pick

| Situation | Start with | Reason |
| --- | --- | --- |
| One status correction you want to inspect | Chiselbase bulk edit | Query, target count and sample diff in one workflow |
| A migration you need to rerun in CI | Node script | Version the filter, patch and error handling |
| A rename with a skip-on-collision policy | Transaction | Read and decide against the current document |
| Several writes that must be atomic together | A bounded batch or transaction | BulkWriter does not make the whole migration atomic |

## Costs, scope and verification

The script reads the matching documents on each pass and writes each matching document in write mode. A preview followed by execution repeats the reads. Firestore read, write and applicable index-entry charges apply; even a query with no results has a minimum read charge. SDK writes can trigger Cloud Functions. See [Firestore pricing](https://firebase.google.com/docs/firestore/pricing).

Only the specified collection is scanned; nested subcollections are untouched. The script uses the `(default)` database. A partial run is not automatically rolled back. With the sample filter, already-delivered orders no longer match on a rerun; this does not make arbitrary increments or other adapted patches safe to repeat.

Verification should include an affected document, an excluded document and the remaining `shipped` query. Check preserved fields as well as the updated value. For a rename, also audit missing and mixed fields with the [Firestore schema guide](https://chiselbase.com/guides/firestore-missing-fields-and-schema/).

## Frequently asked questions

### How do I update all documents in Firestore?

Choose a collection and set `FILTER` to `null` in the example. That updates every document found in that collection, not every collection in the database. Preview the scope first; collections and subcollections require their own deliberate selection.

### Is a Firestore batch update the same as BulkWriter?

No. A write batch is an atomic group of writes. BulkWriter schedules independent writes and handles retries; some writes can succeed while others fail. Pick based on the atomicity your task needs.

### Can I preview a migration without write permissions?

The script’s dry run needs read access and sends no writes. It does not prove that the later writes will pass IAM checks, request limits or other server validation.

## Sources and example checks

Reviewed on October 4, 2026 against Firebase’s [field updates](https://firebase.google.com/docs/firestore/manage-data/add-data), [transactions and batches](https://firebase.google.com/docs/firestore/manage-data/transactions), the [Node BulkWriter reference](https://docs.cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/bulkwriter) and [Enterprise Pipeline mutations](https://firebase.google.com/docs/firestore/pipelines/dml). Chiselbase steps were checked against its query selection and bulk edit implementation.

The published Node code was exercised against the local Firestore emulator for pagination, dry runs, preservation of unrelated fields and failure reporting. The rename transaction was checked for missing sources, null values and target collisions. Emulator checks do not validate production IAM, billing, indexes or contention at production scale.

## Keep working with Firestore

[All guides](https://chiselbase.com/guides/)

-   [Find missing fields and mixed types Audit your schema and explain why queries leave records out.](https://chiselbase.com/guides/firestore-missing-fields-and-schema/)
-   [Back up and restore Firestore Plan your recovery with schedules, PITR and file exports.](https://chiselbase.com/guides/back-up-firestore-and-restore/)
-   [Copy production data to staging Review targets, references and subcollections before copying.](https://chiselbase.com/guides/copy-firestore-data-between-projects/)

Chiselbase

### Work with your data in one place.

Browse collections, preview imports and review migrations in a desktop Firestore GUI.

[Download Chiselbase](https://chiselbase.com/download/)

[Explore the Firestore GUI](https://chiselbase.com/firestore/) [See Free and Pro](https://chiselbase.com/pricing/)
