Build a DOCX agent

Give a model document tools that read, draft, comment, and redline a DOCX file. Validate each call, run it through the editing API, and return a typed result.

This page connects a tool-calling model to a DOCX file. You expose a small catalog of document tools. Each tool validates its input and calls @docx-editor.dev/editor-api. The model does not generate Office Open XML (OOXML).

Try the writer agent

Open the writer agent demo on the homepage. Choose Draft a mutual NDA. After the draft finishes, choose Redline for clarity.

The demo uses the Vercel AI SDK. The document tools work with any tool-calling framework.

Before you start

  • You need @docx-editor.dev/editor-api and @docx-editor.dev/core.
  • If you show the document in a page, install @docx-editor.dev/react or @docx-editor.dev/vue.
  • If the agent writes comments or tracked changes, install @docx-editor.dev/pro and pass a non-empty author.

Install the packages

Install the editing API and the engine:

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

If you mount an editor in the page, also install an adapter:

npm install @docx-editor.dev/react

If you record comments or tracked changes, also install Pro:

npm install @docx-editor.dev/pro

Create a runtime

If you have DOCX bytes and no editor in the page, create a server runtime. The server runtime returns edited bytes. It does not start Microsoft Word and does not use a browser Document Object Model (DOM).

import { DocxEditor } from '@docx-editor.dev/editor-api';

const runtime = await DocxEditor.createServer(bytes, {
  author: 'Contract agent',
});

If a reader already has the document open, create a browser runtime. Edits use the editor undo stack and appear in the page.

import { DocxEditor } from '@docx-editor.dev/editor-api/browser';

const runtime = DocxEditor.createBrowser(editor, {
  author: 'Review agent',
});

Call runtime.dispose() when the work ends. For capabilities, sync() rules, and disposal, see Editing API.

Define one tool per document task

Give the model a small catalog. Each tool does one job, validates its input, and runs inside one runtime.run callback.

This AI SDK example defines a read tool and two write tools:

import type { DocxEditorRuntime } from '@docx-editor.dev/editor-api';
import { tool } from 'ai';
import { z } from 'zod';

export function createDocumentTools(runtime: DocxEditorRuntime) {
  return {
    read_document: tool({
      description: 'Read the current document text.',
      inputSchema: z.object({}),
      execute: async () =>
        runtime.run(async (context) => {
          const body = context.document.body;
          body.load('text');
          await context.sync();
          return { text: body.text };
        }),
    }),

    append_paragraph: tool({
      description: 'Add one paragraph to the end of the document.',
      inputSchema: z.object({
        text: z.string().min(1),
      }),
      execute: async ({ text }) =>
        runtime.run(async (context) => {
          context.document.body.insertParagraph(text, 'End');
          await context.sync();
          return { inserted: true };
        }),
    }),

    replace_exact_text: tool({
      description: 'Replace one exact phrase when it occurs once.',
      inputSchema: z.object({
        search: z.string().min(1),
        replacement: z.string(),
      }),
      execute: async ({ search, replacement }) =>
        runtime.run(async (context) => {
          const matches = context.document.body.search(search, {
            matchCase: true,
          });
          matches.load('items');
          await context.sync();

          if (matches.items.length !== 1) {
            return {
              replaced: false,
              reason: `Expected one match, found ${matches.items.length}.`,
            };
          }

          matches.items[0].insertText(replacement, 'Replace');
          await context.sync();
          return { replaced: true };
        }),
    }),
  };
}

Return a structured result. If a search matches zero times or more than once, return a refusal. Do not pick a match for the model.

This package does not include a model, a tool catalog, or a chat UI. Keep those in your application.

Address text with an exact phrase

Pass a verbatim search phrase from the paragraph you read. Copy case and punctuation.

If you send a document snapshot with the chat request, cap it by paragraph count and character count. A large document must not grow every later turn.

Split a fresh draft into stages

Do not put the whole document in one write_document tool. Split a draft so each failure is easy to see:

  1. Replace the body with styled paragraphs. Return paragraph IDs.
  2. Apply native bullets or numbering.
  3. Wrap exact placeholders in content controls.
  4. Insert a table, then populate its cell paragraphs.
  5. Write the header, footer, and page field.

Do not put or 1. in paragraph text. Native list formatting adds those markers. Literal prefixes show up twice in Word.

Each later tool uses IDs from the first tool. If a required stage fails, stop.

The writer agent example runs this sequence. The comment agent example reads an open document and adds anchored comments.

Choose direct edits or tracked changes

Read the user request, then pick tools:

  • If the user says "update", "rewrite", or "fix", call direct replacement, insertion, or deletion.
  • If the user says "suggest", "review", or "redline", call tracked proposal tools.

Leave accept and reject off the agent allowlist. The reader decides in the editor.

Cap tracked proposals per turn. Skip headings, placeholders, and sentences that are already clear. Prefer the smallest edit that fixes the problem.

For revision markup, see Tracked changes. For comment threads, see Comments.

Wrap values in content controls

A content control is a Structured Document Tag (SDT). Use one when a value has identity beyond its visible text, such as a party name or an effective date.

Pass a paragraph ID, the exact placeholder text, a tag, a title, and a subtype. Search only that paragraph. If the phrase is missing or appears twice, return a refusal.

Wrap the placeholder, not the label. Wrap [Effective Date]. Do not wrap Date of Birth.

For field types and template filling, see Content controls.

Trust the tool result

For every write, do the following:

  1. Read or search for an exact target.
  2. If the match is missing or ambiguous, return a refusal.
  3. Apply one document transaction.
  4. Return a short success object or a typed refusal.
  5. Continue only when the result says the write succeeded.

If the model emits parallel tool calls, queue the writes on the client. Do not tell the reader that a table, field, or revision exists until its tool succeeds.

Next steps

On this page