A useful document agent must produce more than paragraphs. The result needs real Word styles, numbering, fields, tables, headers, comments, and revisions. Those structures must survive editing and a DOCX save.
DOCX Editor separates model decisions from document operations:
- Your model chooses an app-owned tool and supplies structured input.
@docx-editor.dev/editor-apireads and changes the document.@docx-editor.dev/reactpresents the editable DOCX.@docx-editor.dev/proadds comments, tracked changes, and review controls.
The model never generates OOXML. Your code owns the model, prompt, tool permissions, validation, and user interface.
For the current walkthrough, see Build a DOCX agent.
Try the agent
Start with one of the sample requests. The demo creates a structured DOCX in stages. After the draft finishes, choose a redlining prompt to see tracked changes that a user can accept or reject.
The demo uses Vercel AI SDK, but the document tools work with any tool-calling framework.
Keep the model behind a tool boundary
Give the model a small catalog. Each tool should express one document intent, validate its input, and call deterministic document code.
import { tool } from "ai";
import { z } from "zod";
const paragraphId = z.string().min(1);
export const writerTools = {
create_document: tool({
description: "Replace the body with styled paragraphs.",
inputSchema: z.object({
title: z.string().min(2),
blocks: z.array(
z.object({
text: z.string().min(1),
style: z.enum([
"Title",
"Subtitle",
"Heading 1",
"Heading 2",
"Quote",
"Normal",
]),
}),
),
}),
}),
propose_replacement: tool({
description: "Propose a tracked replacement over an exact phrase.",
inputSchema: z.object({
paragraphId,
search: z.string().min(1),
replaceWith: z.string(),
}),
}),
};The tool names form a permission boundary. A review-only agent can receive read and comment tools. A drafting agent can receive direct-write and tracked-change tools without receiving commands that accept its own proposals.
Build the draft in stages
A large write_document call makes failures hard to isolate. It also asks the
model to understand too much DOCX structure. The demo uses five stages:
create_documentreplaces the body with styled paragraphs and returns stable paragraph IDs.format_listsapplies native bullet and numbering definitions.insert_content_controlswraps exact placeholders in typed fields.insert_tablecreates the grid and populates each cell paragraph.write_header_footerreplaces document furniture and adds page fields.
Each later tool addresses paragraphs returned by the first tool. The model chooses the content and intent. The client controls the order and stops on a typed refusal.
Connect the tool loop
The server streams model output and tool calls. Browser tools omit an
execute function because the server cannot reach the editor instance in the
user's tab.
import {
convertToModelMessages,
stepCountIs,
streamText,
} from "ai";
const result = streamText({
model,
system,
messages: await convertToModelMessages(messages),
tools: writerTools,
stopWhen: stepCountIs(16),
});
return result.toUIMessageStreamResponse();The chat client receives each tool call, runs it against the browser runtime, and returns the tool result. The result tells the model whether to continue, retry with corrected input, or explain a refusal.
Keep a hard step limit. Also queue document writes on the client. Parallel model tool calls must not race against the same document state.
Edit a DOCX file on a server
The server runtime takes DOCX bytes and returns edited DOCX bytes. It needs no Microsoft Word process or browser Document Object Model (DOM).
import { DocxEditor } from "@docx-editor.dev/editor-api";
const runtime = await DocxEditor.createServer(bytes, {
author: "Contract agent",
});
try {
await runtime.run(async (context) => {
const matches = context.document.body.search("$50k");
matches.load("items");
await context.sync();
for (const match of matches.items) {
match.insertText("$100k", "Replace");
}
await context.sync();
});
const editedBytes = await runtime.save();
} finally {
runtime.dispose();
}The first sync() loads search results. The second applies every queued write
as one transaction.
The editing API overview documents runtime ownership, capabilities, saving, and disposal. The Office.js compatibility guide lists the supported objects and methods.
Drive an open editor
The browser runtime connects to a document that a user already has open. Agent edits use the editor undo stack and appear in the document interface.
import { DocxEditor } from "@docx-editor.dev/editor-api/browser";
const runtime = DocxEditor.createBrowser(editor, {
author: "Review agent",
});
try {
await runtime.run(async (context) => {
const matches = context.document.body.search("payment terms");
matches.load("items");
await context.sync();
matches.items[0]?.insertComment("Compare this clause with the playbook.");
await context.sync();
});
} finally {
runtime.dispose();
}Browser comment writes need the Pro review module and an editable document. Your application decides which document text reaches the model.
Treat content controls as typed document fields
A content control is a Structured Document Tag (SDT) in WordprocessingML. It wraps document content and adds field metadata without flattening the text into HTML or a separate form.
Three properties matter to an agent tool:
subtypecontrols behavior. This demo usesplainTextanddate.tagis the machine-readable key, such aseffective-date.titleis the label that Word shows to a user.
The demo also supplies a paragraph ID and exact search text:
{
paragraphId: "7A2F1C90",
search: "[Effective Date]",
subtype: "date",
tag: "effective-date",
title: "Effective date",
}The executor searches only that paragraph and refuses missing or ambiguous text. It then wraps the exact match. This matters because an inline content control cannot cross paragraph boundaries.
After selecting the match, the browser executor applies the native editor command:
const result = editor.exec({
type: "insertContentControl",
subtype: "date",
tag: "effective-date",
title: "Effective date",
});
if (!result.ok) {
throw new Error(`${result.code}: ${result.reason}`);
}A tag identifies a field, but it does not create shared data binding by itself.
The demo gives every Party A field the tag party-a-name. A small editor change
listener copies an edited value to the other controls with that tag. For a
template that uses Word custom XML binding, author the binding and data store
as part of the template workflow instead.
Use content controls when the value has identity beyond its visible text: party names, effective dates, approval status, or a product selection. Use ordinary text for prose that the agent can rewrite freely.
For the supported field types and template behavior, see content controls.
Handle complex DOCX elements as structures
Several elements look like text on screen but have separate WordprocessingML structures. Give each one a dedicated tool.
- Styles: Read the document's style catalog, then apply an available
paragraph style. Do not assume every template contains
SubtitleorQuote. - Lists: Apply native numbering to adjacent paragraphs. Do not put
•or1.in the paragraph text, or Word will display duplicate markers. - Tables: Insert the table first, collect its new cell paragraph IDs, then populate those paragraphs.
- Headers and footers: Treat them as separate document stories. Select and
replace existing content before adding text or a
PAGE_X_OF_Yfield. - Comments: A comment has anchors in the story and a record in the comments part. If a workflow replaces the full body, delete the old comment threads so they do not become orphaned review cards.
- Tracked changes: Store proposals as revisions with an author. Keep accepting and rejecting revisions outside the agent's tool allowlist.
These rules keep the model focused on intent. The executor handles numbering definitions, package parts, range boundaries, and revision metadata.
Choose direct edits or tracked proposals from the request
You do not need a separate mode switch. Route the user's wording to different tools:
- “Update,” “rewrite,” and “fix” can call direct replacement, insertion, or deletion tools.
- “Suggest,” “review,” and “redline” can call tracked proposal tools.
Both paths need a paragraph ID and an exact anchor phrase. A text snapshot sent with the chat request can provide those anchors without another model round trip. Cap the snapshot by paragraph count and character count so large documents do not grow every later request.
Tracked proposals require the Pro review module. The user can inspect, revise, accept, or reject each change in the open editor. For a full setup, see the contract redlining API guide.
Verify writes instead of trusting the model
Treat tool results as authoritative. A useful loop does the following:
- Read or search for an exact target.
- Reject missing or ambiguous matches.
- Apply one supported document transaction.
- Return a concise success or typed refusal.
- Continue only when the result permits it.
For a multi-stage draft, serialize the writes even when the model emits tool calls in parallel. Stop the workflow if a required stage fails. Never tell the user that a table, field, or revision exists until its tool confirms the write.
Build the interface around the document
The editor and agent panel are separate React surfaces. Keep the document visible while tools run, show each tool state, and keep errors close to the failed step.
On wide screens, place the agent beside the editor. In a narrow blog or documentation column, collapse the agent into a launcher and open it below the document. This preserves enough width for the page canvas and toolbar.
Use React composition to mount your controls around the editor. The editing API overview covers runtime ownership, capabilities, saving, and disposal. For review features, read tracked changes and comments.
For the product overview and industry examples, see DOCX agents for Word document editing.