# Docx Xml

> Advanced — structural OOXML edits via run_code + lxml + zipfile. Use ONLY when the docx-editing tools (edit_document, add_comment, accept/reject, revert_edit) can't express the change. For ordinary text edits, use the docx-editing skill instead.

- **Type:** Skill
- **Install:** `agentstack add skill-anylegal-ai-anylegal-oss-docx-xml`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [anylegal-ai](https://agentstack.voostack.com/s/anylegal-ai)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [anylegal-ai](https://github.com/anylegal-ai)
- **Source:** https://github.com/anylegal-ai/anylegal-oss/tree/main/backend/anylegal_oss/workspace/skills/docx-xml

## Install

```sh
agentstack add skill-anylegal-ai-anylegal-oss-docx-xml
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# DOCX XML — Advanced Skill

You are looking at this skill because a structural edit needs raw OOXML manipulation. **Always check first whether `edit_document` can do it** — see the docx-editing skill. Only drop to `run_code` for:

- Deleting a clause **including its paragraph mark** (needs `` pattern)
- Inserting multi-paragraph content mid-document
- Editing a part other than `word/document.xml` (styles.xml, headers/footers, numbering.xml)
- Bulk structural rewrites spanning dozens of paragraphs

## Overview

A `.docx` file is a ZIP archive containing XML files. The main content is in `word/document.xml`. The OOXML namespace for Word is `http://schemas.openxmlformats.org/wordprocessingml/2006/main` (prefix `w:`).

**Use `language="python"` + `lxml` + `zipfile` on the XML parts directly.**

**Why NOT docx-js:** docx-js is write-only — there is no `Document.load(...)` API. It's for creating new DOCX files from scratch (the `draft` skill uses it). You cannot use it to load `Contract.docx`, mutate, and save back.

**Why NOT python-docx for run-level mutation:** python-docx silently corrupts complex legal templates. It loses non-target glyphs (other placeholders, special characters) when run.text is reassigned, has no reliable API for footnotes / bookmarks / internal hyperlinks / TableOfContents, and collapses `` run properties when rewriting text — losing bold / font / size on surrounding runs. **Never use python-docx for `run.text.replace(...)` or any run-level mutation.** python-docx for *reading* and *additive* operations (adding images, adding paragraphs) is acceptable.

## The right pattern — read XML, mutate with lxml, repack with zipfile

```python
# run_code(language="python", input_files=["Contract.docx"], code=...)
import zipfile, io
from lxml import etree

NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
W = f"{{{NS}}}"

# 1. Read document.xml from the input DOCX.
with zipfile.ZipFile('/sandbox/input/Contract.docx') as zin:
    doc_xml = zin.read('word/document.xml')
    parts = {name: zin.read(name) for name in zin.namelist()}

# 2. Mutate with lxml (example — delete a specific paragraph including its mark).
root = etree.fromstring(doc_xml)
for p in root.iter(f"{W}p"):
    text = "".join(t.text or "" for t in p.iter(f"{W}t"))
    if "text-to-delete" in text:
        p.getparent().remove(p)

# 3. Repack, preserving every other part.
parts['word/document.xml'] = etree.tostring(root, xml_declaration=True,
                                            encoding='UTF-8', standalone=True)
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
    for name, data in parts.items():
        zout.writestr(name, data)

with open('/sandbox/output/Contract_Edited.docx', 'wb') as f:
    f.write(buf.getvalue())
```

**One document = one `run_code` call.** A single call should open the file, make all changes, and save one output. Never split a document edit across multiple scripts.

## OOXML XML Reference

### Document Structure

```
word/document.xml
  
                        ← paragraph
                      ← paragraph properties (style, numbering, spacing)
        
        ...
        ...
        ...
        ...
        ...  ← always last child of pPr
      
                        ← run (contiguous text with same formatting)
                      ← run properties (bold, italic, font, size)
                       ← bold
                       ← italic
           ← font size in half-points (24 = 12pt)
          
        
        text content
      
    
```

### Schema-compliance rules

| Rule | What goes wrong if ignored |
|---|---|
| `` with leading or trailing whitespace must carry `xml:space="preserve"` | Word strips the spaces during round-trip |
| Inside ``, deleted text content lives in ``, not `` | Tracked changes corrupt; Accept All leaves stale text |
| `` is always the last child of `` | Schema violation; some viewers reject the document |
| When wrapping a run in tracked-change markup, copy the original `` into your `` / `` runs | The change preserves bold / font / size of surrounding text |
| When creating a tracked change, replace the entire `` element rather than injecting markup inside it | Avoids invalid run nesting and preserves Word's display logic |
| Every `w:id` in tracked changes is unique | Word silently merges or drops changes with duplicate IDs |
| `xml:space="preserve"` is the most-missed of these rules — most run-level edits need it | Spaces disappearing is invisible in source diff but obvious to the user |

### Smart quotes (typography)

DOCX documents use smart quotes stored as Unicode. When adding text via lxml, use these:

| Code | Char | Description |
|------|------|-------------|
| `‘` | ' | Left single quote |
| `’` | ' | Right single quote / apostrophe |
| `“` | " | Left double quote |
| `”` | " | Right double quote |

In lxml: `elem.text = 'Here’s a quote: “Hello”'`

### Tracked changes XML patterns

**Insertion:**
```xml

  ...inserted text

```

**Deletion:**
```xml

  ...deleted text

```

**Minimal edit — only mark what changes** (governing law change from Delaware to England and Wales):
```xml
This Agreement is governed by the laws of 

  the State of Delaware

  England and Wales

.
```

**Deleting an entire paragraph** — mark the paragraph mark as deleted too, otherwise "Accept All" leaves an empty line:
```xml

  
    
      
    
  
  
    The deleted clause text...
  

```

**Rejecting a counterparty's insertion** — nest your deletion inside their insertion (preserves their authorship in the audit trail):
```xml

  
    their inserted text
  

```

**Restoring a counterparty's deletion** — add an insertion after their deletion (do not modify their deletion):
```xml

  deleted text

  deleted text

```

### Images (additive — python-docx is acceptable here)

Inserting images is an additive operation; python-docx handles relationships and content types correctly:

```python
from docx import Document
from docx.shared import Inches, Cm

doc = Document('/sandbox/input/contract.docx')
doc.add_picture('/sandbox/input/logo.png', width=Inches(2.0))

# Or in a specific paragraph:
para = doc.paragraphs[0]
run = para.add_run()
run.add_picture('/sandbox/input/signature.png', width=Cm(5), height=Cm(2))

doc.save('/sandbox/output/contract_with_images.docx')
```

This is the **only** place python-docx is the right tool. For text mutation, always use lxml on the raw XML.

If you need raw XML control (custom positioning, wrapping):

```xml

  
      
    
    
      
        
          
            
          
          
            
          
        
      
    
  

```

To add via raw XML you must also (1) copy the image to `word/media/`, (2) add a relationship in `word/_rels/document.xml.rels`, and (3) register the content type in `[Content_Types].xml`.

### Common pitfalls

- **Modifying during iteration**: when removing elements with lxml `findall()`, iterate over a copy: `for elem in list(body.findall(...))`.
- **Namespace-qualified attribute access**: use the full URI — `elem.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}id')`, not `elem.get('w:id')`.
- **Missing paragraph mark deletion**: if you delete all text from a paragraph but don't mark the paragraph mark as deleted, "Accept All" leaves blank lines.
- **Stale element references**: after removing/moving elements, cached references may be invalid. Re-query after structural changes.
- **ID uniqueness**: every `w:id` in tracked changes must be unique. Use `max(existing_ids) + 1`.

## Validation

When `run_code` produces a DOCX file, validate it before reporting success. The most common validation failures and their fixes:

| Failure | Fix |
|---|---|
| `` inside `` | Use `` for deleted text, not `` |
| Missing `xml:space="preserve"` | Add the attribute to `` elements that have leading/trailing whitespace |
| Corrupt ZIP | Ensure `zipfile.ZipFile(..., 'w')` is properly closed before reading the bytes back |
| Schema violation in `` | Verify element order: ``, ``, ``, ``, ``, `` (rPr last) |

If the script produces a corrupt DOCX, fix the Python code and retry rather than retrying the same code.

## Data + spreadsheet recipes (when run_code is the right tool for non-DOCX work)

### Generate an Excel fee schedule

```python
import openpyxl
from openpyxl.styles import Font, PatternFill

wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Fee Schedule"

headers = ['Service', 'Rate (USD/hr)', 'Estimated Hours', 'Total']
for col, header in enumerate(headers, 1):
    cell = ws.cell(row=1, column=col, value=header)
    cell.font = Font(bold=True, color="FFFFFF")
    cell.fill = PatternFill(start_color="4472C4", fill_type="solid")

services = [
    ('Contract Review', 350, 8),
    ('Negotiation Support', 400, 12),
    ('Due Diligence', 375, 20),
]
for row, (service, rate, hours) in enumerate(services, 2):
    ws.cell(row=row, column=1, value=service)
    ws.cell(row=row, column=2, value=rate)
    ws.cell(row=row, column=3, value=hours)
    ws.cell(row=row, column=4, value=rate * hours)

ws.column_dimensions['A'].width = 25
wb.save('/sandbox/output/fee_schedule.xlsx')
```

### Calculate business day deadlines

```python
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

def add_business_days(start, days):
    current = start
    added = 0
    while added < days:
        current += timedelta(days=1)
        if current.weekday() < 5:
            added += 1
    return current

signing_date = datetime(2026, 5, 4)
deadlines = {
    "Signing Date": signing_date.strftime("%Y-%m-%d"),
    "Effective Date (T+5 business days)": add_business_days(signing_date, 5).strftime("%Y-%m-%d"),
    "First Payment Due (30 days)": (signing_date + timedelta(days=30)).strftime("%Y-%m-%d"),
    "Warranty Period Ends (12 months)": (signing_date + relativedelta(months=12)).strftime("%Y-%m-%d"),
}
for name, date in deadlines.items():
    print(f"{name}: {date}")
```

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [anylegal-ai](https://github.com/anylegal-ai)
- **Source:** [anylegal-ai/anylegal-oss](https://github.com/anylegal-ai/anylegal-oss)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-anylegal-ai-anylegal-oss-docx-xml
- Seller: https://agentstack.voostack.com/s/anylegal-ai
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
