Create DOCX redlines in Node.js
Create a tracked replacement in a Node.js job. Save the result, reopen it, and inspect revision metadata.
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/coreUse 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.mjsThe 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.mjsThe script creates two copies from the same redline:
| Output | Expected payment text | Pending revisions |
|---|---|---|
contract.accept.docx | within 30 days | None |
contract.reject.docx | within 7 days | None |
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:
- Read one source version into two jobs.
- Save the first job with that version. Confirm the version increases.
- Save the second job with the original version. Confirm the update fails.
- 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:
- Open the output in Microsoft Word and inspect the revision author.
- Accept the replacement in a copy. Confirm the revised deadline remains.
- Reject the replacement in another copy. Confirm the original deadline returns.
- 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
| Failure | Job behavior |
|---|---|
| Source file cannot open | Stop before creating output; report a document input error |
| Search finds zero or several matches | Request a narrower or refreshed proposal |
| Tracked operation is unsupported | Report it; do not write an untracked replacement |
| Source changes before publication | Keep the output separate and request another review |
| Save or storage write fails | Leave 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.
