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.

  • Documents & Office
  • $document-conversion
  • 193 lines
  • 2 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- document-conversion

Installs to ~/.agents/skills/document-conversion/ 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 $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.

~/.agents/skills/document-conversion/SKILL.md

--- name: document-conversion description: Converts documents between formats with Pandoc and headless LibreOffice - Markdown to and from DOCX, HTML, EPUB and PDF, Office files (DOC, DOCX,… license: MIT ---

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

FromToTool
Markdown, HTML, reStructuredText, LaTeXDOCX, ODT, EPUB, HTML, LaTeXPandoc
DOCX, ODT, EPUBMarkdown, HTMLPandoc (--extract-media)
MarkdownPDFPandoc with --pdf-engine (typst, weasyprint, xelatex, lualatex)
DOCX, XLSX, PPTX, ODT, RTF, DOC, XLS, PPTPDFLibreOffice headless
DOC, XLS, PPT (legacy binary)DOCX, XLSX, PPTXLibreOffice headless
XLSX sheetCSVLibreOffice headless (or openpyxl / DuckDB)
A web page or app viewPDFUse $html-to-pdf
Scanned PDFsearchable PDF / textUse $ocr-documents
PDFDOCX (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 in reference.docx; content in it is ignored.
  • Custom styles: a fenced div or span with custom-style becomes 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.docx and b/report.docx overwrite 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

FeaturePandoc (DOCX <-> MD)LibreOffice (Office -> PDF)
Headings, lists, emphasis, linksKeptKept
Simple tablesKept; merged cells flattened in MarkdownKept
ImagesKept with --extract-mediaKept
Footnotes, citationsKeptKept
Tracked changes, commentsOption-controlled (--track-changes)Printed as shown in the file’s view settings
Text boxes, SmartArt, shapesDroppedMostly kept; SmartArt may degrade
Equations (OMML)Converted to TeX mathKept
Exact layout, page breaksNot 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.txt is empty or every entry is explained.
  • Structure: heading count in the source equals the output (grep -c '^#' guide.md vs headings in pandoc 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.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: "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.

esc