# 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)

