AI agent skill · by @TokenRhythm
Docx
Read, inspect, edit, or create Microsoft Word `.docx` documents, including structured text extraction, style-preserving edits, tracked-change review, and generation from a brief.
Archived 2026-10-23 · Source updated 2026-09-17 · License: Apache-2.0. Check the original repository for the latest version.
SKILL.md instructions
# docx
Work with Microsoft Word `.docx` files. The format is OOXML — a zip container
holding XML parts (`word/document.xml`, `styles.xml`, `numbering.xml`, headers,
footers, relationships). Treat structure as primary; rendered text is a view.
## Decide the path first
Pick **one** path up front. The right path depends only on what is on disk
before you start.
| You have | Goal | Path |
|---|---|---|
| Existing `.docx` | Read text/structure | A. Inspect |
| Existing `.docx` | Modify content while keeping styles | B. Edit-in-place |
| Nothing or a brief | Build a new doc | C. Create from scratch |
If the user hands you a doc and asks for changes, default to path B and treat
the input as the visual style baseline. Only choose path C when the user says
"start fresh" or there is no input.
---
## Path A: Inspect
Dump structure as JSON for inspection without mutating anything.
```bash
python {baseDir}/scripts/inspect_docx.py /path/to/doc.docx
```
Output schema:
```json
{
"paragraphs": [{"index": 0, "text": "...", "style": "Heading 1"}, ...],
"tables": [[["row0,col0", "row0,col1"], ...], ...],
"sections": 1,
"has_tracked_changes": false
}
```
Use this whenever you need to see what is in the doc before deciding how to
edit. The output is stable and machine-readable — diff two inspect outputs to
verify a round-trip preserved everything you intended.
---
## Path B: Edit in place
Two sub-strategies; pick by how invasive the edit is.
### B1. Run-level text replacement (preferred)
When the change is "swap this string" or "fill these placeholders": mutate
runs in place. This preserves all theme/style/font settings.
```bash
python {baseDir}/scripts/edit_docx.py input.docx ops.json --out output.docx
```
`ops.json` is a list of operations:
```json
[
{"op": "replace_run", "para": 0, "run": 0, "text": "Q3 Review"},
{"op": "replace_text", "find": "{{CLIENT}}", "with": "Acme Corp"}
]
```
Edit at the **run** level, not the paragraph level — replacing whole paragraph
text drops formatting. If a placeholder spans multiple runs (often happens
when the original template applied bold/italic mid-word), the helper script
collapses runs into the first one and clears the rest.
### B2. Structural edits (sections / page layout / numbering)
python-docx exposes paragraphs, tables, and runs but has limited support for
page layout, numbering definitions, and tracked changes. For those, unzip the
`.docx`, patch `word/document.xml` and adjacent parts, and repack:
```bash
mkdir _unpacked && (cd _unpacked && unzip -q ../input.docx)
# edit _unpacked/word/document.xml
(cd _unpacked && zip -q -r ../output.docx . -x "*.DS_Store")
```
Rules when patching XML:
- Use `defusedxml.ElementTree` or `lxml`, not stdlib `xml.etree.ElementTree`.
ET drops or rewrites namespace prefixes (`w:`, `r:`) in ways Word refuses to
load.
- Preserve `xml:space="preserve"` on `<w:t>` elements that hold leading or
trailing whitespace.
- `[Content_Types].xml` must list every part type. Removing a header without
also removing its override entry yields a "repair" prompt in Word.
- Numbering definitions live in `numbering.xml`; bullet/number changes must
patch the numbering ID, not just the visible text.
When done, validate by opening in LibreOffice headless before declaring
success — silent failures are common.
---
## Path C: Create from scratch
```bash
python {baseDir}/scripts/create_docx.py spec.json --out out.docx
```
`spec.json` describes content declaratively:
```json
{
"metadata": {"title": "Q3 Review", "author": "Wei E."},
"body": [
{"kind": "heading", "level": 1, "text": "Q3 Review"},
{"kind": "paragraph", "text": "Revenue +18% YoY."},
{"kind": "table", "rows": [["Metric", "Value"], ["Revenue", "$2.1M"]]}
]
}
```
For programmatic use call python-docx directly:
```python
from docx import Document
doc = Document()
doc.add_heading("Q3 Review", level=1)
doc.add_paragraph("Revenue +18% YoY.")
table = doc.add_table(rows=2, cols=2)
table.rows[0].cells[0].text = "Metric"
doc.save("out.docx")
```
See [references/python_docx.md](references/python_docx.md) for paragraphs,
styles, numbering, tables, headers/footers, and section breaks.
---
## Tracked changes
Tracked changes are stored in `word/document.xml` as `<w:ins>` and `<w:del>`
elements. python-docx does not expose them as first-class objects — the
inspect helper sets `has_tracked_changes: true` when any `w:ins` or `w:del`
element is found, and you must resolve them by patching XML directly. Treat
docs with tracked changes as read-only until reviewers accept or reject the
revisions.
---
## Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Word reports "needs repair" | Removed a header part but left override in `[Content_Types].xml` | Strip the override entry too |
| Text replacement drops bold/italic | Replaced `paragraph.text` instead of editing runs | Use `op: replace_run` |
| Numbering restarts unexpectedly | Edited a list item across two `abstractNum` definitions | Patch `numbering.xml`; rebuild numbering IDs |
| Smart-quote characters render as garbage | XML read with stdlib ET dropped namespaces | Switch to `defusedxml` or `lxml` |
| Long string overflows | Cell width is fixed in the template | Either shorten or compute auto-fit before save |
---
## Boundaries
- This skill is for `.docx` (OOXML WordprocessingML). It does **not** handle
`.doc` (legacy binary) or Google Docs. Convert via LibreOffice or Word
export first.
- Do not run macro-enabled `.docm` / VBA. The runtime sandbox does not
execute embedded code, and security scanners flag mixed content.
- For PDF generation from a `.docx`, hand off to LibreOffice headless or a
separate PDF skill. This skill stops at `.docx`.