DOCX Templates skill for Codex

Business owns the layout, code owns the data. Codex sets up Word generation so the template stays editable in Word while the code supplies validated data: it inventories every tag, writes a data contract, uses paragraph and table-row loops, rich text and images, renders with strict undefined handling and escaping, repairs tags Word split across runs, and adds fixture-based render tests that catch leftover placeholders.

  • Documents & Office
  • $docx-templates
  • 209 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- docx-templates

Installs to ~/.agents/skills/docx-templates/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $docx-templates, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds templated Word generation where non-developers own the .docx layout and code supplies data, using docxtpl (Jinja2 tags, paragraph and table-row loops, RichText, InlineImage, subdocuments) in Python or docxtemplater (PizZip, loops, conditions) in Node, with template linting and render tests. Use when asked to generate contracts, quotes, reports or certificates from a Word template, fill {{placeholders}} in a .docx, fix tags that Word split or broke, or add loops and conditionals to a Word template.

Use it when

  • A designer- or business-owned Word template must be filled from application data
  • Templates need loops over table rows, conditional paragraphs or inline images
  • A template broke after someone edited it in Word
  • You want automated tests that guard a template against regressions

Use something else when

  • You are producing hundreds of letters from a spreadsheet: use mail-merge
  • The document is built entirely in code with no template file: use docx-documents

How to check the result

Render every fixture, assert no {{, {% or leftover delimiters, None or undefined remain in the document XML, and review PDF conversions of the long-name and many-row cases.

The mistake it prevents

Rendering without autoescape, so a customer name containing an ampersand produces a .docx that Word reports as corrupt.

~/.agents/skills/docx-templates/SKILL.md

--- name: docx-templates description: Builds templated Word generation where non-developers own the .docx layout and code supplies data, using docxtpl (Jinja2 tags, paragraph and… license: MIT ---

Done well means the template stays editable by the business owner in Word, every tag in it is known to the code, rendering fails loudly on missing data instead of printing blanks, and a render test with realistic data protects the template from future edits.

Pick the engine

SituationEngine
Python service or scriptdocxtpl (pip install docxtpl, Jinja2 syntax)
Node/TypeScript service or browserdocxtemplater + PizZip (npm i docxtemplater pizzip)
Template already uses Word MERGEFIELDs from MailingsKeep them; see mail-merge
Document is built entirely from code, no designer-owned layoutpython-docx or docx.js: see docx-documents

Images in docxtemplater need the paid image module or an open-source alternative; in docxtpl InlineImage is built in. Installing either needs network.

Workflow

- [ ] 1. Read AGENTS.md and the existing template; list every tag
- [ ] 2. Define the data contract (schema + sample JSON)
- [ ] 3. Fix or author tags in the template
- [ ] 4. Render with strict undefined handling
- [ ] 5. Render tests: sample data + edge cases, converted to PDF for review
- [ ] 6. Document the tag vocabulary for template owners

1. Inventory tags

from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/quote.docx")
print(sorted(tpl.get_undeclared_template_variables()))

Also grep the raw XML; tags split across runs show up as fragments:

unzip -p templates/quote.docx word/document.xml | grep -o '{[^}]*' | head -50
unzip -Z1 templates/quote.docx | grep -E 'header|footer'   # tags there are rendered too

2. Data contract

Write the context shape down and validate data before rendering (Pydantic, JSON Schema or zod). Format numbers, dates and currencies in code; templates only place strings.

{
  "quote_no": "Q-2026-0142",
  "issued": "28 October 2026",
  "customer": { "name": "Smith & Sons Ltd", "address_lines": ["1 High St", "Leeds LS1 1AA"] },
  "items": [{ "sku": "SV-01", "desc": "Annual support", "qty": "1", "total": "4,800.00" }],
  "vat_applies": true,
  "grand_total": "5,760.00"
}

3. Tag syntax (docxtpl)

GoalTagNotes
Value{{ customer.name }}Escaped when autoescape=True
Conditional paragraph{%p if vat_applies %} … {%p endif %}Each {%p %} tag on its own paragraph; that paragraph is removed
Loop paragraphs{%p for line in customer.address_lines %} {{ line }} {%p endfor %}
Loop table rows{%tr for it in items %} in its own row, row with {{ it.desc }}, {%tr endfor %} rowTag rows are removed; the middle row repeats
Loop table columns{%tc for c in cols %} … {%tc endfor %}
Inside one run{%r if urgent %}URGENT{%r endif %}Keeps run formatting
Rich text{{r note }} with RichText(...) in contextNote the r
Image{{ logo }} with InlineImage(tpl, "logo.png", width=Mm(30))
Line breaks in a valueListing("a\nb") or RichText with \nPlain \n in a string is ignored by Word
from docxtpl import DocxTemplate, InlineImage, RichText
from docx.shared import Mm
from jinja2 import StrictUndefined, Environment

tpl = DocxTemplate("templates/quote.docx")
ctx = load_context()                                      # validated dict
ctx["logo"] = InlineImage(tpl, "assets/logo.png", width=Mm(30))
ctx["note"] = RichText("Prices valid for 30 days.", italic=True)

env = Environment(undefined=StrictUndefined, autoescape=True)   # missing keys raise
tpl.render(ctx, jinja_env=env, autoescape=True)
tpl.save("out/Q-2026-0142.docx")

autoescape=True is required whenever data can contain &, < or >; without it a company called “Smith & Sons” produces a document Word refuses to open.

Subdocuments (a block built in code and inserted whole; needs the optional docxcompose package, pip install "docxtpl[subdoc]"):

sd = tpl.new_subdoc()
sd.add_paragraph("Terms differ for this customer:")
ctx["custom_terms"] = sd          # template: {{p custom_terms }}

3b. docxtemplater (Node)

import fs from "node:fs";
import PizZip from "pizzip";
import Docxtemplater from "docxtemplater";

const zip = new PizZip(fs.readFileSync("templates/quote.docx"));
const doc = new Docxtemplater(zip, {
  paragraphLoop: true,          // loops spanning whole paragraphs don't leave empty lines
  linebreaks: true,             // "\n" in values becomes a Word line break
  nullGetter(part) {            // fail on missing data instead of printing "undefined"
    if (!part.module) throw new Error(`Missing value for tag {${part.value}}`);
    return "";
  },
});
try {
  doc.render(data);
} catch (err) {
  for (const e of err.properties?.errors ?? [err]) console.error(e.properties?.explanation ?? e.message);
  throw err;
}
fs.writeFileSync("out/quote.docx", doc.getZip().generate({ type: "nodebuffer", compression: "DEFLATE" }));

docxtemplater syntax: {name}, loops and truthy sections {#items}...{/items}, inverted {^items}No items{/items}, raw OOXML {@rawXml} (dangerous with user data). Table-row loops work by placing {#items} in the first cell and {/items} in the last cell of the same row. The delimiters can be changed with the delimiters option if the document legitimately contains braces.

Fixing tags Word broke

Word splits text into runs when it was typed with spell-check, autocorrect, revision tracking or mixed formatting, giving XML like {{ cust</w:t></w:r><w:r><w:t>omer }}. docxtpl and docxtemplater repair many cases, but not all. Fix order:

  1. In Word, select the whole tag, cut, and paste back as plain text (Ctrl+Shift+V), or retype it in one go with proofing disabled for that text.
  2. Replace curly quotes inside tags: {% if status == “paid” %} must use straight quotes. Turn off “straight quotes with smart quotes” in AutoCorrect while editing templates.
  3. Accept all tracked changes in the template; revisions split runs.
  4. Keep formatting uniform across the whole tag (one font, size, colour).

Reject templates that still fail; do not “fix” them by regex on the XML each render.

5. Render tests

Keep fixtures next to the template and test in CI:

import subprocess, zipfile, re
from pathlib import Path

CASES = ["minimal.json", "typical.json", "long_names.json", "many_items.json", "special_chars.json"]

def test_quote_template(tmp_path):
    for case in CASES:
        out = tmp_path / f"{case}.docx"
        render("templates/quote.docx", load(f"tests/fixtures/{case}"), out)
        xml = zipfile.ZipFile(out).read("word/document.xml").decode()
        assert not re.search(r"\{\{|\{%|\}\}|%\}", xml), f"unrendered tag in {case}"
        assert "None" not in xml and "undefined" not in xml

For visual review, convert renders to PDF in one LibreOffice call and look at the pages: soffice --headless --convert-to pdf --outdir out/pdf out/*.docx.

Edge cases worth a fixture: empty lists (does the table header still make sense?), very long names (wrapping in table cells), &, <, quotes and emoji in values, right-to-left text if the business has such customers, 100+ rows (page breaks, repeating header row: set “Repeat as header row” on the template’s header row in Word).

6. Document the vocabulary

Give template owners a one-page reference: every tag, its meaning, sample value, and which tags may be deleted safely. Store it next to the template and update it whenever the data contract changes.

Gotchas

  • {%p %} and {%tr %} tags must be alone in their paragraph or row; other text there is deleted with the tag.
  • A Jinja error message points at XML, not the Word page; search the error’s tag name in word/document.xml to locate it.
  • Headers, footers and footnotes are templated too; tags there need the same care.
  • Fields like TOC and page numbers are not updated by rendering; set “update fields on open” or convert via LibreOffice, which recalculates them.
  • Do not hand untrusted users the ability to upload templates: Jinja in docxtpl executes expressions; use a sandboxed environment (jinja2.sandbox.SandboxedEnvironment) if they must.

Output

## Template: templates/quote.docx
- Engine: docxtpl, StrictUndefined, autoescape on
- Tags (11): quote_no, issued, customer.name, customer.address_lines[], items[] (sku, desc,
  qty, total), vat_applies, grand_total, logo, note
- Fixed: 3 split tags, 2 curly-quote conditions
- Tests: 5 fixtures render, no leftover tags; PDFs reviewed for long-name and 120-row cases

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "DOCX Templates"
  short_description: "Business owns the layout, code owns the data"
  default_prompt: "$docx-templates Generate quotes from templates/quote.docx with a line-item table that grows with the data"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem