
Docx Template Filling
- 16 installs
- 47 repo stars
- Updated August 4, 2026
- belumume/claude-skills
docx-template-filling is a skill that fills DOCX template forms programmatically while preserving 100% of the original structure, so the output is indistinguishable from manual filling.
About
This skill fills DOCX template forms programmatically while preserving the original document structure exactly. A developer uses it to complete applications, questionnaires, and standardized forms by modifying cells in place and inserting content at anchor points via the XML API. It matters because naive approaches (pandoc, appending, recreating tables) destroy logos, styles, and metadata and leave detectable artifacts.
- Fills DOCX template forms while preserving 100% of original structure (logos, footers, styles, metadata)
- Uses anchor-based XML-API insertion so output is indistinguishable from manual filling with zero artifacts
- Documents anti-patterns to avoid: pandoc --reference-doc, appending at the end, and recreating tables
Docx Template Filling by the numbers
- 16 all-time installs (skills.sh)
- Ranked #452 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
docx-template-filling capabilities & compatibility
- Capabilities
- docx template filling · form filling · anchor insertion
- Use cases
- documentation
What docx-template-filling says it does
Fill template forms programmatically with **zero detectable artifacts**.
Modify existing cells. Never remove and recreate.
npx skills add https://github.com/belumume/claude-skills --skill docx-template-fillingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 47 |
| Last updated | August 4, 2026 |
| Repository | belumume/claude-skills ↗ |
What it does
Fill DOCX template forms in place via anchor-based XML insertion, preserving logos, styles, and metadata with zero artifacts.
Who is it for?
Filling standardized forms, application forms, and questionnaires where template integrity must be 100% preserved.
When should I use this skill?
Filling standardized forms, completing application forms, responding to questionnaires, or any case where the recipient must not detect programmatic manipulation.
What you get
Template forms are filled in place with zero detectable artifacts, preserving all original structure.
- filled DOCX template with preserved structure
Files
DOCX Template Filling - Forensic Preservation
Fill template forms programmatically with zero detectable artifacts. The filled document must be indistinguishable from manual typing in the original template.
When to Use This Skill
Invoke when:
- Filling standardized forms and templates
- Completing application forms
- Responding to questionnaires and surveys
- Processing template-based documents
- Any scenario where the recipient must not detect programmatic manipulation
Critical requirement: Template integrity must be 100% preserved (logos, footers, headers, styles, metadata, element structure).
Core Philosophy: Preservation Over Recreation
WRONG approach: Extract content from template, generate new document
- Loses metadata
- Changes element IDs
- Alters styles subtly
- Creates detectable artifacts
RIGHT approach: Load template, insert content at anchor points using XML API
- Preserves all original elements
- Maintains metadata
- Zero structural changes
- Indistinguishable from manual entry
Critical Anti-Patterns
❌ NEVER: Use pandoc with --reference-doc
# This SEEMS correct but ONLY copies styles, NOT structure
pandoc content.md -o output.docx --reference-doc=template.docxWhat happens:
- Template's tables disappear
- Logos, headers, footers lost
- Only style definitions copied
- Looks completely different
Why it fails: --reference-doc means "copy the style definitions," NOT "preserve the document structure"
❌ NEVER: Append content at the end
# This destroys template structure
template = Document('template.docx')
# Remove content after markers
# ... (deletion logic)
# Append all new content at end
for para in new_content:
template.add_paragraph(para.text) # WRONG!What happens:
- Template questions appear unanswered
- All answers grouped at end
- Structure broken
- Obviously programmatic
❌ NEVER: Recreate tables
# DON'T copy table structure and rebuild
new_table = template.add_table(rows=3, cols=2)
# Even if copying all properties, it's not the original!What happens:
- Loses original element IDs
- Style inheritance breaks
- Metadata changes
- Detectable as modified
Essential Workflow
Step 1: Inspect Template Structure FIRST
Always inspect before modifying. Never assume structure.
Use the provided inspection script:
python scripts/inspect_template.py template.docxThis prints:
- All tables with identities
- Potential anchor points (paragraphs ending with ":", "Answer:", etc.)
- Headers and footers
- Document element counts
Why critical: Prevents modifying wrong tables, missing anchors, breaking structure.
Step 2: Selective Table Filling
Modify cells in place. Never recreate tables.
from docx import Document
template = Document('template.docx')
# Fill specific cells in existing table
info_table = template.tables[0]
info_table.rows[0].cells[1].text = "Jane Smith"
info_table.rows[1].cells[1].text = "S12345"
# Table structure, styles, borders all preservedPrinciple: Modify existing cells. Never remove and recreate.
Step 3: Anchor-Based Content Insertion
Insert content at specific positions using XML API.
# Find anchor paragraphs
anchor_positions = []
for i, para in enumerate(template.paragraphs):
if para.text.strip() == "Answer:":
anchor_positions.append(i)
# Insert content after anchor using XML API
def insert_after(doc, anchor_idx, content_paras):
anchor_elem = doc.paragraphs[anchor_idx]._element
parent = anchor_elem.getparent()
for offset, para in enumerate(content_paras):
parent.insert(
parent.index(anchor_elem) + 1 + offset,
para._element
)
# Load content to insert
content_doc = Document('my_content.docx')
section_paragraphs = content_doc.paragraphs[5:64]
# Insert at anchor
insert_after(template, anchor_positions[0], section_paragraphs)
# Save
template.save('completed.docx')Why XML API:
doc.add_paragraph()appends at end → wrong positionpara.insert_paragraph_before()has stale reference issues- XML API: direct element manipulation → correct position, zero artifacts
Step 4: Multi-Anchor Insertion (Reverse Order)
When inserting at multiple positions, insert from bottom to top to preserve earlier indices.
# Template has anchors at paragraphs 18, 27, 37
# Insert in REVERSE order
insert_after(template, 37, section3_content) # Last anchor first
insert_after(template, 27, section2_content) # Middle still at 27
insert_after(template, 18, section1_content) # First still at 18Why reverse: Inserting content shifts later paragraph indices but not earlier ones.
Advanced Patterns
For detailed implementations, see references/patterns.md:
- Content range extraction - Extract multi-section content between markers
- Table identity detection - Identify tables when no IDs exist
- Robust anchor matching - exact/partial/smart modes
- Table repositioning - Move tables without recreating
- Verification - Ensure zero artifacts after filling
Common Scenarios
Scenario 1: Form with Info Table + Q&A
template = Document('form_template.docx')
# Fill info table
info_table = template.tables[0]
info_table.rows[0].cells[1].text = "Applicant Name"
# Find "Answer:" anchors
anchors = [i for i, p in enumerate(template.paragraphs)
if p.text.strip() == "Answer:"]
# Insert responses
responses = Document('my_responses.docx')
response_content = responses.paragraphs[5:30]
insert_after(template, anchors[0], response_content)
template.save('form_completed.docx')Scenario 2: Report with Table Repositioning
template = Document('report_template.docx')
# Fill team table
team_table = template.tables[0]
team_table.rows[0].cells[1].text = "Team 5"
# Insert section content at anchors
# ... (insertion code)
# Move summary table to correct position
summary_heading_idx = next(i for i, p in enumerate(template.paragraphs)
if "Summary Table:" in p.text)
# Move table from end to after summary heading
# See references/patterns.md for move_table_to_position()
template.save('report_completed.docx')Bundled Resources
Scripts
- `scripts/inspect_template.py` - Inspect template structure before modification
- Usage:
python scripts/inspect_template.py <template.docx> - Prevents destructive mistakes by showing all tables, anchors, headers/footers
References
- `references/patterns.md` - Detailed technical patterns
- Content range extraction
- Table identity detection strategies
- XML-level insertion patterns
- Multi-anchor workflows
- Verification procedures
- Complete code examples
Load patterns.md when implementing specific operations beyond basic workflow.
Verification Checklist
Template filling is successful if:
- [ ] Filled document indistinguishable from manual entry
- [ ] All template tables preserved (count unchanged unless expected)
- [ ] Headers/footers unchanged
- [ ] Logo(s) intact
- [ ] Scoring/grading tables empty (if they should be)
- [ ] Styles identical to original
- [ ] Content inserted at correct anchor points (not at end)
- [ ] Template owner cannot detect programmatic manipulation
Key Lessons
This skill documents patterns where:
- Templates have info tables (to fill) and evaluation/scoring tables (preserve empty)
- Multiple anchor points like "Answer:", "Response:", or "Solution:" for content insertion
- Tables may need repositioning to correct sections
- Document structure must remain intact (headers, footers, logos, branding)
- Zero artifacts requirement (recipient cannot detect automation)
Use cases: Forms, questionnaires, standardized documents, applications, reports.
Core principle: Preservation over recreation. Never rebuild - always modify in place.
DOCX Template Filling - Detailed Patterns Reference
This reference contains detailed technical patterns for template filling. Load when implementing specific operations.
Content Range Extraction Pattern
Extract multi-paragraph sections between marker paragraphs.
def extract_content_ranges(doc, markers):
"""
Extract content sections between marker paragraphs.
Args:
doc: Document object
markers: List of marker texts (e.g., ["Task 1:", "Task 2:", "Task 3:"])
Returns:
Dict of marker -> (start_idx, end_idx, paragraphs)
Example:
ranges = extract_content_ranges(doc, ["Task 1:", "Task 2:", "References"])
# Returns: {
# "Task 1:": (5, 64, [para objects]),
# "Task 2:": (65, 107, [para objects]),
# "References": (108, 199, [para objects])
# }
"""
# Find all markers
marker_positions = []
for i, para in enumerate(doc.paragraphs):
for marker in markers:
if marker in para.text:
marker_positions.append((marker, i))
# Sort by position
marker_positions.sort(key=lambda x: x[1])
# Extract ranges between markers
ranges = {}
for i, (marker, start_idx) in enumerate(marker_positions):
# Content starts AFTER the marker paragraph
content_start = start_idx + 1
# Content ends at next marker (or end of document)
if i + 1 < len(marker_positions):
content_end = marker_positions[i + 1][1]
else:
content_end = len(doc.paragraphs)
# Extract paragraph range
content_paras = doc.paragraphs[content_start:content_end]
ranges[marker] = {
'start_idx': content_start,
'end_idx': content_end,
'paragraphs': content_paras,
'count': len(content_paras)
}
return rangesTable Identity Detection Strategies
python-docx doesn't assign table IDs. Identify tables by examining content.
Strategy 1: Check First Cell Text
def identify_tables(doc):
"""
Identify tables by examining their content.
Returns dict of table_type -> table_index
"""
table_identities = {}
for i, table in enumerate(doc.tables):
# Strategy 1: Check first cell text
first_cell = table.rows[0].cells[0].text.strip()
if "ID" in first_cell or "Name" in first_cell:
table_identities['info'] = i
elif "Grading" in first_cell or "Points" in first_cell or "Score" in first_cell:
table_identities['scoring'] = i
# Strategy 2: Check dimensions
elif len(table.rows) == 9 and len(table.columns) == 4:
# Likely the comparison table
table_identities['comparison'] = i
# Strategy 3: Check header row content
else:
first_row_text = " ".join([c.text for c in table.rows[0].cells])
if "Metric" in first_row_text and "Original" in first_row_text:
table_identities['comparison'] = i
elif all(keyword in first_row_text.lower()
for keyword in ['name', 'id']):
table_identities['info'] = i
return table_identitiesUsage
doc = Document('template.docx')
tables = identify_tables(doc)
# Now safely access tables by type
if 'info' in tables:
info_table = doc.tables[tables['info']]
info_table.rows[0].cells[1].text = "Jane Smith"
if 'scoring' in tables:
# Leave scoring table untouched
print("Preserving scoring table")
if 'comparison' in tables:
comparison_table = doc.tables[tables['comparison']]
# Move or modify as neededRobust Anchor Matching Modes
Trade-off between precision and robustness.
def find_anchors(doc, anchor_text, mode='exact'):
"""
Find anchor paragraphs with configurable matching.
Args:
doc: Document object
anchor_text: Text to search for
mode: 'exact' (fragile to spacing) or 'partial' (more robust)
Returns:
List of paragraph indices
"""
anchors = []
for i, para in enumerate(doc.paragraphs):
text = para.text.strip()
if mode == 'exact':
# Exact match - fragile to whitespace changes
if text == anchor_text:
anchors.append(i)
elif mode == 'partial':
# Partial match - more robust
if anchor_text in text:
anchors.append(i)
elif mode == 'smart':
# Smart match - case-insensitive, whitespace-tolerant
normalized_text = ' '.join(text.lower().split())
normalized_anchor = ' '.join(anchor_text.lower().split())
if normalized_anchor == normalized_text:
anchors.append(i)
return anchorsGuidelines:
- Use
exactwhen anchor is highly specific (e.g., "Answer:" with no other text) - Use
partialwhen anchor might have prefix/suffix (e.g., "Summary Table: Comparison") - Use
smartfor maximum robustness
XML-Level Paragraph Insertion
Insert paragraphs at specific positions without stale reference issues.
def insert_paragraphs_after_anchor(doc, anchor_text, content_paragraphs):
"""
Insert content immediately after anchor paragraph using XML API.
This is forensically clean - inserted paragraphs become part of
the original document structure without artifacts.
Args:
doc: Document object
anchor_text: Text to search for (e.g., "Answer:")
content_paragraphs: List of paragraph objects from source document
Returns:
Number of paragraphs inserted
"""
# Find anchor
anchor_idx = None
for i, para in enumerate(doc.paragraphs):
if para.text.strip() == anchor_text:
anchor_idx = i
break
if anchor_idx is None:
raise ValueError(f"Anchor '{anchor_text}' not found in template")
# Get XML elements
anchor_element = doc.paragraphs[anchor_idx]._element
parent = anchor_element.getparent()
# Insert each paragraph right after anchor
inserted_count = 0
for source_para in content_paragraphs:
# Use XML element directly - preserves all formatting
new_para_element = source_para._element
# Insert after anchor position
parent.insert(
parent.index(anchor_element) + 1 + inserted_count,
new_para_element
)
inserted_count += 1
return inserted_countWhy XML API:
doc.add_paragraph()appends at end → wrong positionpara.insert_paragraph_before()has index tracking issues- XML API: direct element manipulation → correct position, zero artifacts
Table Element Repositioning
Move existing table to new position without recreating.
def move_table_to_position(doc, table_index, insert_before_para_index):
"""
Move existing table to new position without recreating.
Use when table is in wrong location but must preserve its structure.
"""
table = doc.tables[table_index]
table_element = table._element
# Remove from current position
current_parent = table_element.getparent()
current_parent.remove(table_element)
# Insert at new position
target_para = doc.paragraphs[insert_before_para_index]
target_element = target_para._element
target_parent = target_element.getparent()
target_parent.insert(
target_parent.index(target_element),
table_element
)Reverse-Order Multi-Anchor Insertion
Insert at multiple positions without index shifting.
def insert_at_multiple_anchors(doc, anchor_content_pairs):
"""
Insert content at multiple anchor positions safely.
Args:
anchor_content_pairs: List of (anchor_idx, content_paras) tuples
"""
# Sort in reverse order (largest index first)
sorted_pairs = sorted(anchor_content_pairs, key=lambda x: x[0], reverse=True)
# Insert from bottom up to preserve earlier indices
for anchor_idx, content_paras in sorted_pairs:
insert_after(doc, anchor_idx, content_paras)Why reverse order:
# Example: Template has "Answer:" at paragraphs 18, 27, 37
# WRONG: Forward insertion shifts later indices
insert_after(doc, 18, task1_content) # Task 1 inserted
# Now the "Answer:" that WAS at 27 is now at 27 + len(task1_content)
insert_after(doc, 27, task2_content) # WRONG! Inserts at wrong position
# CORRECT: Reverse order preserves earlier indices
insert_after(doc, 37, task3_content) # Insert last first
insert_after(doc, 27, task2_content) # Middle still at 27
insert_after(doc, 18, task1_content) # First still at 18Selective Table Cell Modification
Fill specific cells without recreating table.
def fill_table_cells(template, table_index, cell_values):
"""
Fill specific cells in existing table without recreating.
Args:
template: Document object
table_index: Which table to modify (0-indexed)
cell_values: Dict of (row, col) -> value
Example:
fill_table_cells(doc, 0, {
(0, 1): "5",
(1, 1): "Jane Smith",
(1, 2): "S12345"
})
"""
table = template.tables[table_index]
for (row, col), value in cell_values.items():
# Modify existing cell - don't recreate
table.rows[row].cells[col].text = value
# Table structure, styles, borders all preservedKey principle: Modify cells in place. Never remove and recreate the table.
Complete Multi-Section Workflow
from docx import Document
# STEP 1: Load template (never copy, never recreate)
template = Document('Form_Template.docx')
# STEP 2: Inspect structure
print("=== Template Structure ===")
print(f"Tables: {len(template.tables)}")
print(f"Paragraphs: {len(template.paragraphs)}")
# Find anchors
answer_positions = []
for i, para in enumerate(template.paragraphs):
if para.text.strip() == "Answer:":
answer_positions.append(i)
print(f" Found 'Answer:' at paragraph {i}")
# STEP 3: Fill info table (if exists)
if len(template.tables) > 0:
info_table = template.tables[0]
# Check if this is the info table
if "Name" in info_table.rows[0].cells[0].text:
# Fill cells in place
info_table.rows[0].cells[1].text = "Jane Smith"
info_table.rows[1].cells[1].text = "S12345"
info_table.rows[2].cells[1].text = "Dept A"
print(" Filled info table")
# STEP 4: Load content
content_doc = Document('my_content.docx')
# Find where each section starts
section1_start = None
section2_start = None
for i, para in enumerate(content_doc.paragraphs):
if "Section 1" in para.text or "Question 1" in para.text:
section1_start = i + 1 # Content starts after header
elif "Section 2" in para.text or "Question 2" in para.text:
section2_start = i + 1
# Extract content paragraphs
section1_paragraphs = content_doc.paragraphs[section1_start:section2_start-1]
section2_paragraphs = content_doc.paragraphs[section2_start:]
# STEP 5: Insert at anchors using XML API
def insert_after(doc, anchor_idx, content_paras):
anchor_elem = doc.paragraphs[anchor_idx]._element
parent = anchor_elem.getparent()
for offset, para in enumerate(content_paras):
parent.insert(
parent.index(anchor_elem) + 1 + offset,
para._element
)
# Insert in REVERSE order to preserve indices
insert_after(template, answer_positions[1], section2_paragraphs)
insert_after(template, answer_positions[0], section1_paragraphs)
print(f" Inserted {len(section1_paragraphs)} paragraphs for Section 1")
print(f" Inserted {len(section2_paragraphs)} paragraphs for Section 2")
# STEP 6: Save (original template fully preserved with content inserted)
template.save('Form_Completed.docx')
print("\n✓ Template filled - zero artifacts")Verification Pattern
def verify_template_preservation(original_path, filled_path):
"""
Verify that only expected content was added.
Checks:
- Table count unchanged (unless expected)
- Section count unchanged
- Styles unchanged
- Headers/footers preserved
"""
original = Document(original_path)
filled = Document(filled_path)
# 1. Table count
if len(original.tables) != len(filled.tables):
print(f"Warning: Table count changed: {len(original.tables)} → {len(filled.tables)}")
else:
print(f"✓ Table count preserved: {len(original.tables)}")
# 2. Section count
if len(original.sections) != len(filled.sections):
print(f"Warning: Section count changed")
else:
print(f"✓ Section count preserved: {len(original.sections)}")
# 3. Check specific table integrity
for i, (orig_table, fill_table) in enumerate(zip(original.tables, filled.tables)):
orig_rows = len(orig_table.rows)
fill_rows = len(fill_table.rows)
if orig_rows != fill_rows:
print(f"Warning: Table {i} rows changed: {orig_rows} → {fill_rows}")
else:
print(f"✓ Table {i} structure preserved")
# 4. Headers/Footers
for i, (orig_sec, fill_sec) in enumerate(zip(original.sections, filled.sections)):
orig_header = orig_sec.header.paragraphs[0].text if orig_sec.header.paragraphs else ""
fill_header = fill_sec.header.paragraphs[0].text if fill_sec.header.paragraphs else ""
if orig_header != fill_header:
print(f"Warning: Section {i} header changed")
else:
print(f"✓ Section {i} header preserved")#!/usr/bin/env python3
"""
Inspect DOCX template structure before modification.
This script prints complete template analysis to prevent destructive mistakes.
Always run before filling templates.
Usage:
python scripts/inspect_template.py <template.docx>
"""
import sys
from pathlib import Path
try:
from docx import Document
except ImportError:
print("Error: python-docx not installed")
print("Install with: pip install python-docx")
sys.exit(1)
def inspect_template(doc_path):
"""
Print complete template structure before any modifications.
Identifies:
- Table types and identities
- Anchor points for content insertion
- Headers/footers
- Document element counts
Prevents:
- Modifying wrong tables
- Missing anchor points
- Breaking headers/footers
- Index out-of-bounds errors
"""
doc_path = Path(doc_path)
if not doc_path.exists():
print(f"Error: File not found: {doc_path}")
sys.exit(1)
doc = Document(doc_path)
print("=" * 70)
print("TEMPLATE STRUCTURE ANALYSIS")
print("=" * 70)
# 1. High-level counts
print(f"\nDocument Elements:")
print(f" Tables: {len(doc.tables)}")
print(f" Paragraphs: {len(doc.paragraphs)}")
print(f" Sections: {len(doc.sections)}")
# 2. Table identities
print(f"\nTable Analysis:")
for i, table in enumerate(doc.tables):
first_cell = table.rows[0].cells[0].text[:60] if table.rows else ""
print(f" Table {i}:")
print(f" Size: {len(table.rows)}x{len(table.columns)}")
print(f" First cell: '{first_cell}...'")
# Sample first row to identify table type
if table.rows and table.rows[0].cells:
first_row_text = " | ".join([c.text[:20] for c in table.rows[0].cells])
print(f" First row: {first_row_text}")
# 3. Potential anchor points
print(f"\nPotential Anchor Points:")
anchors_found = 0
for i, para in enumerate(doc.paragraphs):
text = para.text.strip()
# Common anchor patterns
if (text.endswith(':') or
'Answer' in text or
'Summary' in text or
'FILL' in text or
'Response' in text or
'Solution' in text or
text in ['', '\n']): # Empty paragraphs might be fill points
print(f" Para {i}: '{text}' (style: {para.style.name})")
anchors_found += 1
if anchors_found > 20: # Limit output
print(f" ... ({len(doc.paragraphs) - i} more paragraphs)")
break
# 4. Headers/Footers
print(f"\nHeaders/Footers:")
for i, section in enumerate(doc.sections):
header_text = section.header.paragraphs[0].text[:50] if section.header.paragraphs else "(empty)"
footer_text = section.footer.paragraphs[0].text[:50] if section.footer.paragraphs else "(empty)"
print(f" Section {i}:")
print(f" Header: {header_text}")
print(f" Footer: {footer_text}")
print("=" * 70)
print("\nNow safe to proceed with modifications.")
print("=" * 70)
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <template.docx>")
sys.exit(1)
inspect_template(sys.argv[1])
Related skills
FAQ
Why not use pandoc with --reference-doc to fill a template?
--reference-doc only copies style definitions, not structure, so the template's tables, logos, headers, and footers are lost.
How is content inserted at the right position?
By finding anchor paragraphs and using the XML API to insert elements after them, inserting multiple anchors in reverse order to preserve indices.