| name | docx |
| version | 1.1.0 |
| description | Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). For CREATING new documents (report, memo, letter, résumé, contract from scratch), call the built-in tool `renderDocx` instead — it renders Markdown to .docx in milliseconds without forking Node.js. This skill remains authoritative for EDITING existing .docx (unpack/edit XML/pack), tracked changes, comments, image manipulation, find-and-replace, and conversions. Triggers include: "Word doc", "word document", ".docx", or requests to edit/extract/restructure existing Word files. Do NOT use for PDFs, spreadsheets, or general coding tasks unrelated to document generation. |
| dependencies | {"commands":["python3","node"],"tools":["skillScriptTool","skillFileTool"]} |
| platforms | ["macos","linux","windows"] |
Important: All scripts/ paths are relative to this skill directory.
Use run_skill_script tool to execute scripts, or run with: cd {this_skill_dir} && python scripts/...
DOCX creation, editing, and analysis
Quick Start — Pick the Right Tool
| Task | Recommended Tool |
|---|
| Create a new document (report / résumé / contract / memo) | renderDocx() — millisecond render, no subprocess |
| Edit an existing .docx (content / formatting) | unpack → edit XML → pack workflow below |
| Add tracked changes / comments | unpack → edit XML → pack workflow below |
| GB/T 9704 official document | writeGongwen() (BmacClaw only) |
Create a new document (recommended path)
Call the in-process Java tool — no Node.js install, no fork, no disk round-trip:
renderDocx(
markdown="# Title\n\nBody paragraph...",
filename="monthly-report",
pageSize="A4"
)
Returns a clickable link of the form
[monthly-report.docx](/api/v1/files/generated/<uuid>) valid for 10 minutes.
The user clicks it to download — no follow-up Agent step needed.
renderDocx supports headings (# ## ###), bold (**text**), bullet
lists (- item), numbered lists (1. item), pipe-style tables, and plain
paragraphs. For images, headers/footers, or precise OOXML control, fall back
to the docx-js workflow below.
Prerequisites
- python-docx (
pip install python-docx): direct structure reading and light editing (paragraphs, styles, tables)
- docx (
npm install -g docx): new document creation
- LibreOffice (
soffice): .doc -> .docx conversion, tracked-changes acceptance, and PDF export
- pandoc: text extraction
- pdftoppm (poppler-utils): document-to-image workflows
- If
pdftoppm is unavailable, a Python fallback path may use pdf2image.
- On Windows, dependencies must be installed and available in
PATH; if missing, report the dependency issue and stop (do not keep retrying).
Overview
A .docx file is a ZIP archive containing XML files.
Quick Reference
| Task | Approach |
|---|
| Read/analyze content | pandoc or unpack for raw XML |
| Create new document | Use docx-js - see Creating New Documents below |
| Edit existing document | Unpack → edit XML → repack - see Editing Existing Documents below |
Converting .doc to .docx
Legacy .doc files must be converted before editing:
python scripts/office/soffice.py --headless --convert-to docx document.doc
Reading Content
Option A: python-docx (recommended for structured access)
Install: pip install python-docx. Gives direct access to paragraphs, styles, tables, and metadata without unpacking ZIP.
from docx import Document
doc = Document("document.docx")
for para in doc.paragraphs:
print(f"[{para.style.name}] {para.text}")
for i, table in enumerate(doc.tables):
print(f"Table {i+1}:")
for row in table.rows:
print([cell.text for cell in row.cells])
for para in doc.paragraphs:
for run in para.runs:
print(f" run: bold={run.bold} italic={run.italic} text={run.text!r}")
Use python-docx when you need to read or lightly modify content. Fall back to the unpack/XML workflow for complex structural changes.
Option B: pandoc (plain text extraction)
pandoc --track-changes=all document.docx -o output.md
python scripts/office/unpack.py document.docx unpacked/
Converting to Images
python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page
Accepting Tracked Changes
To produce a clean document with all tracked changes accepted (requires LibreOffice):
python scripts/accept_changes.py input.docx output.docx
Creating New Documents
Generate .docx files with JavaScript, then validate. Install: npm install -g docx
Setup
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
VerticalAlign, PageNumber, PageBreak } = require('docx');
const doc = new Document({ sections: [{ children: [] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));
Validation
After creating the file, validate it. If validation fails, unpack, fix the XML, and repack.
python scripts/office/validate.py doc.docx
Page Size
sections: [{
properties: {
page: {
size: {
width: 12240,
height: 15840
},
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
}
},
children: []
}]
Common page sizes (DXA units, 1440 DXA = 1 inch):
| Paper | Width | Height | Content Width (1" margins) |
|---|
| US Letter | 12,240 | 15,840 | 9,360 |
| A4 (default) | 11,906 | 16,838 | 9,026 |
Landscape orientation: docx-js swaps width/height internally, so pass portrait dimensions and let it handle the swap:
size: {
width: 12240,
height: 15840,
orientation: PageOrientation.LANDSCAPE
},
Styles (Override Built-in Headings)
Use Arial as the default font (universally supported). Keep titles black for readability.
const doc = new Document({
styles: {
default: { document: { run: { font: "Arial", size: 24 } } },
paragraphStyles: [
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 32, bold: true, font: "Arial" },
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } },
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, font: "Arial" },
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
]
},
sections: [{
children: [
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }),
]
}]
});
Lists (NEVER use unicode bullets)
const doc = new Document({
numbering: {
config: [
{ reference: "bullets",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "numbers",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
]
},
sections: [{
children: [
new Paragraph({ numbering: { reference: "bullets", level: 0 },
children: [new TextRun("Bullet item")] }),
new Paragraph({ numbering: { reference: "numbers", level: 0 },
children: [new TextRun("Numbered item")] }),
]
}]
});
Tables
CRITICAL: Tables need dual widths - set both columnWidths on the table AND width on each cell.
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };
new Table({
width: { size: 9360, type: WidthType.DXA },
columnWidths: [4680, 4680],
rows: [
new TableRow({
children: [
new TableCell({
borders,
width: { size: 4680, type: WidthType.DXA },
shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
margins: { top: 80, bottom: 80, left: 120, right: 120 },
children: [new Paragraph({ children: [new TextRun("Cell")] })]
})
]
})
]
})
Width rules:
- Always use
WidthType.DXA - never WidthType.PERCENTAGE
- Table width must equal the sum of
columnWidths
- Cell
width must match corresponding columnWidth
Images
new Paragraph({
children: [new ImageRun({
type: "png",
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150 },
altText: { title: "Title", description: "Desc", name: "Name" }
})]
})
Page Breaks
new Paragraph({ children: [new PageBreak()] })
Table of Contents
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
Headers/Footers
sections: [{
properties: {
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } }
},
headers: {
default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] })
},
footers: {
default: new Footer({ children: [new Paragraph({
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
})] })
},
children: []
}]
Critical Rules for docx-js
- Set page size explicitly - docx-js defaults to A4
- Landscape: pass portrait dimensions - docx-js swaps width/height internally
- Never use
\n - use separate Paragraph elements
- Never use unicode bullets - use
LevelFormat.BULLET with numbering config
- PageBreak must be in Paragraph
- ImageRun requires
type
- Always set table
width with DXA - never use WidthType.PERCENTAGE
- Tables need dual widths -
columnWidths array AND cell width
- Use
ShadingType.CLEAR - never SOLID for table shading
- TOC requires HeadingLevel only
- Override built-in styles - use exact IDs: "Heading1", "Heading2", etc.
- Include
outlineLevel - required for TOC (0 for H1, 1 for H2, etc.)
Editing Existing Documents
Follow all 3 steps in order.
Step 1: Unpack
python scripts/office/unpack.py document.docx unpacked/
Extracts XML, pretty-prints, merges adjacent runs, and converts smart quotes to XML entities. Use --merge-runs false to skip run merging.
Step 2: Edit XML
Edit files in unpacked/word/. See XML Reference below for patterns.
Use "MateClaw" as the author for tracked changes and comments, unless the user explicitly requests a different name.
CRITICAL: Use smart quotes for new content:
<w:t>Here’s a quote: “Hello”</w:t>
| Entity | Character |
|---|
‘ | ' (left single) |
’ | ' (right single / apostrophe) |
“ | " (left double) |
” | " (right double) |
Adding comments: Use comment.py to handle boilerplate:
python scripts/comment.py unpacked/ 0 "Comment text with & and ’"
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author"
Then add markers to document.xml (see Comments in XML Reference).
Step 3: Pack
python scripts/office/pack.py unpacked/ output.docx --original document.docx
Validates with auto-repair, condenses XML, and creates DOCX. Use --validate false to skip.
Auto-repair will fix:
durableId >= 0x7FFFFFFF (regenerates valid ID)
- Missing
xml:space="preserve" on <w:t> with whitespace
Common Pitfalls
- Replace entire
<w:r> elements: When adding tracked changes, replace the whole <w:r>...</w:r> block.
- Preserve
<w:rPr> formatting: Copy the original run's <w:rPr> block into your tracked change runs.
XML Reference
Schema Compliance
- Element order in
<w:pPr>: <w:pStyle>, <w:numPr>, <w:spacing>, <w:ind>, <w:jc>, <w:rPr> last
- Whitespace: Add
xml:space="preserve" to <w:t> with leading/trailing spaces
- RSIDs: Must be 8-digit hex (e.g.,
00AB1234)
Tracked Changes
Insertion:
<w:ins w:id="1" w:author="MateClaw" w:date="2025-01-01T00:00:00Z">
<w:r><w:t>inserted text</w:t></w:r>
</w:ins>
Deletion:
<w:del w:id="2" w:author="MateClaw" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
Inside <w:del>: Use <w:delText> instead of <w:t>, and <w:delInstrText> instead of <w:instrText>.
Minimal edits - only mark what changes:
<w:r><w:t>The term is </w:t></w:r>
<w:del w:id="1" w:author="MateClaw" w:date="...">
<w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="MateClaw" w:date="...">
<w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> days.</w:t></w:r>
Deleting entire paragraphs - mark the paragraph mark as deleted:
<w:p>
<w:pPr>
<w:rPr>
<w:del w:id="1" w:author="MateClaw" w:date="2025-01-01T00:00:00Z"/>
</w:rPr>
</w:pPr>
<w:del w:id="2" w:author="MateClaw" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r>
</w:del>
</w:p>
Rejecting another author's insertion:
<w:ins w:author="Jane" w:id="5">
<w:del w:author="MateClaw" w:id="10">
<w:r><w:delText>their inserted text</w:delText></w:r>
</w:del>
</w:ins>
Restoring another author's deletion:
<w:del w:author="Jane" w:id="5">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
<w:ins w:author="MateClaw" w:id="10">
<w:r><w:t>deleted text</w:t></w:r>
</w:ins>
Comments
After running comment.py, add markers to document.xml:
CRITICAL: <w:commentRangeStart> and <w:commentRangeEnd> are siblings of <w:r>, never inside <w:r>.
<w:commentRangeStart w:id="0"/>
<w:r><w:t>commented text</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
Images
- Add image file to
word/media/
- Add relationship to
word/_rels/document.xml.rels:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
- Add content type to
[Content_Types].xml:
<Default Extension="png" ContentType="image/png"/>
- Reference in document.xml:
<w:drawing>
<wp:inline>
<wp:extent cx="914400" cy="914400"/>
<a:graphic>
<a:graphicData uri=".../picture">
<pic:pic>
<pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>