Turn AI proposals into targeted DOCX redlines
Validate a model-proposed payment deadline, target the correct clause, and create a tracked DOCX change for human review.
A contract has two deadlines: payment within 7 days and delivery within 7 days. Your agent recommends more time for invoice review. It must change payment without touching delivery.
This guide connects a model tool response to a tracked edit. Your application selects the clause and permits payment terms from 15 to 45 days. The model proposes a deadline and explains it. Your code validates the proposal before editing.
Enter 90 and select Validate and create redline. The policy check refuses the edit. Try 21 or 30 instead. Only the payment clause changes; the delivery deadline stays at 7 days. Use the review card to accept or reject the change, then download the DOCX.
You enter the proposal in this demo. In an agent integration, the model's tool response supplies those two fields. The demo and the tests use the same validator and editing function shown below.
Keep the editing task in your application
Capture the selected clause before asking the model for a recommendation. Keep its source document version with the job that requested the review. Do not ask the model to return the version or choose which file to edit.
For this sample, the application selects the payment clause and sets the policy:
const task = {
clause: "The customer pays the invoice within 7 days.",
source: "within 7 days",
minDays: 15,
maxDays: 45,
};In a larger contract, obtain clause from the reviewer's selection or your clause extraction step.
Keep the exact text, rather than a model's paraphrase. The editing function uses it to locate the selected passage again.
The deadline range is a sample application policy. It does not interpret legal terms. For a different clause, define both the allowed output and the validation rule for that task.
Ask for a proposal, not an editing command
Install the API, engine, and schema validator:
npm install @docx-editor.dev/editor-api @docx-editor.dev/core zodFor the tool example below, also install ai and your chosen model provider.
The agent integration guide
shows how the tool catalog connects to a model loop.
The model returns only the proposed wording and an explanation:
{
"replacement": "within 30 days",
"reason": "Allow time for invoice review and approval."
}The tool uses the same schema as the validator. Save the next section as redlining-proposal.ts,
then import its schema into your tool catalog:
import { tool } from "ai";
import { proposalSchema } from "./redlining-proposal";
export const redliningTools = {
propose_replacement: tool({
description: "Propose a payment deadline for the selected clause.",
inputSchema: proposalSchema,
}),
};Send the selected clause, permitted range, and reviewer's request with your model call. For example: “Recommend a payment deadline between 15 and 45 days to allow invoice review. Return the proposed wording and explain your choice.”
The range check runs in application code even if the prompt states the range. A tool schema validates the response shape; it cannot establish that a proposed edit fits your policy.
Validate and edit the selected clause
This is the source used by the demo. It accepts any whole-number deadline within the policy range, rather than one predetermined answer. It then searches within the selected clause, so a matching deadline in the delivery clause is excluded.
import { z } from "zod";
import type { DocxEditorRuntime } from "@docx-editor.dev/editor-api/browser";
export const proposalSchema = z.object({
replacement: z.string().min(1).max(100),
reason: z.string().trim().min(1).max(500),
}).strict();
export type PaymentTask = {
clause: string;
source: string;
minDays: number;
maxDays: number;
};
export function validatePaymentProposal(value: unknown, task: PaymentTask) {
const result = proposalSchema.safeParse(value);
if (!result.success) {
throw new Error("Provide replacement text and a reason of 1–500 characters, with no extra fields.");
}
const proposal = result.data;
const match = /^within ([1-9]\d*) days$/.exec(proposal.replacement);
const days = match ? Number(match[1]) : NaN;
if (!Number.isSafeInteger(days) || days < task.minDays || days > task.maxDays) {
throw new Error(`Use a payment deadline between ${task.minDays} and ${task.maxDays} days.`);
}
return proposal;
}
export async function applyPaymentProposal(
runtime: Pick<DocxEditorRuntime, "run">,
value: unknown,
task: PaymentTask,
) {
const proposal = validatePaymentProposal(value, task);
await runtime.run(async (context) => {
const clauses = context.document.body.search(task.clause, { matchCase: true });
clauses.load("items");
await context.sync();
if (clauses.items.length !== 1) {
throw new Error("The payment clause changed or is not unique. Select it again.");
}
const matches = clauses.items[0]!.search(task.source, { matchCase: true });
matches.load("items");
await context.sync();
if (matches.items.length !== 1) {
throw new Error("The selected clause needs one matching deadline.");
}
context.document.changeTrackingMode = "TrackMineOnly";
matches.items[0]!.insertText(proposal.replacement, "Replace");
await context.sync();
});
return proposal;
}The function performs two searches: one for the complete payment clause, then one for the deadline inside it. If the complete clause occurs twice, the function stops. Ask the reviewer to select a narrower scope instead of choosing the first match. If the captured text no longer appears exactly once, the search also stops. This text check does not detect added sentences around an otherwise unchanged passage.
TrackMineOnly records the replacement as a tracked change. Passing validation does not accept that change.
The reviewer still decides whether to keep the new wording.
Connect the tool result to your open editor
Create one browser runtime for the attached editor, then pass each tool result to the helper.
This excerpt assumes editor is the instance from useDocxEditor() and toolInput is the untrusted tool response:
import { DocxEditor } from "@docx-editor.dev/editor-api/browser";
import { applyPaymentProposal } from "./redlining-proposal";
const runtime = DocxEditor.createBrowser(editor, {
author: "Contract review agent",
revisionTextView: "allMarkup",
});
try {
const proposal = await applyPaymentProposal(runtime, toolInput, task);
console.log(proposal.reason);
} finally {
runtime.dispose();
}Show proposal.reason beside the proposed edit in your application.
The helper does not store the explanation as a Word comment.
For the editor host and per-card controls, see build review controls in React.
Handle changes during the review
Check for outdated state at each stage:
| Change | How to detect it | Next step |
|---|---|---|
| The captured text is removed, changed, or duplicated | The exact-text search returns zero or multiple matches | Read the clause again and request another proposal |
| A runtime edit invalidates a range between API batches | The API reports StaleDocument | Search again and check the new text before editing |
| Another job saves the stored document during processing | A conditional save returns no updated row | Keep the output separate and review the new stored version |
A job-local server runtime can invalidate its own ranges through an earlier edit. A browser or collaborative runtime can also change through another writer. Do not reuse a range after a dependent edit; obtain a fresh one. See batching and errors for the runtime behavior.
For background jobs, read the DOCX bytes and version together before the model call. Keep that version in your job record, not in the model response. Pass it to the conditional save after serialization. The text search protects the editing target; the conditional save protects the stored document.
Test the actual helper
The automated tests call this helper against the real server runtime and inspect the saved DOCX. They cover both policy and targeting:
| Input | Expected result |
|---|---|
| 15, 21, 30, or 45 days | Payment gets a tracked replacement; delivery remains unchanged |
| 14, 46, or 90 days | No document edit |
| Empty reason, extra command, or malformed wording | Schema or policy rejection |
| Same deadline in payment and delivery | Only payment changes |
| Missing or duplicated payment clause | No document edit |
Use these cases when adding the helper to your own test suite. For saved acceptance and rejection checks, use the Node.js tutorial.
