Document Conversion skill for Codex
Format conversions that never drop content silently. Codex picks the converter that suits each format pair, styles Word output through a reference.docx and Lua filters instead of post-editing, and runs LibreOffice batches with isolated profiles and timeouts so parallel or hung conversions do not fail silently. It warns what will be lost before a big batch and checks outputs for page counts, fonts and structure.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- document-conversion Installs to ~/.agents/skills/document-conversion/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install document-conversion The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project document-conversion Installs to .agents/skills/document-conversion/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/document-conversion.zipunzip -o document-conversion.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) document-conversion PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $document-conversion, 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
Converts documents between formats with Pandoc and headless LibreOffice - Markdown to and from DOCX, HTML, EPUB and PDF, Office files (DOC, DOCX, XLSX, PPTX, ODT, RTF) to PDF, legacy formats to modern ones - with reference.docx styling, Lua filters, batch runs and fidelity checks. Use when the user asks to convert a file or folder to another format, turn Markdown docs into Word or PDF, get Markdown out of a .docx, or batch-convert Office files to PDF on a server.
Use it when
- A file or folder must move between Markdown, Word, HTML, EPUB and PDF
- Office files need converting to PDF without Microsoft Office
- Legacy .doc, .xls or .ppt files need upgrading to modern formats
- Pandoc output needs house styles or structural fixes
Use something else when
- The source is a live web page or JavaScript app: use html-to-pdf
- The source is a scanned document: use ocr-documents
- You are writing a new LaTeX paper rather than converting one: use latex-documents
How to check the result
Count outputs against inputs, compare page counts with pdfinfo, check substituted fonts with pdffonts, and confirm headings and images survived in a sample.
The mistake it prevents
Running several soffice conversions in parallel on one user profile, which exits 0 and writes no output at all.
Done well means: the right converter for the pair of formats, styling supplied by a template rather than patched afterwards, every input accounted for in a batch, and the output checked against the source (page count, headings, images, tables) because conversions lose things quietly.
Pick the converter
| From | To | Tool |
|---|---|---|
| Markdown, HTML, reStructuredText, LaTeX | DOCX, ODT, EPUB, HTML, LaTeX | Pandoc |
| DOCX, ODT, EPUB | Markdown, HTML | Pandoc (--extract-media) |
| Markdown | Pandoc with --pdf-engine (typst, weasyprint, xelatex, lualatex) | |
| DOCX, XLSX, PPTX, ODT, RTF, DOC, XLS, PPT | LibreOffice headless | |
| DOC, XLS, PPT (legacy binary) | DOCX, XLSX, PPTX | LibreOffice headless |
| XLSX sheet | CSV | LibreOffice headless (or openpyxl / DuckDB) |
| A web page or app view | Use $html-to-pdf | |
| Scanned PDF | searchable PDF / text | Use $ocr-documents |
| DOCX (editable) | No reliable open tool; set expectations: layout will need manual fixing |
Check what is installed before planning: pandoc --version, soffice --version,
typst --version. Installing them needs network and often admin rights; in a Codex
sandbox ask for approval, or tell the user what to install. Read AGENTS.md for docs
build conventions (some repos already have a make docs or pandoc defaults file).
Pandoc
# Markdown -> Word, styled by a reference document
pandoc -o reference.docx --print-default-data-file reference.docx # then edit its styles in Word
pandoc guide.md -o guide.docx --reference-doc=reference.docx --toc --toc-depth=2 -N
# Word -> Markdown with images extracted, no hard wrapping
pandoc report.docx -t gfm --extract-media=media --wrap=none -o report.md
# Accept tracked changes on the way out (or reject / all)
pandoc draft.docx --track-changes=accept -t gfm -o draft.md
# Markdown -> PDF without LaTeX
pandoc guide.md -o guide.pdf --pdf-engine=typst
pandoc guide.md -o guide.pdf --pdf-engine=weasyprint --css print.css
# Markdown -> PDF with LaTeX, Unicode fonts
pandoc guide.md -o guide.pdf --pdf-engine=lualatex -V mainfont="Noto Serif" -V geometry:margin=2cm
# Single self-contained HTML (images and CSS inlined)
pandoc guide.md -s --embed-resources --css style.css -o guide.html
# Citations
pandoc paper.md --citeproc --bibliography refs.bib --csl apa.csl -o paper.docx
Put repeated options in a defaults file and commit it:
# pandoc-docx.yaml -> pandoc -d pandoc-docx.yaml guide.md -o guide.docx
from: markdown+smart
reference-doc: templates/reference.docx
toc: true
toc-depth: 2
number-sections: true
resource-path: [".", "docs/images"]
metadata:
lang: en-GB
Styling Word output
- Pandoc maps Markdown structure to Word styles:
Heading 1..6,Body Text,First Paragraph,Block Text,Source Code,Table,Image Caption. Change those styles inreference.docx; content in it is ignored. - Custom styles: a fenced div or span with
custom-stylebecomes a Word paragraph or character style of that name.
::: {custom-style="Warning Box"}
Back up the database before running this migration.
:::
Lua filters for structural fixes
-- demote-headings.lua: shift every heading down one level (H1 -> H2)
function Header(el)
el.level = math.min(el.level + 1, 6)
return el
end
-- page-break.lua: a \newpage line (parsed as a raw TeX block) becomes a Word page break
function RawBlock(el)
if FORMAT == "docx" and el.format:match("tex") and el.text:match("^%s*\\newpage%s*$") then
return pandoc.RawBlock("openxml", '<w:p><w:r><w:br w:type="page"/></w:r></w:p>')
end
end
Run with --lua-filter=demote-headings.lua. Prefer Lua filters over regex post-processing
of the output file.
LibreOffice headless
# One file or a whole folder to PDF
soffice --headless --convert-to pdf --outdir out/ contracts/*.docx
# Legacy binary formats to OOXML
soffice --headless --convert-to docx --outdir out/ archive/*.doc
soffice --headless --convert-to xlsx --outdir out/ archive/*.xls
# Spreadsheet to CSV (UTF-8, comma); the last token -1 exports every sheet to its own file
soffice --headless --convert-to 'csv:Text - txt - csv (StarCalc):44,34,76,1,,0,false,true,false,false,false,-1' \
--outdir out/ book.xlsx
# PDF export options as JSON (LibreOffice 7.4+): PDF/A-2b
soffice --headless \
--convert-to 'pdf:writer_pdf_Export:{"SelectPdfVersion":{"type":"long","value":"2"}}' \
--outdir out/ report.docx
Operational rules:
- LibreOffice refuses to run twice on one user profile. If a desktop instance is open, or
you convert in parallel, the command exits 0 and produces nothing. Give each worker its
own profile:
soffice -env:UserInstallation=file:///tmp/lo-profile-$$ --headless .... - Use per-file timeouts in batches (a corrupt file can hang forever). Example loop:
mkdir -p out logs
for f in in/*.docx; do
timeout 120 soffice -env:UserInstallation=file:///tmp/lo-$$ --headless \
--convert-to pdf --outdir out "$f" >>logs/convert.log 2>&1 \
|| echo "FAILED $f" >>logs/failures.txt
[ -f "out/$(basename "${f%.*}").pdf" ] || echo "MISSING $f" >>logs/failures.txt
done
(timeout is GNU coreutils; on macOS install coreutils and use gtimeout, or drive the
loop from Python with subprocess.run(..., timeout=120).)
- Output names reuse the input basename: two inputs
a/report.docxandb/report.docxoverwrite each other in one--outdir. Mirror the folder structure. - Fonts: missing fonts are substituted and pagination shifts. Install the document’s fonts
(
fc-list | grep -i calibri); Microsoft’s Carlito and Caladea are metric-compatible with Calibri and Cambria.
Fidelity: what to expect
| Feature | Pandoc (DOCX <-> MD) | LibreOffice (Office -> PDF) |
|---|---|---|
| Headings, lists, emphasis, links | Kept | Kept |
| Simple tables | Kept; merged cells flattened in Markdown | Kept |
| Images | Kept with --extract-media | Kept |
| Footnotes, citations | Kept | Kept |
| Tracked changes, comments | Option-controlled (--track-changes) | Printed as shown in the file’s view settings |
| Text boxes, SmartArt, shapes | Dropped | Mostly kept; SmartArt may degrade |
| Equations (OMML) | Converted to TeX math | Kept |
| Exact layout, page breaks | Not preserved (Markdown has no pages) | Close to Word; font-dependent |
Tell the user what will be lost before converting a large batch, and do a sample of 3-5 files first.
Verification
pdfinfo out/report.pdf | grep Pages # compare with Word's page count if known
pdffonts out/report.pdf # substituted fonts show here
pandoc out/guide.docx -t plain | head -40 # headings and text survived
pdftoppm -f 1 -l 2 -r 60 -png out/report.pdf preview # look at the first pages
- Batch: number of outputs equals number of inputs;
failures.txtis empty or every entry is explained. - Structure: heading count in the source equals the output
(
grep -c '^#' guide.mdvs headings inpandoc out.docx -t gfm). - Images: count
media/files after extraction against images in the source.
Report
**Converted:** 214 of 216 .docx files to PDF (LibreOffice 25.2, isolated profiles, 120 s timeout)
**Failed:** 2 files (password-protected) - listed in logs/failures.txt
**Fidelity notes:** text boxes in 11 files render slightly offset; fonts Calibri -> Carlito
**Checked:** page counts match Word for a 10-file sample; previews reviewed What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Document Conversion"
short_description: "Format conversions that never drop content silently"
default_prompt: "$document-conversion Convert our docs/ Markdown folder into a single styled Word document" 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.