# Vespper > Vespper is a DOCX MCP Server for AI agents that need to read, search, and edit existing Word documents. Agents work with HTML; Vespper applies edits to the DOCX. New edits are tracked by default, with a configurable tracking preference. The hosted service uses document sessions. Upload a DOCX once, attach the session ID to tool calls, retrieve the updated document, and close the session. The tools include read_document, search_document, and edit_document. A Vespper API key and the agent's model-provider configuration are required. A local MCP server, vespper-mcp, lets AI assistants such as Claude Code, Claude Desktop, Cursor, Codex and ChatGPT read and edit .docx files in directories you allow. Live editing streams an agent's edits into a document open in Word, ONLYOFFICE, or another editor, either applied directly or proposed as suggestions the user accepts. These reference guides are published separately from the product blog. Each Markdown guide has a canonical HTML page containing the same guidance. Tool schemas and implementation details are maintained in the official API documentation. ## Start here: build a DOCX editing agent Follow this path from choosing tools to delivering changes a person can review, or jump to the topic that matches your current task. 1. [Choose your tools](https://www.vespper.com/llms/docx-libraries-for-agents.md): Match libraries and MCP servers to the document workflow you need to build. 2. [Build an editing agent](https://www.vespper.com/llms/build-docx-editing-agent.md): Connect document tools, manage a session, and deliver the updated DOCX. 3. [Preserve formatting](https://www.vespper.com/llms/preserve-docx-formatting.md): Scope the edit and check surrounding text, tables, images, and document structure. 4. [Return changes for review](https://www.vespper.com/llms/tracked-changes-api.md): Run a contract edit and inspect the native revisions and preservation checks. ## Choosing DOCX tools Start with the document your application needs to deliver. Compare libraries, converters, and MCP servers by whether you need to generate a file, extract content, or edit an existing document. Start with: [DOCX libraries for AI agents: choose by editing workflow](https://www.vespper.com/llms/docx-libraries-for-agents.md). - [DOCX libraries for AI agents: choose by editing workflow](https://www.vespper.com/llms/docx-libraries-for-agents.md): Which DOCX library or editing API should I use for an AI agent? Compare Python DOCX editing, JavaScript generation, templates, HTML conversion, and Vespper MCP based on the document task your agent must perform. ## Agent and editor integration Connect document tools to your application and return the edited file to the user. Start with the session lifecycle, then use the Office.js example if your agent needs to work inside Word. Start with: [Build an AI agent that edits existing DOCX documents](https://www.vespper.com/llms/build-docx-editing-agent.md). - [Build an AI agent that edits existing DOCX documents](https://www.vespper.com/llms/build-docx-editing-agent.md): How do I build an AI agent that edits an existing DOCX file? Connect a document-editing agent to Vespper DOCX MCP: open a session, read or search, apply targeted edits, save the document, and close the session. - [Build an AI Word add-in with Office.js and Vespper](https://www.vespper.com/llms/build-ai-word-add-in.md): How do I build an AI Word add-in with Vespper? Build a Word add-in with Office.js, React, and Vespper DOCX MCP. Run our open-source example, stream AI edits into Word, and review tracked changes. ## Editing existing documents Locate the right passage, make a targeted change, and check what survives the edit. These guides cover formatting, ambiguous matches, and finding content in long documents. Start with: [Preserving DOCX formatting when an AI agent edits a file](https://www.vespper.com/llms/preserve-docx-formatting.md). - [Find and edit a passage in a large DOCX document](https://www.vespper.com/llms/search-large-documents.md): How do I find and edit a passage in a long DOCX without reading the whole file? Use document search, page ranges, unique HTML anchors, and revision checks to target edits in long Word files without loading the whole file into an agent. - [Preserving DOCX formatting when an AI agent edits a file](https://www.vespper.com/llms/preserve-docx-formatting.md): How can an AI edit a Word document without losing its formatting? Understand run formatting, targeted HTML edits, document structure, and practical checks for preserving an uploaded DOCX during agent-driven editing. ## Review and validation Return edits as native Word tracked changes so a person can review them. Use a sample contract, recorded API outputs, and verification code to inspect the result and understand the checks' limits. Start with: [DOCX tracked changes API: redline a contract with an agent](https://www.vespper.com/llms/tracked-changes-api.md). - [DOCX tracked changes API: redline a contract with an agent](https://www.vespper.com/llms/tracked-changes-api.md): How do I return AI edits as native Word tracked changes? Return agent edits as native Word tracked changes. Run a Python contract-editing example with a sample DOCX, revision authors, and preservation checks. ## Python DOCX troubleshooting Use Python for document changes your application can specify directly. Work through text split across runs, table cells, missing images, and review comments before connecting those operations to an agent. Start with: [python-docx: create and edit Word documents in Python](https://www.vespper.com/llms/python-docx.md). - [Add review comments to DOCX with python-docx](https://www.vespper.com/llms/python-docx-comments.md): How do I add a comment to a sentence in a Word document using Python? Use python-docx 1.2.0 to anchor a review comment to runs. Understand sentence boundaries, comment authors, and the difference between comments and tracked edits. - [Find and replace text in DOCX tables with Python](https://www.vespper.com/llms/python-docx-replace-in-tables.md): Why does my python-docx find-and-replace skip text inside tables? Find text in table-cell paragraphs, including nested tables, without treating merged cells as separate values or overwriting the whole cell. - [python-docx: create and edit Word documents in Python](https://www.vespper.com/llms/python-docx.md): How do I create and edit Word documents with Python? Install python-docx, read and edit existing Word files, and replace text while keeping run formatting. Runnable examples, comments, and tracked-change limits. - [Replace text split across runs in python-docx](https://www.vespper.com/llms/python-docx-replace-across-runs.md): How do I replace text in a DOCX when the phrase is split across runs? Replace a phrase that spans multiple Word runs without flattening the paragraph. Decide how replacement text inherits formatting and handle ambiguous matches. - [Why images disappear after python-docx text replacement](https://www.vespper.com/llms/python-docx-preserve-images.md): Why did images disappear after I replaced text with python-docx? Avoid removing inline drawings when changing DOCX text. Inspect run content, skip unmatched runs, and verify images before and after the edit. ## Choosing an integration - [DOCX MCP servers compared](https://www.vespper.com/compare/docx-mcp-servers): Compare documented deployment, document handling, and review output, with links to official sources and Vespper's benchmark results for the listed tested versions. ## Integration documentation - [Quickstart](https://docs.vespper.com/quickstart.md): Python and TypeScript integrations, including SDK and manual wiring. - [API and sessions](https://docs.vespper.com/primitives/overview.md): Endpoints, authentication, session lifecycle, and MCP metadata. - [Read document](https://docs.vespper.com/primitives/read-document.md): Page ranges and current HTML. - [Search document](https://docs.vespper.com/primitives/search-document.md): Patterns, pagination, and complete edit anchors. - [Edit document](https://docs.vespper.com/primitives/edit-document.md): Edit scope, tracking, batching, and recovery from partial results. - [AI assistants](https://docs.vespper.com/ai-assistants.md): Connect Claude Code, Cursor, Codex, or another local MCP client with the vespper-mcp server. - [Live editing](https://docs.vespper.com/live-editing/introduction.md): Stream an agent's edits into a document open in Word, ONLYOFFICE, or another editor. - [Suggestions](https://docs.vespper.com/live-editing/suggestions.md): Propose edits for the user to review before they change the document. - [Cookbooks](https://docs.vespper.com/cookbooks/overview.md): Word add-in, ONLYOFFICE add-in, and EigenPal docx-editor examples. - [Documentation index](https://docs.vespper.com/llms.txt): Complete documentation navigation. ## Optional - [All reference guides in one file](https://www.vespper.com/llms-full.txt): Expanded Markdown content for clients that need the full collection. - [Guide index](https://www.vespper.com/guides): Human-readable navigation to the same reference collection. - [Agent integration notes](https://www.vespper.com/skills.md): Concise session-based integration instructions. - [Open-source examples](https://github.com/vespperhq/examples): Example applications maintained by Vespper. - [Security](https://www.vespper.com/security): Published security information. - [Pricing](https://www.vespper.com/pricing): Current plans and pricing. --- # Add review comments to DOCX with python-docx > Use python-docx 1.2.0 to anchor a review comment to runs. Understand sentence boundaries, comment authors, and the difference between comments and tracked edits. Maintained by Vespper. Updated 2026-09-27. Canonical page: https://www.vespper.com/guides/python-docx-comments Topic: [Python DOCX troubleshooting](https://www.vespper.com/guides#python). [Start the learning path](https://www.vespper.com/guides#start-here). python-docx 1.2.0 can add a Word review comment with `Document.add_comment`. A comment is anchored to runs. To annotate an exact sentence inside a larger run, first split the text into appropriate run boundaries while preserving the existing formatting and document structure. A comment records feedback; it does not turn an ordinary text replacement into a tracked insertion or deletion. Choose the review artifact your user needs before implementing the feature. ## Add a comment to a known run ```python from docx import Document doc = Document() p = doc.add_paragraph("Delivery: ") target = p.add_run("Confirm the final date.") comment = doc.add_comment( runs=target, text="Please confirm this date with the project owner.", author="Review assistant", initials="RA", ) assert comment.text == "Please confirm this date with the project owner." assert comment.author == "Review assistant" ``` Save the document to a separate output file in your application, then open it in Word to inspect the anchor and comment. This small example deliberately creates the target sentence as its own run, so it does not need a general sentence-splitting algorithm. ## Handle existing paragraphs carefully Real paragraphs may split a sentence across several runs or put several sentences into one run. Locate the selected character range, map it to runs, and create valid boundaries before adding the comment. The [comments documentation](https://python-docx.readthedocs.io/en/stable/user/comments.html) explains anchoring constraints and unsupported locations. Do not recreate a mixed paragraph from `paragraph.text` just to obtain a convenient anchor. That can remove hyperlinks, drawings, and character formatting. Test repeated sentences so that a comment is attached to the occurrence the user selected rather than the first textual match. ## Keep the author and purpose explicit Use the application's real review identity in the comment metadata. Comment text should explain the issue and the requested decision, for example “Confirm whether this date includes the review period.” A vague “AI suggestion” comment gives the reviewer little useful context. The historical [sentence-comment request](https://github.com/python-openxml/python-docx/issues/949) is evidence of this workflow need, not evidence that current python-docx lacks comments. Check examples against the installed API; documentation inconsistencies have also been [reported and closed](https://github.com/python-openxml/python-docx/issues/1551). Preserve existing comments when moving a document between stages. ## Next steps - [Return edits as native revisions](https://www.vespper.com/guides/tracked-changes-api): Use tracked changes when the user needs to accept or reject an actual text replacement. - [Build an agent editing workflow](https://www.vespper.com/guides/build-docx-editing-agent): Connect read, search, and edit tools to your application and deliver the resulting DOCX. ## Related guides - [python-docx: create and edit Word documents in Python](https://www.vespper.com/llms/python-docx.md) --- # Build an AI agent that edits existing DOCX documents > Connect a document-editing agent to Vespper DOCX MCP: open a session, read or search, apply targeted edits, save the document, and close the session. Maintained by Vespper. Updated 2026-10-17. Canonical page: https://www.vespper.com/guides/build-docx-editing-agent Topic: [Agent and editor integration](https://www.vespper.com/guides#integration). [Start the learning path](https://www.vespper.com/guides#start-here). An agent that edits an existing DOCX needs a way to locate the requested passage, change the current document, and return the edited file. Vespper's **DOCX MCP Server** provides tools for that workflow, including `read_document`, `search_document`, and `edit_document`. The agent reads an HTML representation of the document; Vespper applies the edits to the DOCX. The HTML is the agent's editing interface. Vespper's reconciler maps the requested change back to OOXML for the affected block, then the service computes revision markup and patches the original file. The [architecture explanation](https://www.vespper.com/blog/launching-vespper-docx-mcp) describes the trained reconciler and deterministic revision calculation. This is not a promise that every untouched XML part remains byte-identical; the [recorded contract results](https://www.vespper.com/guides/tracked-changes-api/RESULTS.md) show the exact preservation checks and font-property changes. Use this approach when the input is a document supplied by a user and the request describes a change in natural language. For a fixed report generated from known data, a document-generation or templating library may be sufficient. The [library selection guide](https://www.vespper.com/guides/docx-libraries-for-agents) explains that distinction. ## What your team stops maintaining Because the agent edits HTML and Vespper writes the OOXML, your application does not contain a DOCX editing layer. It does not split or merge runs, resolve numbering definitions, generate `w:ins` and `w:del` markup with unique revision IDs, or patch relationships and package parts. Hand-written code has to get each of those right for every document variation it meets, and the [python-docx run-replacement guide](https://www.vespper.com/guides/python-docx-replace-across-runs) shows how much code that takes for one narrow case. What remains in your codebase is small: the session lifecycle, the tool descriptions you give the model, and your own validation fixtures. The same read, search, and edit tools serve every surface in the next section, so supporting another surface does not mean writing another editing implementation. Validation stays your responsibility, as the [formatting checks](https://www.vespper.com/guides/preserve-docx-formatting) describe. ## Choose where the agent runs Vespper connects to several surfaces. Pick the one that matches where your users work: | Surface | How it connects | Start with | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | Headless backend or pipeline | The hosted MCP endpoint or an SDK, with a document session. | This guide and the [quickstart](https://docs.vespper.com/quickstart). | | Microsoft Word | An Office.js add-in that calls the hosted service and applies returned revisions in the open document. | The [Word add-in guide](https://www.vespper.com/guides/build-ai-word-add-in). | | An editor inside your product | Live editing: your API runs the agent and streams edits into the open document in ONLYOFFICE, EigenPal's docx-editor, or any client that can load a DOCX. Edits apply directly, tracked or plain, or arrive as suggestions the user accepts. Vespper is editor-agnostic. | The [live editing docs](https://docs.vespper.com/live-editing/introduction) and [cookbooks](https://docs.vespper.com/cookbooks/overview). | | AI assistants such as Claude, ChatGPT and Codex | The local `vespper-mcp` server, which reads and edits `.docx` files in directories you allow. | The [AI assistants setup](https://docs.vespper.com/ai-assistants). | The hosted MCP endpoint works on a document uploaded into a session. The local server exists so an assistant can work on files on the user's machine. It runs with `npx`, needs Node.js 22 or newer and a Vespper API key, limits file access to the directories you allow, saves edits back to the original `.docx`, and records them as tracked changes by default. It can also create a blank `.docx` in an allowed directory. Per the docs it communicates with Vespper's cloud service, so it is not an offline tool. The rest of this guide covers the hosted session workflow. For the local server, follow the [AI assistants setup](https://docs.vespper.com/ai-assistants) for your client. ## What your application supplies Your application needs the DOCX file, a Vespper API key, an MCP client, and the model configuration for its agent framework. The hosted MCP endpoint is `https://mcp.vespper.com/mcp`, using Streamable HTTP. Authentication belongs in the connection's `Authorization: Bearer ...` header, not in model-generated tool arguments. Vespper has Python and TypeScript SDKs. The [quickstart](https://docs.vespper.com/quickstart) includes framework integrations and examples for manual MCP wiring. Use its current install commands and SDK signatures when building an application. ## Start with a complete contract-editing example The [contract redlining example](https://www.vespper.com/guides/tracked-changes-api#run-a-contract-redlining-example) provides an input DOCX, actual API outputs, a complete Python integration, pinned direct dependencies, and a standalone output verifier. Its task is to change a payment term from 30 to 45 days while preserving bold text and another reviewer's pending changes. The recorded tracking-on and tracking-off runs passed the fixture checks with the documented font-property normalization. After following its setup instructions, run the agent path with a model configured in your environment: ```bash python redline_contract.py --agent --output service-agreement-agent.docx ``` The integration opens one session, connects the MCP server, runs the agent, retrieves the current bytes before closing the session, and checks the saved file. The essential SDK wiring is: ```python async with mcp: await client.patch_mcp_tools( mcp=mcp, session_id=session_id, author="Contract assistant", track_changes=True, ) await Runner.run(agent, instruction, max_turns=8) document = client.get_session_document(session_id) ``` This is an excerpt, not a standalone program. The [full script](https://www.vespper.com/guides/tracked-changes-api/redline_contract.py) supplies imports, credentials, the MCP connection, agent instructions, file writing, verification, and session cleanup in `finally`. Its default mode makes a fixed sequence of MCP calls without a separate agent-model call. Vespper's own reconciler still runs inside the service. The [published results](https://www.vespper.com/guides/tracked-changes-api#inspect-a-real-api-result) verify this default path; the optional model-driven path has not been live-tested for those results. Keep the DOCX bytes in application code. SDK patching retains them for download while exposing the editing result to the agent. Do not treat the model's final message as the output artifact. ## Follow the document session lifecycle 1. Open a document session by uploading the DOCX once. The HTTP endpoint is `POST https://mcp.vespper.com/v1/docx/sessions`; the SDK exposes session-opening methods. 2. Connect the MCP client and discover the tool schemas through `tools/list`. Preserve the returned tool descriptions when giving them to the model. 3. Attach the session ID to subsequent tool calls. SDK auto-patching does this for supported clients; manual integrations supply `com.vespper/session-id` in MCP `_meta`. 4. Search for known text or read the necessary pages, then apply an edit against the returned HTML. 5. Retrieve and save the latest DOCX. Keep the input and output separately when the user needs to review the result. 6. Close the session in cleanup code, including when the agent fails or is cancelled. The server keeps the current document for the session. The client does not need to upload the document again on every tool call. The [session API reference](https://docs.vespper.com/primitives/overview) documents ownership, revision numbers, and cleanup. ## Check whether the workflow fits your application | Requirement | Vespper integration scope | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Edit a user-uploaded DOCX in a backend | Open a hosted session, supply the session ID on calls, and deliver the returned bytes. | | Let a human review the proposed edit | Enable tracking and set an author; deliver the DOCX to the review UI your product uses. Vespper is editor-agnostic. | | Apply an approved change directly | Disable tracking for that edit. Existing revisions require separate, explicit handling. | | Edit global styles | These are excluded from the current edit target scope; preserving them during a body edit is a different requirement. | | Connect a generic MCP client with only a URL | Tool discovery alone does not handle document upload, session metadata and output delivery. Add the SDK or lifecycle adapter. | The [current edit reference](https://docs.vespper.com/primitives/edit-document) is the authority for supported targets and metadata. Validate your own document structures before exposing a broader editing promise to customers. ## Diagnose an integration that connects but cannot edit Tool discovery only verifies part of the workflow. A [developer report about available MCP tools failing to edit and save Word files](https://www.reddit.com/r/mcp/comments/1o1yxys/cant_reliably_edit_word_docx_using_mcp_servers_on/) illustrates why document state and file delivery need separate checks. | Symptom | Check first | | ------------------------------------------------ | --------------------------------------------------------------- | | Cannot list tools | Endpoint, Streamable HTTP transport, and bearer authentication. | | Tools appear but reads fail | Document upload and the session ID in each call's `_meta`. | | Reads work but edits fail | Current complete anchors, uniqueness, and supported edit scope. | | Edit succeeds but the user receives the old file | Revision capture, binary decoding, and artifact delivery. | Putting a session ID in the agent's prompt does not attach it to a protocol call. A generic MCP client that only accepts a server URL and credentials may need an adapter to upload the document, attach metadata, and capture its output. First prove a single read/edit/save operation using the [quickstart](https://docs.vespper.com/quickstart), then connect that lifecycle to your application. ## Choose the tool from the request | Request | First operation | Reason | | --------------------------------------------- | -------------------------------------------- | ---------------------------------------------------- | | “Change the deadline in the delivery section” | Search for the deadline or section text | Find candidate passages without reading every page. | | “Add a summary after the introduction” | Read the relevant pages | Inspect the document structure and insertion point. | | “Revise these three separate paragraphs” | Read or search, then batch independent edits | Each edit needs its own current, unambiguous anchor. | For example, this is a **tool-call parameters fragment**, assuming your integration already holds a valid session ID: ```json { "name": "search_document", "arguments": { "pattern": "(?i)delivery" }, "_meta": { "com.vespper/session-id": "YOUR_SESSION_ID" } } ``` Search results contain complete HTML blocks. Copy a current block into the edit's `old` value and write its intended replacement in `new`. Resolve ambiguity before editing; a repeated phrase is not enough to identify which occurrence the user meant. ## Treat the tool result as the source of completion An agent saying “done” does not prove a document changed. Check the edit result, retain its latest document and revision, and report any failed edits. A batch can succeed only in part; successful edits are already applied, so retry only corrected failures. See the [edit reference](https://docs.vespper.com/primitives/edit-document). New edits are tracked by default. Configure the author and tracking preference in the integration, and test the output in the viewer your users rely on. Start with a small known edit before testing long documents, tables, repeated passages, and existing revisions. ## Return the latest DOCX bytes Keep the document binary in application code rather than passing the entire file through the model's conversation. An edit response's `base64` field contains the latest cumulative DOCX; `current_revision` identifies the revision and is not the file itself. With SDK auto-patching, retrieve the retained bytes using `getSessionDocument(sessionId)` or `get_session_document(session_id)`. With manual wiring, retain the latest `base64` value and decode it once. The [edit response reference](https://docs.vespper.com/primitives/edit-document) documents these fields. Capture the output before closing the session. A partially successful batch can already have changed the document: return that latest state together with any unresolved edits, rather than the uploaded file or the first intermediate result. Keep the original input separately. For an HTTP download, use a `.docx` filename and `Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document`. Pass bytes through storage and attachment handling; decoding a DOCX as UTF-8 and writing the resulting text back can corrupt it. A [reported binary-routing failure in an agent framework](https://github.com/langchain-ai/deepagents/issues/3657) documents this class of issue without establishing a failure rate for other implementations. ## Validate the delivered document Test the file users actually download, including the storage and attachment path. Use a fixture with a known target sentence, an unrelated clause, an image, and a pending revision. Keep the input, instruction, output, and observed results together so failures are reproducible. | Check | What to verify | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Package integrity | The output is a readable ZIP with DOCX parts and opens in a document reader. If you change package internals, also validate XML and relationships. | | Requested scope | The target changed once; the unrelated clause, image, and links remain in their intended locations. File hashes or media counts alone cannot establish this. | | Review state | The configured author and tracking mode are respected, and unrelated pending revisions remain pending. | | Partial failure | The returned file includes successful edits and the application reports the failed ones accurately. | | Layout | Tables, lists, images, and page boundaries remain usable in the viewer your users rely on. | Package parsing cannot prove layout fidelity or correct review markup. Include representative customer structures in the [formatting checks](https://www.vespper.com/guides/preserve-docx-formatting), and exercise both tracking-on and tracking-off cases using the [review workflow](https://www.vespper.com/guides/tracked-changes-api). ## Next steps - [Check formatting and edit scope](https://www.vespper.com/guides/preserve-docx-formatting): Continue the learning path by defining what must survive a targeted edit. - [Run the contract-editing example](https://www.vespper.com/guides/tracked-changes-api#run-a-contract-redlining-example): Download the Python code and sample DOCX, then inspect the output and verification report. - [Connect with the API quickstart](https://docs.vespper.com/quickstart): Use the maintained setup instructions for your SDK or manual integration. ## Related guides - [Find and edit a passage in a large DOCX document](https://www.vespper.com/llms/search-large-documents.md) - [DOCX tracked changes API: redline a contract with an agent](https://www.vespper.com/llms/tracked-changes-api.md) --- # Build an AI Word add-in with Office.js and Vespper > Build a Word add-in with Office.js, React, and Vespper DOCX MCP. Run our open-source example, stream AI edits into Word, and review tracked changes. Maintained by Vespper. Updated 2026-10-17. Canonical page: https://www.vespper.com/guides/build-ai-word-add-in Topic: [Agent and editor integration](https://www.vespper.com/guides#integration). [Start the learning path](https://www.vespper.com/guides#start-here). Our [open-source Word add-in](https://github.com/vespperhq/examples/tree/main/word-add-in) gives you a starting point for building an AI assistant inside Microsoft Word. It uses Office.js and React for the task pane, Mastra to run the agent, and Vespper DOCX MCP to read, search, and edit the open document. In this tutorial, you'll run the example locally, make your first tracked edit, and see which parts to change when building your own add-in. The example code is [MIT-licensed](https://github.com/vespperhq/examples/blob/main/word-add-in/LICENSE). Running it requires a Vespper API key and a model-provider API key. ## What you'll build The add-in opens a chat pane alongside your Word document. A user asks for a change, the agent edits the document, and revisions stream back into Word while the agent is working. The example includes: - **Streamed document edits**, so users can see changes as they arrive. - **Tracked changes** that follow Word's current Track Changes setting. - **Selected-text and pasted-image context** to give the agent more information about the request. - **Model selection and an editable tracked-change author** in the task pane. [Watch: The Vespper Word add-in streaming edits into an open document](https://www.youtube.com/watch?v=xuJ7OsZ5I04) ## How Office.js and Vespper fit together Office.js connects the add-in to the open Word document. The local server runs the agent, and Vespper handles document editing through tools including `read_document`, `search_document`, and `edit_document`. | Part | Responsibility in this example | | ----------------------------------- | --------------------------------------------------------------------------------------------------- | | Office.js and React | Show the task pane, read the open DOCX and its tracking mode, and apply returned revisions in Word. | | Local Node.js server and Mastra | Receive the document and user request, run the agent, and stream updates to the task pane. | | Vespper TypeScript SDK and DOCX MCP | Manage the document session and give the agent tools to read, search, and edit the document. | The server opens a Vespper session for each agent turn. The document is uploaded once for that session, and subsequent tool calls use the session ID. The agent works with HTML through the document tools; Vespper returns updated DOCX revisions. The API keys stay in the local server's environment and are not bundled into the task pane. The workflow still calls Vespper and your selected model provider. ## Run the Word add-in locally You'll need Microsoft Word desktop on macOS or Windows, Node.js **22.13 or newer**, Git, a [Vespper account](https://app.vespper.com), and an API key for OpenAI, Anthropic, or Google. These steps use the example's default OpenAI configuration. ### 1. Clone and install the example ```bash git clone https://github.com/vespperhq/examples.git cd examples/word-add-in npm install ``` This installs the task pane, local server, agent dependencies, and Office add-in development tools. ### 2. Configure your API keys Create a key on the [Vespper API keys page](https://app.vespper.com/keys). Copy the secret when it is shown, then create your local environment file. On macOS: ```bash cp .env.example .env ``` On Windows PowerShell: ```powershell Copy-Item .env.example .env ``` Open `.env` and replace the placeholders: ```dotenv VESPPER_API_KEY=sk_live_your_key_here VESPPER_MCP_URL=https://mcp.vespper.com/mcp OPENAI_API_KEY=sk-your_openai_key_here ``` The repository ignores `.env` in Git. Keep your keys in that file rather than putting them in the React code. To use another model provider, configure its key and set `WORD_AGENT_MODEL` to the corresponding model. For example, the sample supports: ```dotenv ANTHROPIC_API_KEY=sk-ant-your_anthropic_key_here WORD_AGENT_MODEL=anthropic/claude-sonnet-4.5 ``` Only the selected model provider's key is needed. The [README](https://github.com/vespperhq/examples/blob/main/word-add-in/README.md) lists the supported environment variables, including those for Google models. ### 3. Set up local HTTPS The add-in loads its task pane over HTTPS. Install the development certificate using the repository's setup command: ```bash npm run setup:certs ``` Follow the certificate installation prompts. You can check the certificate setup with `npm run setup:certs:check`. ### 4. Launch the add-in in Word ```bash npm start ``` This builds the task pane, starts the local HTTPS server at `https://localhost:3100`, launches Word, and sideloads the add-in. If the pane doesn't open automatically, choose **Home → Add-ins → Vespper**. Open [the local health endpoint](https://localhost:3100/health). Both `vespperConfigured` and `agentConfigured` should be `true`. These flags indicate that keys are present for Vespper and the configured default model; the first successful edit verifies that the integration works with those credentials. ## Make your first tracked edit Start with a simple document so you can see exactly what changed: 1. Create and save a Word document containing: **The draft is due on Friday.** 2. Turn on **Review → Track Changes**. 3. Open the Vespper task pane and ask: **Change the deadline from Friday to Monday.** 4. Review the edit in Word, then accept or reject it. The sample reads Word's tracking mode when you submit the request. With tracking enabled, the edit should appear as a deletion and an insertion. With tracking disabled, changes are applied directly. You can set the tracked-change author in the add-in's settings. For a more specific request, select text in Word before submitting your prompt. You can also paste an image into the prompt to give the agent visual context. ## How streamed edits reach Word The integration lives in [`api/agent.ts`](https://github.com/vespperhq/examples/blob/main/word-add-in/api/agent.ts). It opens a document session and patches the MCP tools with the session ID, author, and tracking preference. This excerpt shows how the SDK also forwards document updates to the task pane: ```typescript await client.patchMCPTools({ mcp, sessionId, author: opts.author, trackChanges: opts.trackChanges, onDocumentUpdated(update) { output.write({ type: "edit_applied", docx_b64: Buffer.from(update.document).toString("base64"), edit_count: update.editCount, revision: update.revision, }); }, }); ``` The example wraps `edit_document` to parse edit arguments as the model produces them. Each completed edit pair can be sent to Vespper before the model finishes generating the full tool input. The wrapper supplies the streaming batch ID and edit index, while the SDK supplies the ordinary session metadata and retains updated document revisions. The task pane receives those revisions and applies them through Office.js. At the end of the turn, the server disconnects the MCP client and closes the Vespper session. The [Word add-in cookbook](https://docs.vespper.com/cookbooks/live-editing/word-add-in) documents the complete integration. ## Handle concurrent typing, revision order, and cancellation Changing a file on disk does not synchronize Word's active document. A [developer question about live Word editing](https://www.reddit.com/r/mcp/comments/1o449qd/ms_word_mcp_live_editing/) asks for this distinction explicitly. Office.js supplies the editor integration here; Vespper's tools are editor-agnostic, and another editor needs its own adapter for returned DOCX updates. Track three separate states in that adapter: the model proposing an edit, the service applying it, and the editor displaying the returned revision. Parse complete edit pairs before submitting them; a partial JSON fragment is not a document mutation. Each agent turn starts from a document snapshot. If the user types afterward, applying an output derived from the older snapshot can overwrite newer work. Track the input version, serialize conflicting operations, and choose a conflict policy before supporting simultaneous user and agent edits. Prevent a late update from replacing a newer displayed revision. These are requirements for your application, not a claim that the sample implements collaborative merging. On cancellation, stop scheduling edits and retain the latest successful state before closing the session. Tell the user whether changes were already applied. Test cancellation after one successful edit, a failed update, typing during an operation, and both Word tracking modes. Compare the final saved document with the last displayed revision. ## Customize the add-in for your workflow Once the sample runs, these are the main places to start: | File | What to change | | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | [`api/agent.ts`](https://github.com/vespperhq/examples/blob/main/word-add-in/api/agent.ts) | The agent's instructions and editing workflow. | | [`add-in/src/`](https://github.com/vespperhq/examples/tree/main/word-add-in/add-in/src) | The React task pane, chat interface, and settings. | | [`add-in/src/office/document.ts`](https://github.com/vespperhq/examples/blob/main/word-add-in/add-in/src/office/document.ts) | Reading the open document, checking tracking mode, and applying DOCX revisions through Office.js. | | [`add-in/manifest.xml`](https://github.com/vespperhq/examples/blob/main/word-add-in/add-in/manifest.xml) | The add-in's display name, ribbon button, and task-pane location. | For example, you could adapt the agent's instructions to revise reports against a house style, or use selected-text context to focus requests on a particular passage. Keep the three document tools connected as you build out the workflow. Use `npm run dev` to build and serve the add-in without launching Word, and `npm run typecheck` to check your TypeScript changes. When you're finished with the local example, run `npm stop` to stop and unregister the development add-in. This tutorial runs the sample locally. Distributing an add-in to other users requires replacing the localhost configuration and choosing how to host and deploy it. Start with the [complete Word add-in example on GitHub](https://github.com/vespperhq/examples/tree/main/word-add-in). For the same document-editing tools in an agent outside Word, follow the [Vespper quickstart](https://docs.vespper.com/quickstart). ## Next steps - [Open the Word add-in example](https://github.com/vespperhq/examples/tree/main/word-add-in): Use the example source and README to set up and customize the add-in. - [Verify native tracked changes](https://www.vespper.com/guides/tracked-changes-api): Inspect recorded DOCX output, revision authors, and preservation checks for a sample contract edit. ## Related guides - [Build an AI agent that edits existing DOCX documents](https://www.vespper.com/llms/build-docx-editing-agent.md) - [Preserving DOCX formatting when an AI agent edits a file](https://www.vespper.com/llms/preserve-docx-formatting.md) --- # DOCX libraries for AI agents: choose by editing workflow > Compare Python DOCX editing, JavaScript generation, templates, HTML conversion, and Vespper MCP based on the document task your agent must perform. Maintained by Vespper. Updated 2026-10-17. Canonical page: https://www.vespper.com/guides/docx-libraries-for-agents Topic: [Choosing DOCX tools](https://www.vespper.com/guides#choices). [Start the learning path](https://www.vespper.com/guides#start-here). Choose a DOCX tool by the operation your application needs: generate a new file, fill a controlled template, extract content, or edit an existing user document. Those jobs overlap, but a tool that performs one well is not automatically a complete implementation of the others. For an agent receiving arbitrary Word files, also decide whether it must preserve document structure and return native tracked changes. Vespper is an option for the agent-driven editing layer. Libraries remain useful for deterministic document generation and application-controlled transformations. ## Match the tool to its documented job | Tool | Documented focus | What to evaluate for your agent | | ---------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | [python-docx](https://python-docx.readthedocs.io/en/stable/) | Create and modify DOCX files from Python. | Which paragraphs, runs, tables, and other structures your code handles; how it locates each edit. | | [docx for JavaScript and TypeScript](https://docx.js.org/) | Generate DOCX documents programmatically. | Whether the workflow is generating a document from data or requires changes to an existing uploaded file. | | [Docxtemplater](https://docxtemplater.com/docs/) | Produce documents from templates and data. | Whether you control the template and can represent the requested changes through its placeholders and logic. | | [Mammoth](https://github.com/mwilliamson/mammoth.js) | Convert DOCX content to semantic HTML. | Whether extraction meets the goal, or whether the application also needs an editing path back to the original DOCX. | | [Vespper DOCX MCP](https://docs.vespper.com/primitives/overview) | Expose read, search, and edit tools over an existing DOCX session. | The supported edit targets, agent framework, review requirements, and hosted-service integration. | This table compares documented purposes. It is not a performance benchmark, and it does not imply that a library cannot be extended beyond its primary use case. ## When Python logic is sufficient Use `python-docx` when your application knows exactly what to modify and can encode that operation. Filling a controlled report or changing a field in a known template is different from interpreting an open-ended request against an unfamiliar document. An agent can call your Python functions. You still implement target selection, ambiguous matches, formatting boundaries, validation, and any additional revision handling. The [Python DOCX guide](https://www.vespper.com/guides/python-docx) includes tested replacements and the distinction between comments and tracked changes in version 1.2.0. ## When HTML extraction is sufficient If the user wants a summary or searchable text, extracting document content may satisfy the request without modifying the original file. Mammoth describes its output as semantic HTML and deliberately does not reproduce every visual styling detail. That is a useful choice for content extraction, but it does not establish a format-preserving round trip back into the original Word file. Before building a conversion pipeline, identify what must survive it. Test tables, lists, styles, and other structures your application accepts instead of assuming that readable HTML contains everything needed to rebuild the document. An HTML export can also omit Word fields, section settings, or pending revisions. The [developer report about DOCX-to-HTML round-trip losses](https://stackoverflow.com/questions/63694445/aspose-word-convert-docx-to-html-looses-mergefield-if-conditions-headers-and-f) identifies fields and headers as requirements to test, not a verdict on every current converter. Run an unchanged round trip as a control: if it loses required information without an agent edit, address the conversion before adding a model. Vespper retains the source DOCX in a session while exposing HTML for targeted edits. That avoids treating a generic HTML export as the entire source document, but still requires following the API's supported scope and testing the structures you promise to preserve. ## Choose where the editing runs Editing a DOCX does not inherently require an installed copy of Microsoft Word. A local library such as python-docx modifies the file package. Vespper offers a hosted document session for products and agents, and a local `vespper-mcp` server for assistants such as Claude, ChatGPT and Codex that work on files in directories you allow; the local server still communicates with Vespper's cloud service. Neither approach requires automating Word on your application server. If the file must stay entirely on the user's machine, choose a library that runs entirely locally; assess Vespper's requirements against the published [security information](https://www.vespper.com/security). Editing and rendering are separate requirements. Specify the viewer and fonts used to verify layout. If the job is to update the document already open in Word, a file download alone is insufficient: use an editor integration such as the [Office.js add-in example](https://www.vespper.com/guides/build-ai-word-add-in). ## When to use an agent editing interface For requests such as “revise this proposal's delivery terms and return changes for review,” an agent needs to locate a passage, inspect its context, and change the existing file. Vespper gives it document tools (read, search, and edit) and an HTML editing representation. New edits are tracked by default; the integration can configure that behavior. It also moves OOXML handling out of your codebase, so you maintain the session lifecycle and your validation fixtures instead of run, numbering, and revision logic; see [what your team stops maintaining](https://www.vespper.com/guides/build-docx-editing-agent#what-your-team-stops-maintaining). The current service has explicit limits on supported edit targets. It requires an API key and runs either as a hosted API or as a local server that talks to the cloud service. Evaluate those constraints alongside any local-execution, integration, or review requirements. See the [editing API](https://docs.vespper.com/primitives/edit-document) and [formatting guide](https://www.vespper.com/guides/preserve-docx-formatting). ## Compare on the same inputs For a choice between MCP implementations, the [DOCX MCP server comparison](https://www.vespper.com/compare/docx-mcp-servers) covers deployment, document handling, and review output, with official sources and Vespper's benchmark results for the listed tested versions. Test candidate implementations with the same files and instructions. Record whether the requested edit was correct, unchanged content survived, the output reopened, and required revisions remained reviewable. Measure elapsed time and total workflow cost separately if they matter to your product. A generated report, an extracted HTML summary, and a redlined version of an uploaded document are different deliverables. Define the output first, then choose the tools needed to produce and validate it. ## Next steps - [Compare DOCX MCP servers](https://www.vespper.com/compare/docx-mcp-servers): Compare deployment, document handling, and review output, with sources and benchmark limits. - [Build an editing agent](https://www.vespper.com/guides/build-docx-editing-agent): Continue the learning path with document sessions, tool calls, and output delivery. ## Related guides - [python-docx: create and edit Word documents in Python](https://www.vespper.com/llms/python-docx.md) - [Preserving DOCX formatting when an AI agent edits a file](https://www.vespper.com/llms/preserve-docx-formatting.md) --- # DOCX tracked changes API: redline a contract with an agent > Return agent edits as native Word tracked changes. Run a Python contract-editing example with a sample DOCX, revision authors, and preservation checks. Maintained by Vespper. Updated 2026-10-17. Canonical page: https://www.vespper.com/guides/tracked-changes-api Topic: [Review and validation](https://www.vespper.com/guides#review). [Start the learning path](https://www.vespper.com/guides#start-here). To let a reviewer accept or reject an AI edit in Word, return a DOCX containing native tracked revisions. A textual diff in your application and a Word file with tracked changes are different outputs. Vespper's `edit_document` tool records new edits as Word tracked changes by default, and lets an integration set the revision author or turn tracking off. The [Vespper API reference](https://docs.vespper.com/primitives/overview) describes these settings. This guide covers the editing and review workflow, including what happens when the input already contains pending revisions. ## Inspect a real API result On 29 September 2026, we ran the contract fixture below through Vespper in two modes. Both outputs changed the bold payment term from **30** to **45** days and retained the other reviewer's two pending confidentiality revisions. | Mode | Download the actual API output | Structural result | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | Tracking enabled | [Redlined DOCX](https://www.vespper.com/guides/tracked-changes-api/results/service-agreement-redlined.docx) · [check report](https://www.vespper.com/guides/tracked-changes-api/results/service-agreement-redlined.checks.json) | 15 checks passed, with font-property normalization reported. | | Tracking disabled | [Direct-edit DOCX](https://www.vespper.com/guides/tracked-changes-api/results/service-agreement-direct.docx) · [check report](https://www.vespper.com/guides/tracked-changes-api/results/service-agreement-direct.checks.json) | 13 checks passed, with the same qualification. | ![The same payment clause in the original document, the actual tracked output with 30 deleted and 45 inserted, and the direct output showing 45.](https://www.vespper.com/guides/tracked-changes-api/results/payment-change-preview.png) The preview uses crops of the actual LibreOffice renders. The one-page input and both outputs were inspected for layout, including their table, list, image, header and footer. Native Word accept/reject interaction was not part of this recorded check. **What “passed with normalization” means:** Vespper added matching complex-script font properties in the table header and page header/footer. Those locations retained their appearance in this ASCII-text fixture, but their XML was not identical. The verifier reports the additions rather than treating them as byte preservation. The [complete results and original strict failures](https://www.vespper.com/guides/tracked-changes-api/RESULTS.md) document the difference. These are two runs on one fixture, not a benchmark across arbitrary Word files. ## Run a contract redlining example Suppose your product receives an existing contract and a user asks: **“Change the payment term from 30 to 45 days. Keep the number bold and leave the other reviewer's pending changes alone.”** The deliverable is an edited DOCX that the reviewer can open and review in Word. [Download the complete Python example](https://www.vespper.com/guides/tracked-changes-api/contract-redlining-example.zip). The archive includes the [editing script](https://www.vespper.com/guides/tracked-changes-api/redline_contract.py), [verification script](https://www.vespper.com/guides/tracked-changes-api/verify_contract.py), [dependency versions](https://www.vespper.com/guides/tracked-changes-api/requirements.txt), [setup instructions](https://www.vespper.com/guides/tracked-changes-api/README.md), and recorded outputs in `results/`. You can also download the [sample service agreement](https://www.vespper.com/guides/tracked-changes-api/service-agreement.docx) separately. It is a fictional test document, not a contract template. The input contains one payment clause with **30** in bold. A separate confidentiality clause already has a deletion and insertion attributed to “Counterparty reviewer.” It also contains a table with a merged row, a numbered list, a hyperlink, an image, and a header and footer. These give the example specific things to check outside the requested edit. Unzip the archive, enter its directory, and create a Python 3.10+ environment: ```bash python3 -m venv .venv source .venv/bin/activate python -m pip install -r requirements.txt ``` Create a local `.env` containing `VESPPER_API_KEY=your_key`, using a key from your [Vespper account](https://app.vespper.com). The archive's `.gitignore` excludes local credentials and generated outputs. Run the fixed MCP sequence first: ```bash python redline_contract.py ``` This path requires a Vespper API key and makes metered Vespper calls. It finds the unique payment clause and changes the number in its returned HTML without a separate agent-model call. Vespper's service still uses its own model to reconcile the edit. The fixed sequence isolates document integration from an agent's tool selection. The core edit uses the current search result: ```python old = matches[0]["match_text"] edited = checked_result(await mcp.call_tool( "edit_document", {"edits": [{"old": old, "new": replacement(old)}]}, meta={ "com.vespper/session-id": session_id, "com.vespper/author": "Contract assistant", "com.vespper/track-changes": True, }, )) document = base64.b64decode(edited["base64"], validate=True) ``` This excerpt belongs to the downloadable script; `replacement` changes a single text-node occurrence of `30` and preserves the surrounding HTML. It refuses ambiguous matches or an unexpected text representation. It is deliberately specific to this fixture, not a general contract-editing algorithm. To let an agent choose and call the tools, add `OPENAI_API_KEY` and `OPENAI_MODEL` to your local environment or `.env`, selecting a model available to your account. Then run: ```bash python redline_contract.py --agent --output service-agreement-agent.docx ``` Agent mode sends the instruction above through the OpenAI Agents SDK. The Vespper SDK supplies the MCP tools, attaches the session and tracking metadata, and retains the updated document bytes. It keeps the server's tool descriptions. Agent tracing is disabled in this example; model requests still go to the configured provider and incur its usage charges. The recorded outputs above use the fixed MCP path; the optional model-driven path is not presented as a live-tested run. ## Check the returned DOCX The script writes the returned document and a `.checks.json` report beside it. A saved file is a candidate until the checks pass: the script exits with an error if they fail. It refuses to overwrite an existing output, so choose a new filename for each run. | Check | Required result for this fixture | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Requested edit | The proposed text changes only the payment term, from 30 to 45 days. | | Native revisions | A new deletion contains `30`; a new insertion contains `45`, both attributed to “Contract assistant.” | | Existing review state | The two “Counterparty reviewer” revisions retain their content, author, date, and revision ID. | | Formatting and structure | The payment number remains bold; unedited body structure is preserved apart from explicitly reported matching font properties. | | Package contents | The image, styles, numbering and theme retain their bytes; existing relationships remain present. Header/footer structure permits only the reported matching font additions. | You can rerun the structural checks independently, without API credentials: ```bash python verify_contract.py service-agreement.docx service-agreement-redlined.docx ``` To check the bundled recorded output before making any API call, use `results/service-agreement-redlined.docx` as the second argument. A report with `status: "passed_with_normalization"` identifies allowed additions in `font_property_additions`; conflicting values and unrelated changes still fail. It does not mean every ZIP member stayed byte-identical. These checks are specific to the supplied fixture. They are stricter than a text comparison, but do not certify visual fidelity or support for every Word feature. Open the output in Word, inspect the payment clause with revisions visible, and confirm that accepting or rejecting the payment change leaves the confidentiality revisions pending. The [formatting guide](https://www.vespper.com/guides/preserve-docx-formatting#check-the-contract-fixture) explains the remaining visual checks. To exercise the same change with tracking disabled: ```bash python redline_contract.py --tracking-off --output service-agreement-direct.docx ``` The recorded direct-edit output contains the new payment term with no new text revisions, while the two existing revisions remain pending. A new run exercises the same checks against the file your account actually receives. Keep the recorded outputs in `results/` separate from your new candidates. ## Set the author and tracking behavior The session ID identifies the document. The author and tracking flag describe how the next edit should be recorded. In a manual MCP integration, these values go in `_meta`: ```json { "_meta": { "com.vespper/session-id": "YOUR_SESSION_ID", "com.vespper/author": "Proposal assistant", "com.vespper/track-changes": true } } ``` This is a metadata fragment, not a complete request. SDK integrations can configure the equivalent author and tracking options when patching their MCP client. Setting the tracking flag to `false` applies new edits directly. It does not automatically accept, reject, or remove revisions already in the document. ## Propose edits as suggestions instead Tracked changes and suggestions solve different review problems. Tracked changes are a way to record an edit that has already been applied. In suggestions mode, the document stays unchanged until the user accepts an edit. Enable it when patching the MCP client with `suggest=True` in Python or `suggest: true` in TypeScript: ```python await client.patch_mcp_tools( mcp=mcp, session_id=session_id, suggest=True, ) ``` This is an excerpt, not a standalone program. With suggestions on, `read_document` and `search_document` behave as before. `edit_document` only locates each `old`/`new` edit against the current document and returns it as a suggestion instead of applying it. Your client shows each suggestion, and the user can change its HTML, accept it, or discard it. Your API then sends the accepted pairs to `apply_edits()` (`applyEdits()` in TypeScript), which applies them in parallel. Each outcome's `position` is the edit's index in the list you sent. `apply_edits()` accepts any session ID, so the request that applies suggestions does not have to be the one that ran the agent. Suggestions fit products that show proposed edits in a chat or side panel before anything touches the file. Direct mode with tracking on fits workflows where reviewers work inside Word. The [suggestions reference](https://docs.vespper.com/live-editing/suggestions) and the [live editing overview](https://docs.vespper.com/live-editing/introduction) cover streaming suggestions as the model writes them, and the [ONLYOFFICE add-in cookbook](https://docs.vespper.com/cookbooks/live-editing/onlyoffice-add-in) shows both modes in an editor. ## Supply the final text of the replacement Read or search the document first. Supply the current HTML block as `old` and the intended final version as `new`. For ordinary new edits, do not manufacture `` or `` tags: the editing tool creates the revision markup according to the configured tracking setting. For a deadline change, the intended result is that the old day is marked as deleted and the new day as inserted. A viewer may show both while the revision remains pending. That display is expected and is not a reason to repeat an edit that already succeeded. ## Keep unrelated pending revisions intact Vespper represents existing revisions in its reading output with `` and `` elements and a `data-author` attribute. When a user asks to resolve a specific change, copy its complete current block and preserve other pending changes in that block. | Requested action | Intended final content for that revision | | ------------------- | ---------------------------------------------------------------- | | Accept an insertion | Keep the inserted text and remove its pending-insertion wrapper. | | Reject an insertion | Remove the inserted text and its wrapper. | | Accept a deletion | Remove the deleted text and its wrapper. | | Reject a deletion | Restore the text without its pending-deletion wrapper. | These operations are scoped to the revision the user asked about. Follow the [existing-revision rules](https://docs.vespper.com/primitives/edit-document#resolving-an-existing-tracked-change), including author preservation when modifying a pending change. Flattening all revision markup would also resolve changes the user never selected. The table describes text insertions and deletions. It is not a universal implementation of “accept all”: Word revisions can also involve moves, formatting, and table structure. Check the revision types present and the engine's supported scope before offering that operation. Comments are separate objects; verify what happens to a comment when its anchored text is deleted or its revision is resolved. ## Read text without silently resolving revisions Before extracting plain text, choose whether the application needs proposed wording, original wording, or the pending review state. | View | Inserted text | Deleted text | | --------------------- | -------------------- | -------------------- | | Proposed wording | Include | Exclude | | Original wording | Exclude | Include | | Review representation | Keep with provenance | Keep with provenance | The [python-docx Document API](https://python-docx.readthedocs.io/en/stable/api/document.html) notes that paragraphs inside revision containers are omitted from `document.paragraphs`. [Issue 1123](https://github.com/python-openxml/python-docx/issues/1123) records the resulting missing-text problem. Concatenating every XML text node is not a general repair: it can join deleted and inserted alternatives into wording the author never wrote. Use a revision-aware extractor or resolve revisions on a separate copy with an implementation that supports those revision types. Producing a proposed-text view does not accept changes in the source file, and reading a file is not authorization to resolve its pending suggestions. For an editing agent using Vespper, keep the returned revision wrappers and authors in the current HTML anchor. ## Connect the example to your product The sample proves a narrow workflow only when its output passes your checks. Extend it with documents and revision types your customers actually submit. The [agent integration guide](https://www.vespper.com/guides/build-docx-editing-agent) covers session ownership, partial edits, and delivering the latest file through your application. The [Word add-in example](https://www.vespper.com/guides/build-ai-word-add-in) follows Word's tracking mode when submitting a request. A background agent should instead expose and document its own tracking policy. If you are implementing this with `python-docx`, its comment API and ordinary text setters are not a tracked-edit API. The [Python guide](https://www.vespper.com/guides/python-docx) covers that distinction for version 1.2.0. Review comments are useful, but adding a comment does not convert a direct text replacement into a tracked insertion and deletion. ## Next steps - [Connect your application](https://docs.vespper.com/quickstart): Use the API quickstart to connect your SDK or client and save the edited document. - [Build the review workflow inside Word](https://www.vespper.com/guides/build-ai-word-add-in): Use the Office.js example when users need the agent's edits in their open document. ## Related guides - [Build an AI agent that edits existing DOCX documents](https://www.vespper.com/llms/build-docx-editing-agent.md) - [Preserving DOCX formatting when an AI agent edits a file](https://www.vespper.com/llms/preserve-docx-formatting.md) --- # Find and edit a passage in a large DOCX document > Use document search, page ranges, unique HTML anchors, and revision checks to target edits in long Word files without loading the whole file into an agent. Maintained by Vespper. Updated 2026-09-28. Canonical page: https://www.vespper.com/guides/search-large-documents Topic: [Editing existing documents](https://www.vespper.com/guides#editing). [Start the learning path](https://www.vespper.com/guides#start-here). When an agent knows the text it needs to change, searching for that text can avoid loading the entire document into its context. Vespper's `search_document` searches the current DOCX session and returns complete HTML blocks that can be used as edit anchors. Use `read_document` when the agent needs surrounding structure or a range of pages. This is a document-editing workflow. A semantic search result or a summary can help identify a topic, but an edit still needs to target the actual current passage unambiguously. ## Start with a phrase or a regular expression The search tool accepts a Python regular expression in `pattern`. For example, the following arguments search case-insensitively for a deadline label: ```json { "pattern": "(?i)delivery date", "start_at": 1 } ``` These are tool arguments; the integration also supplies the document session through MCP metadata. The [search reference](https://docs.vespper.com/primitives/search-document) explains pattern syntax and the returned fields. Search matches both the document's HTML and its visible text. That lets a plain phrase match text split across formatting spans. A returned `match_text` contains a complete block, while `matched` identifies the portion that matched the pattern. Preserve the complete block when building an edit anchor. ## Check scope and ambiguity before editing A result's `page` helps locate the passage. Its `unique` flag tells you whether the block's text also occurs elsewhere. Two identical placeholders can be separate results even when their HTML anchors look the same. If `unique` is false, the page number helps you choose the intended occurrence, but simply copying that duplicate block into `old` does not make the edit unique. Read its surroundings and extend the anchor with an adjacent, distinguishing block. Do not combine non-adjacent passages into one anchor. The [anchor rules](https://docs.vespper.com/primitives/edit-document#make-old-unique) describe this operation. If the user asked for one deadline and search returns several plausible deadlines, use section context to resolve the target or ask the user which one they mean. Applying every match changes the scope of the request. ## Follow the returned pagination One response can contain only part of a large result set. When `has_more` is true, advance `start_at` by `returned_matches` to retrieve the next set. Do not treat the first response as evidence that no further matches exist. For page reading, advance from the response's actual `end_page`, not the end page originally requested. The [read reference](https://docs.vespper.com/primitives/read-document) explains that a token limit may shorten the returned range. Its `has_more` flag tells you whether pages remain. | Need | Operation | | --------------------------------------------- | --------------------------------------------------------------- | | Locate a known date, name, or phrase | Search, then inspect candidate blocks. | | Understand a section's structure | Read its page range. | | Change every occurrence requested by the user | Follow search pagination and account for every intended result. | | Insert content after a particular section | Read the section and identify its final block. | ## Treat page numbers as locations, not permanent anchors DOCX content flows across pages according to fonts, margins, document settings, and the rendering environment. A page range helps locate a passage; the edit must still use its current structural block. Adding words can move that block to another page. | Break information | Meaning | | ------------------------------- | -------------------------------------------------------------------- | | Explicit page break | The document requests a new page. | | Paragraph page-break setting | A paragraph should begin on a new page. | | Last rendered page-break marker | A saved trace of earlier pagination, not a fresh layout calculation. | The [python-docx question about inserting at a new page](https://github.com/python-openxml/python-docx/issues/1245) illustrates the danger of treating saved break markers as page containers. If a user asks to insert at the start of page 3, identify the paragraph currently there. If they instead need a section to always start on a new page, that is a structural formatting requirement for the chosen editing implementation. After a layout-changing edit, read again and verify the result in the target viewer. ## Use current anchors and retain the new document Read, search, and edit results include the session revision. If the target passage changes after it was read, obtain its current HTML before constructing another edit. Coordinate concurrent requests in your application so an agent does not unknowingly work from an older state. After editing, inspect the applied count and any failures, save the latest returned DOCX, and verify the requested passage. A partial edit batch has already changed the document; recover only the failed edits instead of resending the whole request. ## Next steps - [Check the structures around the edit](https://www.vespper.com/guides/preserve-docx-formatting): Define which formatting and document structures must survive a change to the matched passage. - [Return a reviewable document](https://www.vespper.com/guides/tracked-changes-api): Apply the review workflow and inspect native revisions in the returned DOCX. ## Related guides - [Build an AI agent that edits existing DOCX documents](https://www.vespper.com/llms/build-docx-editing-agent.md) --- # Find and replace text in DOCX tables with Python > Find text in table-cell paragraphs, including nested tables, without treating merged cells as separate values or overwriting the whole cell. Maintained by Vespper. Updated 2026-09-28. Canonical page: https://www.vespper.com/guides/python-docx-replace-in-tables Topic: [Python DOCX troubleshooting](https://www.vespper.com/guides#python). [Start the learning path](https://www.vespper.com/guides#start-here). `document.paragraphs` does not walk into every table cell. A replacement loop that only visits body paragraphs can finish successfully while leaving all the text in tables unchanged. Traverse table cells explicitly, and inspect the paragraphs inside each cell. Before changing content, separate traversal from replacement. First print the paragraphs your code will visit. Then apply a replacement function with a known scope, such as the [text-run example](https://www.vespper.com/guides/python-docx-replace-across-runs). ## Traverse cells and nested tables This example for python-docx 1.2.0 visits direct body paragraphs, then table-cell paragraphs. It does not preserve the reading order between body paragraphs and tables. ```python from docx import Document def paragraphs_in(container, seen_cells=None): if seen_cells is None: seen_cells = set() yield from container.paragraphs for table in container.tables: for row in table.rows: for cell in row.cells: # A merged cell can appear at several grid positions. if cell._tc in seen_cells: continue seen_cells.add(cell._tc) yield from paragraphs_in(cell, seen_cells) doc = Document() table = doc.add_table(rows=1, cols=2) table.cell(0, 0).text = "Client: Example Co" table.cell(0, 1).text = "Status: Draft" assert [p.text for p in paragraphs_in(doc)] == [ "Client: Example Co", "Status: Draft" ] ``` `_tc` is a private XML attribute. This example uses its element identity to avoid visiting the same merged cell repeatedly; pin your dependency and test against your fixtures. [Repeated merged-cell output](https://github.com/python-openxml/python-docx/issues/670) is a longstanding source of confusion. ## Replace content at the right level Assigning `cell.text` replaces the cell's existing contents with a new paragraph and run. That is appropriate when creating a plain table from scratch. It is unsuitable when an existing cell contains several paragraphs, bold labels, hyperlinks, or an embedded table that must survive. A phrase can still span several runs inside a cell. Finding the right cell does not solve the formatting problem. Count matches before saving, and use row labels and column headings to distinguish repeated values such as “Pending.” ## Scope an agent's table edit For an agent, “change the delivery date for Phase 2” is a better target than “replace the second occurrence of Friday.” Read the surrounding table, resolve the row and column, and retain its structure when constructing the edit. With Vespper, use the exact current HTML returned by reading or searching; do not rebuild the whole table from a plain-text extraction. This traversal does not include headers, footers, text boxes, or revision-contained paragraphs. Test those separately if they are part of your input contract. ## Preserve merged-cell structure Several grid positions can refer to one logical merged cell. Deduplicating those positions prevents repeated writes, but does not reconstruct the complete visual geometry. The [python-docx table guide](https://python-docx.readthedocs.io/en/stable/user/tables.html) covers merged, omitted, and nested cells. For a text update, retain the table, rows, cells, and merge structure. Rebuilding a table from extracted strings can lose widths, borders, alignment, or nested content. With Vespper, retain the current returned HTML structure and classes. If candidate cells are identical, use distinguishing adjacent context; a page number alone does not make an identical anchor unique. Verify a horizontal merge, a vertical merge, repeated labels, and a nested table. Check that the target changes once and unrelated cells keep their content, then inspect widths, merges, and page-spanning behavior in the target viewer. Correct text alone cannot establish correct table layout. ## Next steps - [Handle text split across cell runs](https://www.vespper.com/guides/python-docx-replace-across-runs): Apply a formatting policy when the phrase spans multiple runs inside a cell paragraph. - [Verify the surrounding document](https://www.vespper.com/guides/preserve-docx-formatting): Check tables and other document structures after applying the targeted change. ## Related guides - [python-docx: create and edit Word documents in Python](https://www.vespper.com/llms/python-docx.md) --- # Preserving DOCX formatting when an AI agent edits a file > Understand run formatting, targeted HTML edits, document structure, and practical checks for preserving an uploaded DOCX during agent-driven editing. Maintained by Vespper. Updated 2026-10-17. Canonical page: https://www.vespper.com/guides/preserve-docx-formatting Topic: [Editing existing documents](https://www.vespper.com/guides#editing). [Start the learning path](https://www.vespper.com/guides#start-here). Preserving DOCX formatting requires keeping the document structures that carry it. Changing a sentence's text is not the same operation as rebuilding its paragraph or regenerating the entire document. For an agent editing a user-uploaded file, prefer a targeted change and verify the surrounding content in the output. Formatting includes several distinct concerns: character formatting such as bold, paragraph styles, tables, lists, section layout, and existing review markup. A successful text match does not, by itself, verify all of them. ## Why a simple replacement can change appearance In `python-docx`, a paragraph contains runs that hold character formatting. Assigning to `paragraph.text` replaces its content with one run. The paragraph style remains, but the original run-level formatting is removed. Assigning to a plain-text run's `.text` preserves that run's formatting, while replacing its content. These behaviors are documented in the [text API reference](https://python-docx.readthedocs.io/en/stable/api/text.html). The [runnable Python example](https://www.vespper.com/guides/python-docx) demonstrates this on the sentence “The draft is due on Friday,” where Friday is bold. It also shows why a per-run replacement misses a word split between two runs. Those are concrete, reproducible cases rather than evidence that every library save destroys formatting. ## Edit the current document through a bounded representation Vespper exposes document content as HTML through `read_document` and `search_document`. The agent supplies a current HTML anchor and its replacement to `edit_document`; Vespper applies that change to the DOCX. This gives the agent an editing interface without requiring it to rewrite the underlying OOXML package. Vespper uses a trained reconciler to map the affected HTML block to OOXML, then computes the revision markup and patches it into the original file. Keeping the original package is different from promising that every XML byte stays unchanged. The [launch article's architecture section](https://www.vespper.com/blog/launching-vespper-docx-mcp) explains the mechanism; the recorded example below exposes the actual files and the differences we found. This is also a maintenance boundary. Run mapping, numbering definitions, and revision markup live in Vespper's service, so your code does not carry a hand-built version of them that must be revisited for each new document structure. The [python-docx run-replacement guide](https://www.vespper.com/guides/python-docx-replace-across-runs) shows what that hand-built version looks like for a single narrow case. Your tests still matter: the checks below apply to either approach. For a local text change: 1. Read or search the relevant passage in the current session. 2. Use a complete, unique block as the anchor. 3. Change only the requested content in the replacement. 4. Retain the updated DOCX and check the tool's applied and failed edit results. 5. Inspect the affected area and nearby content in the document viewer. The [edit API](https://docs.vespper.com/primitives/edit-document) documents a `class="..."` shorthand for retaining existing classes. New blocks need an explicit class when they have no corresponding original block from which to inherit. Use the current tool descriptions for the exact anchor and class rules. ## Account for unsupported edit targets Vespper's current edit reference rejects edits to the global HTML stylesheet and direct anchors on Word-generated list numbers or bullets. To change a list item's content, target its content block; do not treat its generated number as ordinary text. These limits matter when choosing a document engine. Test the structures your application actually promises to edit, and describe the supported scope to users. A body-text demonstration does not establish support for every object in a Word file. ## Preserve generated list numbering A Word list's visible label can come from numbering definitions rather than literal paragraph text. If an extractor returns “Scope of work” for a paragraph displayed as “3. Scope of work,” adding the string “3.” can create a duplicate label. The [python-docx lists-as-paragraphs report](https://github.com/python-openxml/python-docx/issues/217) illustrates why text alone is insufficient to reconstruct an outline. For a wording change, keep the existing list structure, level, and classes, and anchor on its content. Adding, restarting, or moving an item is a separate structural operation. A default `List Number` style does not reproduce every custom multilevel scheme. Cross-references and the table of contents may also need refreshing in the target application. Test a nested list, a list that continues across intervening paragraphs, and an intentional restart. Inspect later items as well as the edited paragraph: a numbering error may appear farther down the document. ## Check the contract fixture Use the [contract redlining example](https://www.vespper.com/guides/tracked-changes-api#run-a-contract-redlining-example) as a small, inspectable starting point. Its [input DOCX](https://www.vespper.com/guides/tracked-changes-api/service-agreement.docx) asks the editor to change a bold payment term from 30 to 45 days. The other clause contains pending revisions, and the surrounding document includes a merged table row, a numbered list, an image, a hyperlink, and header and footer content. The downloadable [verifier](https://www.vespper.com/guides/tracked-changes-api/verify_contract.py) examines both the original and proposed wording, revision metadata, bold formatting on the changed number, unedited body structure, package relationships, and selected unchanged package parts. It fails if the input is returned without the requested edit. It also distinguishes tracking enabled from a direct replacement and reports a narrow class of matching font-property additions separately. | Structural check | Visual check still needed | | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Payment term retains bold run properties | The revised number has the intended appearance in Word. | | Unedited table/list structure matches, with any allowed font additions listed | Table cells still fit their content and list labels display correctly. | | Image bytes and its body anchor remain intact | The image appears in the intended position at the intended size. | | Styles and section properties remain intact | Fonts, page boundaries, and spacing still work in the target viewer. | | Prior revision markup remains intact | Word exposes both reviewers' pending changes for review. | Run the script to generate your candidate output and its checks report, then open the original and output side by side in the viewer your users use. With tracked changes visible, the old and new payment terms should both appear as pending alternatives. Inspect a copy with the new payment change accepted to assess the final layout. Do not accept the unrelated confidentiality revisions as part of that check. ## What the recorded outputs preserve On 29 September 2026, the [tracked output](https://www.vespper.com/guides/tracked-changes-api/results/service-agreement-redlined.docx) passed 15 structural checks and the [direct output](https://www.vespper.com/guides/tracked-changes-api/results/service-agreement-direct.docx) passed 13. Both changed **30** to **45** in bold and retained the original reviewer, content, dates and IDs of the pending confidentiality revisions. Both rendered as one page, and we inspected the payment clause, table, list, image, header, footer and surrounding layout in LibreOffice. The service also added matching `w:bCs` font properties to the two bold table-header runs and matching `w:szCs` properties to the header and footer. The affected runs contain only ASCII text. Their appearance was retained in this render, but the table/header/footer XML was not identical to the input. The initial strict verifier rejected those changes. Version 2 reports **`passed_with_normalization`** only when the other checks pass and those additions match existing font settings; it still rejects conflicting properties or unrelated changes. The [result record](https://www.vespper.com/guides/tracked-changes-api/RESULTS.md) includes the initial failures, current reports, file hashes and validation limits. We did not edit the API output files to make them pass. This distinction matters when your requirement is exact XML preservation or when documents contain complex-script text. Do not generalize the ASCII fixture's result to other scripts, inherited styles, revision types or larger documents. Native Word accept/reject interaction and a model-driven agent run were not verified in these recorded results. ## Expand beyond the fixture Keep a small set of input documents with the structures your users submit: mixed bold and italic text, repeated phrases, lists, merged table cells, section breaks, and existing revisions. Include unchanged sections so you can detect unintended modifications outside the target. For each requested edit, check both meaning and presentation. Did the intended passage change? Is neighboring content intact? Does the file reopen? Are lists and tables still usable? If review is required, are the revisions still pending and attributed correctly? Use these checks for any editing implementation, including Vespper. They define what “preserved” means for your application and help separate a successful demonstration from a workflow validated on representative inputs. ## Next steps - [Return edits as tracked changes](https://www.vespper.com/guides/tracked-changes-api): Continue the learning path with native revisions, author metadata, and a reviewable output. - [Inspect the recorded preservation checks](https://www.vespper.com/guides/preserve-docx-formatting#what-the-recorded-outputs-preserve): Review what the contract fixture preserved, the normalization observed, and the remaining visual checks. ## Related guides - [Build an AI agent that edits existing DOCX documents](https://www.vespper.com/llms/build-docx-editing-agent.md) - [DOCX libraries for AI agents: choose by editing workflow](https://www.vespper.com/llms/docx-libraries-for-agents.md) --- # python-docx: create and edit Word documents in Python > Install python-docx, read and edit existing Word files, and replace text while keeping run formatting. Runnable examples, comments, and tracked-change limits. Maintained by Vespper. Updated 2026-09-27. Canonical page: https://www.vespper.com/guides/python-docx Topic: [Python DOCX troubleshooting](https://www.vespper.com/guides#python). [Start the learning path](https://www.vespper.com/guides#start-here). [`python-docx`](https://python-docx.readthedocs.io/en/stable/) is an open-source Python library for reading, creating, and modifying `.docx` files. Use it to generate a report, inspect paragraphs and tables, or make a known change to an existing Word document. This guide starts with a small document you can create locally. You'll read it back, replace a deadline, and see why the way you replace text affects its formatting. We'll also cover comments and the extra work involved when an AI agent needs to return Word tracked changes. ## Install python-docx Use Python 3.9 or newer. In your project's virtual environment, install the package with the same Python interpreter that will run your script: ```bash python -m pip install "python-docx==1.2.0" ``` The package name is **python-docx**; the Python import is **docx**. These examples were run with version 1.2.0, the [current PyPI release when this guide was published](https://pypi.org/project/python-docx/). ```python from docx import Document ``` You don't need Microsoft Word installed to run the library. You'll need a document viewer to inspect the resulting layout. The library works on `.docx` files; convert legacy `.doc` files to `.docx` before opening them. ## Create a Word document in Python Save this as `create_document.py`, then run `python create_document.py`. It creates `proposal.docx` in your current directory: ```python from docx import Document document = Document() title = document.add_paragraph() title.add_run("Project proposal").bold = True paragraph = document.add_paragraph("The draft is due on ") paragraph.add_run("Friday").bold = True paragraph.add_run(".") document.add_paragraph("Prepare a draft for review", style="List Bullet") document.add_paragraph("Collect feedback", style="List Bullet") document.save("proposal.docx") ``` The deadline paragraph contains three **runs**: the opening text, the bold word “Friday,” and the period. A run holds text and its character formatting. A paragraph can contain many runs, even when it looks like one continuous sentence in Word. Starting with `Document()` uses the library's default template. To reuse your own document's styles and layout, open it with `Document("template.docx")` instead. ## Read an existing DOCX file Open the file and iterate over its body paragraphs: ```python from docx import Document document = Document("proposal.docx") for paragraph in document.paragraphs: print(paragraph.text) ``` For this example, the output is: ```text Project proposal The draft is due on Friday. Prepare a draft for review Collect feedback ``` `document.paragraphs` isn't a complete text extractor for every part of a Word file. For example, table cells have their own paragraphs. The [Document API](https://python-docx.readthedocs.io/en/stable/api/document.html) also notes that paragraphs inside revision elements don't appear in this list. Decide which parts of a document your application needs to read before treating extracted text as the whole file. ### Read and update a table This example adds a small table to the proposal, updates a cell, and reads the rows back: ```python from docx import Document document = Document("proposal.docx") table = document.add_table(rows=2, cols=2) table.style = "Table Grid" table.cell(0, 0).text = "Milestone" table.cell(0, 1).text = "Status" table.cell(1, 0).text = "Draft" table.cell(1, 1).text = "Planned" table.cell(1, 1).text = "In review" for row in table.rows: print([cell.text for cell in row.cells]) document.save("proposal-with-table.docx") ``` For an existing table, select it from `document.tables`. Assigning a cell's `.text` replaces its content; use its paragraphs and runs when you need finer control over text that is already formatted. This example uses ordinary, unmerged cells. ## Replace text without removing run formatting For a known phrase contained in a plain-text run, edit that run's text. This changes Friday to Monday in the proposal while keeping the deadline bold: ```python from docx import Document document = Document("proposal.docx") replacements = 0 for paragraph in document.paragraphs: for run in paragraph.runs: if "Friday" in run.text: replacements += run.text.count("Friday") run.text = run.text.replace("Friday", "Monday") if replacements != 1: raise ValueError(f"Expected one deadline, found {replacements}") document.save("proposal-run-edit.docx") ``` The count check catches a missing or ambiguous match before saving. Keeping the original and output filenames different also makes it easy to inspect the result against the source. **Scope of this example:** it visits direct runs in body paragraphs. It doesn't search table cells, headers, footers, hyperlinks, text boxes, or revision markup. Setting `run.text` preserves run formatting but replaces the run's content, so don't apply this blindly to runs containing drawings or fields. ### Why paragraph.text can remove bold and italic This shorter replacement changes the text, but removes the separate bold run: ```python from docx import Document document = Document("proposal.docx") for paragraph in document.paragraphs: if "Friday" in paragraph.text: paragraph.text = paragraph.text.replace("Friday", "Monday") document.save("proposal-paragraph-edit.docx") ``` The [`Paragraph.text` setter](https://python-docx.readthedocs.io/en/stable/api/text.html#docx.text.paragraph.Paragraph.text) replaces the paragraph's content with a single run. Its paragraph style remains, but the original run-level bold and italic formatting is removed. In our sample, Monday is plain text after the paragraph assignment and bold after the run assignment. | Editing method | Result | Deadline formatting | | -------------------------- | --------------------------- | --------------------------- | | Original document | The draft is due on Friday. | Friday is bold; three runs. | | Assign to `paragraph.text` | The draft is due on Monday. | Monday is plain; one run. | | Assign to `run.text` | The draft is due on Monday. | Monday is bold; three runs. | Reproduce that result with the [complete formatting example](https://www.vespper.com/guides/python-docx/formatting-example.py). After installing `python-docx`, save the script in an empty working directory and run `python formatting-example.py`. It writes three files there; rerunning it replaces those same example files. You can also inspect the outputs directly: [original proposal](https://www.vespper.com/guides/python-docx/proposal.docx), [paragraph edit](https://www.vespper.com/guides/python-docx/proposal-paragraph-edit.docx), and [run edit](https://www.vespper.com/guides/python-docx/proposal-run-edit.docx). We reopened these files to check their text and run formatting, and rendered each for a visual check. This demonstrates the behavior on this small sample, not on every possible Word document. ### What if the phrase spans several runs? A per-run replacement won't find a phrase split across runs. You can reproduce that case without loading a file: ```python from docx import Document document = Document() paragraph = document.add_paragraph() paragraph.add_run("Fri").bold = True paragraph.add_run("day") print(paragraph.text) # Friday print([run.text for run in paragraph.runs]) # ['Fri', 'day'] for run in paragraph.runs: run.text = run.text.replace("Friday", "Monday") print(paragraph.text) # Still Friday ``` The full word exists in the paragraph, but neither run contains it. Handling that case means mapping character positions back to runs and deciding which formatting the replacement should inherit. For example, if only “Fri” is bold, should all of “Monday” be bold? That decision belongs in your editing logic. When your script controls the template, you can arrange placeholders to make replacements predictable. For documents uploaded by users, test split runs and the other document structures your workflow accepts. ## Add a comment with python-docx Version 1.2.0 includes a public API for adding comments. This anchors a review note to the deadline run: ```python from docx import Document document = Document("proposal.docx") deadline = next( run for paragraph in document.paragraphs for run in paragraph.runs if run.text == "Friday" ) document.add_comment( runs=deadline, text="Please confirm this deadline before sending.", author="Reviewer", initials="RV", ) document.save("proposal-commented.docx") ``` Comments attach to run boundaries. The [comment API reference](https://python-docx.readthedocs.io/en/stable/api/document.html#docx.document.Document.add_comment) explains how a run or sequence of runs defines the commented range. A comment adds a review note; it doesn't turn a text replacement into a tracked insertion and deletion. ## Does python-docx support tracked changes? **The public API in python-docx 1.2.0 doesn't provide a method that records ordinary text edits as Word tracked changes.** The project's [tracked-changes feature request](https://github.com/python-openxml/python-docx/issues/340) remains open as of this guide's publication. The replacements above write the new text directly. Returning a redline requires another layer: for example, code that creates the appropriate revision markup, or a document-editing service that produces tracked changes. Writing the XML yourself also means handling revision authorship and checking how your edits interact with existing revisions. That limitation doesn't mean every save discards all existing document features. The important questions are which structures your code reads, which it changes, and whether the output preserves what your workflow requires. Test representative input documents, including ones that already contain revisions. ## Using python-docx in an AI document-editing agent `python-docx` is useful when your application knows exactly what operation to perform. An agent can call your own Python functions to update a field, add a paragraph, or fill a table. Your application still needs to choose the target, handle formatting boundaries, and validate the result. For an instruction such as “update the delivery terms in this proposal and let me review the changes,” the editing layer has more work to do. The agent must locate the relevant passage, make the change in the existing file, and return a usable document for review. Vespper's **DOCX MCP Server** gives an agent three document tools: `read_document`, `search_document`, and `edit_document`. The agent works with an HTML representation; Vespper applies edits to the DOCX and can return them as Word tracked changes. You can connect it from Python using the [Vespper SDK](https://www.vespper.com/blog/vespper-sdks). | Your requirement | Where to start | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Generate a report or make a known edit in a controlled template | `python-docx`, with explicit Python logic for the document structures you use. | | Add review comments to selected text | `python-docx` 1.2.0's comment API. | | Let an agent edit an existing document through read, search, and edit tools | Vespper DOCX MCP, connected to your agent framework. | | Return the agent's text edits as tracked changes | A workflow that creates native revisions, such as Vespper's tracked-edit workflow. | `python-docx` runs locally and is MIT-licensed. Vespper uses a hosted service and requires a Vespper API key; an AI agent also needs its model-provider configuration. Choose based on the actual editing and review workflow you need to build. To try the agent workflow, follow the [Python integration in the Vespper quickstart](https://docs.vespper.com/quickstart) with your own DOCX. For a task pane inside Word, see [Build an AI Word add-in with Office.js and Vespper](https://www.vespper.com/guides/build-ai-word-add-in). ## Next steps - [Handle phrases split across runs](https://www.vespper.com/guides/python-docx-replace-across-runs): Extend a simple replacement to text that crosses formatting boundaries. - [Choose the tools for your agent](https://www.vespper.com/guides/docx-libraries-for-agents): Compare application-controlled Python operations with templates, conversion, and an MCP editing interface. ## Related guides - [Preserving DOCX formatting when an AI agent edits a file](https://www.vespper.com/llms/preserve-docx-formatting.md) - [DOCX tracked changes API: redline a contract with an agent](https://www.vespper.com/llms/tracked-changes-api.md) --- # Replace text split across runs in python-docx > Replace a phrase that spans multiple Word runs without flattening the paragraph. Decide how replacement text inherits formatting and handle ambiguous matches. Maintained by Vespper. Updated 2026-09-27. Canonical page: https://www.vespper.com/guides/python-docx-replace-across-runs Topic: [Python DOCX troubleshooting](https://www.vespper.com/guides#python). [Start the learning path](https://www.vespper.com/guides#start-here). A Word paragraph can display one continuous sentence while storing it in several runs. Searching each run separately misses a phrase that crosses a run boundary. Searching `paragraph.text` finds the phrase, but assigning back to that property replaces the paragraph's run structure. This is a documented developer problem in [python-docx issue 415](https://github.com/python-openxml/python-docx/issues/415). The following example demonstrates a deliberately narrow solution for a single match in a paragraph containing only ordinary text runs. It uses python-docx 1.2.0. ## Map character positions back to runs ```python from docx import Document from docx.oxml.ns import qn def replace_once(paragraph, old, new): if not old: raise ValueError("The search text must not be empty") # Reject hyperlinks, fields, drawings, breaks, and revision containers. if any(child.tag not in {qn("w:pPr"), qn("w:r")} for child in paragraph._p): raise ValueError("Expected direct text runs only") runs = paragraph.runs for run in runs: if any(child.tag not in {qn("w:rPr"), qn("w:t")} for child in run._r): raise ValueError("Expected ordinary text only") text = "".join(run.text for run in runs) start = text.find(old) if start < 0 or text.find(old, start + 1) >= 0: raise ValueError("Expected exactly one match") stop = start + len(old) offset = 0 for run in runs: value = run.text end = offset + len(value) if offset < stop and end > start: left = max(0, start - offset) right = min(len(value), stop - offset) replacement = new if offset <= start < end else "" run.text = value[:left] + replacement + value[right:] offset = end doc = Document() p = doc.add_paragraph() p.add_run("Fri").bold = True p.add_run("day deadline") replace_once(p, "Friday", "Monday") assert p.text == "Monday deadline" assert p.runs[0].bold is True ``` ## Choose a formatting policy This function gives the entire replacement the formatting of the run where the match begins. In the example, all of “Monday” becomes bold, although only “Fri” was bold originally. Unmatched text remains in its original runs. That is a policy choice, not the only correct interpretation of mixed formatting. The two underscored XML properties are implementation details used to reject unsupported content. Pin and test your library version. This is not a complete replacement engine for arbitrary customer documents. ## Move from a script to an agent An agent also needs to establish which occurrence the user means. For Vespper, search the document, take the complete current HTML block, and edit that block with its classes intact. If the block is not unique, extend the anchor with adjacent content. The agent works with returned document structure rather than implementing run offsets itself. Test your replacement on an absent phrase, a repeated phrase, mixed formatting, and a run containing an image. The last case should be rejected by this example. See the [Python text API](https://python-docx.readthedocs.io/en/stable/api/text.html) for the behavior of paragraph and run setters. ## Next steps - [Check images inside text runs](https://www.vespper.com/guides/python-docx-preserve-images): Inspect non-text content before assigning to a run's text. - [Define document preservation checks](https://www.vespper.com/guides/preserve-docx-formatting): Expand validation beyond the matched phrase to surrounding formatting and document structure. ## Related guides - [python-docx: create and edit Word documents in Python](https://www.vespper.com/llms/python-docx.md) --- # Why images disappear after python-docx text replacement > Avoid removing inline drawings when changing DOCX text. Inspect run content, skip unmatched runs, and verify images before and after the edit. Maintained by Vespper. Updated 2026-09-27. Canonical page: https://www.vespper.com/guides/python-docx-preserve-images Topic: [Python DOCX troubleshooting](https://www.vespper.com/guides#python). [Start the learning path](https://www.vespper.com/guides#start-here). An image can share a Word run with text. Assigning to that run's `.text` replaces the run's content, including any drawing it contained. This can remove a picture even when the assigned text looks unchanged. Replacing an entire paragraph's `.text` can remove its non-text content too. The failure was reported in [python-docx issue 272](https://github.com/python-openxml/python-docx/issues/272). Its lesson still matters: a text replacement is a structural write, not a harmless string assignment. ## Avoid touching runs that do not match Consider this pattern: ```python for run in paragraph.runs: run.text = run.text.replace("OLD", "NEW") ``` It writes to every run, including runs with no matching text. Checking for the target before assigning reduces unnecessary writes, but does not make the operation safe for a run that contains both matching text and a drawing. For a narrow replacement helper, require ordinary text-only runs and reject everything else. The [cross-run replacement example](https://www.vespper.com/guides/python-docx-replace-across-runs) demonstrates this guard. A general implementation needs to edit the appropriate text nodes while retaining drawing elements and their relationships. ## Record what should survive Before editing, record the location and purpose of the images that matter: a logo in a header, an inline chart in a table cell, or an illustration beside the paragraph being changed. An unchanged number of files under `word/media/` is not sufficient proof that pictures still appear. Unreferenced media can remain in the package after the drawing that displayed it has been removed. Compare the relevant drawing elements, relationships, and visible output. If you inspect `document.inline_shapes`, remember that inline shapes are only one category of document artwork. Do not interpret that count as a complete inventory of floating shapes, header images, and every other drawing. ## Test the edit next to the image Use a fixture in which the target text and a picture share a paragraph, plus another fixture with the picture in the same run. Ask for a small text change. Reopen the output and inspect both pictures in the target viewer. Include a control case where no text matches: that run should remain untouched. For Vespper, read the current document block and preserve unrelated structure when submitting the replacement. Do not reconstruct an image-bearing paragraph from its visible text alone. The [formatting guide](https://www.vespper.com/guides/preserve-docx-formatting) covers using returned HTML and classes as the basis for an edit. ## Next steps - [Handle phrases split across runs](https://www.vespper.com/guides/python-docx-replace-across-runs): Locate text across formatting boundaries while deciding which run content is safe to change. - [Check preservation beyond images](https://www.vespper.com/guides/preserve-docx-formatting): Include tables, lists, formatting, and existing review markup in your validation. ## Related guides - [Build an AI agent that edits existing DOCX documents](https://www.vespper.com/llms/build-docx-editing-agent.md)