Guide

Create DOCX redlines in Node.js

Create a tracked replacement in a Node.js job. Save the result, reopen it, and inspect revision metadata.

4 min read

Read contract.docx, change a payment deadline from 7 to 30 days, and write contract.redlined.docx. The output keeps both wordings as tracked changes, so a reviewer can accept or reject the edit.

This guide provides a Node.js script you can run against the sample document.

The browser example uses the same tracked-edit operation as the server script. It does not run Node.js on this page. Use its input download to reproduce the steps locally.

Before you begin

Use Node.js ^20.16.0 || >=22.3.0. Select Download input DOCX from the demo into a working directory. Its filename is contract.docx, and it contains within 7 days exactly once. Keep the phrase within one ordinary paragraph for this example.

Install the editing API and its engine peer dependency:

npm install @docx-editor.dev/editor-api @docx-editor.dev/core

Use matching package versions. The editing API uses the EigenPal Pro License. Review evaluation and production terms before deployment.

Create a tracked replacement

Save this example as redline.mjs. You can also download the source:

import { readFile, writeFile } from "node:fs/promises";
import { DocxEditor } from "@docx-editor.dev/editor-api";
 
const input = await readFile("contract.docx");
const runtime = await DocxEditor.createServer(input, {
  author: "Contract review agent",
});
 
try {
  await runtime.run(async (context) => {
    const matches = context.document.body.search("within 7 days", {
      matchCase: true,
    });
    matches.load("items");
    await context.sync();
    if (matches.items.length !== 1) {
      throw new Error("Expected exactly one deadline clause.");
    }
    context.document.changeTrackingMode = "TrackMineOnly";
    matches.items[0].insertText("within 30 days", "Replace");
    await context.sync();
  });
  await writeFile("contract.redlined.docx", await runtime.save());
} finally {
  runtime.dispose();
}

Run the example:

node redline.mjs

The output contains a pending tracked replacement. The input file stays unchanged.

TrackMineOnly applies to this runtime. It does not set a document-wide policy for other editors or reviewers. Without it, API edits are untracked.

Inspect the saved output

Save this script as inspect-redlines.mjs to reopen the output. The source file matches this example:

import { readFile } from "node:fs/promises";
import { DocxEditor } from "@docx-editor.dev/editor-api";
 
const runtime = await DocxEditor.createServer(
  await readFile("contract.redlined.docx"),
);
try {
  const changes = await runtime.run(async (context) => {
    const revisions = context.document.revisions;
    revisions.load("items");
    await context.sync();
    for (const revision of revisions.items) {
      revision.load(["author", "type"]);
    }
    await context.sync();
    return revisions.items.map((revision) => ({
      author: revision.author,
      type: revision.type,
    }));
  });
  console.log(changes);
} finally {
  runtime.dispose();
}

The main-story revisions should include the configured author. Do not assume one replacement produces exactly one revision object.

Test accepted and rejected copies

Run the review-copies.mjs example against contract.redlined.docx:

node review-copies.mjs

The script creates two copies from the same redline:

OutputExpected payment textPending revisions
contract.accept.docxwithin 30 daysNone
contract.reject.docxwithin 7 daysNone

It reopens each saved copy and asserts these results. It also checks that the delivery clause still contains within 14 days. This catches serialization problems that an in-memory check would miss.

The script resolves all main-story revisions because the sample has only one proposed replacement. For a review interface, resolve a selected review card.

Save only if the source version matches

The file script writes a separate artifact. A stored contract needs an additional check before you replace it. Someone can edit the source while your job runs.

Read the document bytes and their version together:

SELECT content, version::text AS version
FROM contracts WHERE id = $1;

Create the runtime from those bytes. Keep that version until the job finishes. For PostgreSQL, include it in the update condition:

/** The caller must authorize documentId before calling this function. */
export async function publishIfCurrent(
  pool,
  documentId,
  sourceVersion,
  output,
) {
  const result = await pool.query(
    `UPDATE contracts
     SET content = $1, version = version + 1
     WHERE id = $2 AND version = $3
     RETURNING version::text AS version`,
    [Buffer.from(output), documentId, sourceVersion],
  );
  if (result.rowCount !== 1) {
    throw new Error("The document changed or was removed. Review it again.");
  }
  return result.rows[0].version;
}

This helper expects a pg pool and an authorized document ID. The table needs id uuid PRIMARY KEY, version bigint NOT NULL, and content bytea NOT NULL. Pass the result of runtime.save() as output. Every writer must increment version when changing the document. Keep bigint versions as strings to avoid JavaScript integer rounding.

If another writer updates the row first, the conditional update returns no row. Keep your output separate and review the changed source again. Do not retry with a newer version number and the same output.

Download publish-if-current.mjs for the storage helper. PostgreSQL documents the concurrent update behavior.

Test this against an isolated database row before connecting your job queue:

  1. Read one source version into two jobs.
  2. Save the first job with that version. Confirm the version increases.
  3. Save the second job with the original version. Confirm the update fails.
  4. Read the row again. Confirm the first job's content remains.

The example checks use a fake pool. They do not verify PostgreSQL integration.

Verify the review workflow

Test the output in Microsoft Word:

  1. Open the output in Microsoft Word and inspect the revision author.
  2. Accept the replacement in a copy. Confirm the revised deadline remains.
  3. Reject the replacement in another copy. Confirm the original deadline returns.
  4. Save and reopen both copies. Compare tables, numbering, and unrelated text.

Record the input, outputs, package version, Word version, and visible differences. Keep these results separate from rendering comparisons.

Handle refusals

FailureJob behavior
Source file cannot openStop before creating output; report a document input error
Search finds zero or several matchesRequest a narrower or refreshed proposal
Tracked operation is unsupportedReport it; do not write an untracked replacement
Source changes before publicationKeep the output separate and request another review
Save or storage write failsLeave the source untouched; record an incomplete job

A missing or repeated match stops this script before the write. An unsupported tracked edit can also fail. Do not retry it with tracking disabled. A failed sync preserves earlier successful batches, if your job contains several.

For target validation, see validate AI redlining proposals. For a review interface, see accept and reject Word tracked changes.

.docx in. .docx out.

Open a real document in the editor, then wire it into your app. It all runs client-side.