LlmFiles
Converter to Markdown
LlmFiles/Guides/DOCX to Markdown

DOCX to Markdown

A .docx file is the best input a converter can get: Word stores headings as styles, lists as numbering definitions and tables as a real grid. Most of that maps to Markdown one to one. The rest — the empty header row, images, comments, tracked changes — is what this page is about, measured on real files rather than promised.

Updated 24 Oct 2026 · LlmFiles

Why DOCX converts better than PDF

In a PDF the structure is a side effect of position and font size, so a converter has to guess. In a DOCX the structure is data: every paragraph points at a style, every list points at a numbering definition, every table declares its rows and cells. A converter reads those pointers instead of guessing, which is why the output has real # headings and real pipe tables. If you have the Word file, convert it instead of its PDF export — that advice alone fixes most bad Markdown.

What LlmFiles returns for a DOCX

The pipeline is: Word styles and numbering are read, converted to HTML, then to Markdown. Here is the actual output for a 4.6 KB report — a title, a subtitle, a bold line that is not a heading, two lists, a table, a footnote and one image (926 characters, 36 lines):

# Quarterly Report

Prepared for the board, Q3 2026.

**Results**

Revenue grew **12%** quarter over quarter, and *churn* fell. See [the dashboard](https://llmfiles.com/pricing) for details.

* First bullet about pipeline
* Second bullet with a nested item
  + Nested bullet under the second

1. Open the config file.
2. Set the timeout to 30 seconds.

## Numbers by region

|  |  |
| --- | --- |
| **Region** | **Revenue** |
| EMEA | 48.2 |

A footnote marker sits here[[1]](#footnote-1) and the sentence ends.

![](data:image/png;base64...)

1. Numbers are unaudited, see appendix B.

Note what happened in that excerpt: the heading levels came from the styles, the bold paragraph stayed a bold paragraph, the footnote moved to the bottom as a numbered list item, and the picture turned into a stub. The empty header row in the middle of it gets its own section below.

The mapping, measured element by element

Each row of this table was checked on a generated file and on Word documents saved by Word itself, not taken from documentation:

Word elementMarkdown you getNote
Heading 1 … Heading 6 style# … ######Exact. The level comes from the style, never from the font size
Bold, italic, strikethrough runs**b**, *i*, ~~s~~Exact
Hyperlink[text](url)Exact, including external links and internal anchors
Bulleted and numbered lists* item and 1. item, nesting by two spacesNumbers are renumbered from one; nested level comes out as +
Footnote and endnote[[1]](#footnote-1) plus a numbered list at the endBoth markers and text survive, but they land at the bottom of the file
TablePipe table with a separator rowRows and columns survive, the header row usually does not — see below
Merged cells (gridSpan)Extra empty cellColumn count is preserved; a horizontal merge is expressed as a blank cell
Text boxIts text as a paragraphContent is not lost
Bold paragraph in the Normal style**text**Bold is not a heading. If the author styled titles by hand, fix them afterwards
Monospace font (Consolas, Courier)Plain textNo backticks: a font is not a style
Title and Subtitle stylesPlain paragraphsWord's dedicated title style is not mapped to #
Quote stylePlain paragraphNo > blockquote
Tabs inside a paragraphA single spaceColumns of an old-style layout collapse
Shift+Enter line breakA plain newlineNot a Markdown hard break — add two spaces if you need one

What does not survive

These are the losses, measured on purpose-built files and on Word's own samples:

What disappearsWhat you actually getWhy
Images![](data:image/png;base64...)A text placeholder, not the picture: the alt text is kept, the bytes are not
CommentsNothing at allReviewer notes are a separate part of the file and are skipped
Tracked changesInsertions kept, deletions dropped, no markupThe converter silently shows the accepted version
Headers and footersNothingSo no "Confidential" stamp and no page numbers
Auto-numbered headings# Introduction without the numberThe number is generated by Word at render time, it is not in the text
A table of contents fieldIts cached text as one lineThe TOC is a field with a stored preview, links included are lost
Legacy .docA code block of raw bytesThe old binary format is not supported — save as .docx first
Tracked changes are the dangerous one. Convert a document still under review and the output is neither the old nor the new version: it is the insertion applied and the deletion applied, with nothing marking either. Accept or reject the changes in Word before converting, so the Markdown matches a version someone approved.

The empty header row, and how to fix it

This is the defect people notice first. A Word table has no header cells — a row is just cells that happen to be bold. So the converter cannot know which row is the header, and Markdown requires a header row. The result is an empty one, followed by the separator, followed by your data:

|  |  |
| --- | --- |
| Top left | Top right |
| Bottom left | Bottom right |

There is one way to fix it at the source: in Word select the first row, open Table Properties, Row, and tick Repeat as header row at the top of each page. Only that flag turns the row into a header cell — measured: the look-and-feel option firstRow alone changes nothing in the output. If you would rather not touch the document, run this over the Markdown instead. It moves the first data row up into the header and leaves tables that already have one alone:

import re, sys

SEP = re.compile(r"^\|(\s*:?-{3,}:?\s*\|)+$")     # | --- | --- |
EMPTY = re.compile(r"^\|(\s*\|)+$")               # |  |  |

def is_row(line):
    return line.strip().startswith("|") and line.strip().endswith("|")

def fix_tables(md):
    lines = md.split("\n")
    out, i, fixed = [], 0, 0
    while i < len(lines):
        line = lines[i]
        if (is_row(line) and EMPTY.match(line.strip())
                and i + 2 < len(lines)
                and SEP.match(lines[i + 1].strip())
                and is_row(lines[i + 2]) and not EMPTY.match(lines[i + 2].strip())):
            out.append(lines[i + 2])   # first data row becomes the header
            out.append(lines[i + 1])   # separator row stays
            i += 3
            fixed += 1
            continue
        out.append(line)
        i += 1
    return "\n".join(out), fixed

for path in sys.argv[1:]:
    text = open(path, encoding="utf-8").read()
    fixed_text, n = fix_tables(text)
    if n:
        open(path, "w", encoding="utf-8").write(fixed_text)
    print(f"{path}: {n} table(s) fixed")

It was run on a Word-produced sample and on the report above: one table fixed in each, and a table that already had a real header row came through unchanged.

A pipe inside a cell breaks the whole row

Markdown separates cells with |, and nothing escapes it for you. A cell containing a|b turns a two-column row into three columns, and every row below it shifts:

| Format | Files |
| --- | --- |
| a|b\*c\_d | 3 |     <- three cells instead of two

Asterisks and underscores are escaped automatically (that \* and \_ are the converter's own work), pipes are not. Replace | with / in the source, or escape it as \| in the Markdown afterwards.

Lists inside table cells collapse

A cell that holds a bullet list has no line structure in the output — the items are joined into the cell text: Cell with a list * item one * item two. If the documents you convert look like that, take the lists out of the cells before converting, or split the table into one table per list.

Repair pass before the text goes to a model

  1. Fix the header rows — the script above, one pass over the whole folder.
  2. Replace pipes inside values, or the tables mislead a reader and a parser alike.
  3. Promote the fake headings: if the author used bold instead of styles, lines that are only **text** are probably your section titles. Turn them into ## text — carefully, some of them are genuinely just emphasised paragraphs.
  4. Delete the image stubs (![](data:image/png;base64...)) — they carry no information and they teach a retriever nothing.
  5. Keep one # per document and push everything else down a level, so chunk boundaries mean something.
  6. Move the footnote list where the footnote is, if the document is read in fragments rather than top to bottom.

Privacy, limits and the API

Files are parsed on our server: a temporary copy exists for the duration of the conversion and is deleted right after it. Nothing is sent to third-party OCR or LLM services, and file contents never reach analytics — only an event name and a hashed IP. For documents under NDA there is an on-prem install inside your own network.

Free tier: 5 files a day from one IP, up to 25 MB each, no sign-up and no account. Pro (300 MB, no daily cap) is not on sale yet — we are not taking money for it. Leave a request on the pricing page and we will write when launch pricing opens. For pipelines there is POST /api/convert (JSON with a markdown field), POST /api/convert/batch (up to 20 files in one request) and POST /api/convert/zip (a ZIP in, a ZIP of .md files out). Working examples are in the API documentation.

Frequently asked questions

Do Word heading styles become real Markdown headings?

Yes: Heading 1 to Heading 6 become # to ######, in both the short and the deep direction — a level-6 heading was measured coming out as six hashes. A bold paragraph in the Normal style does not: it stays **text**, because bold is formatting, not structure.

Why does my table start with an empty row?

Because a Word table has no header cells. Markdown needs one, so an empty header row and its separator are emitted. Tick Repeat as header row in Word's table properties, or run the repair script from this page — it moves the first data row into the header.

Are footnotes and comments kept?

Footnotes and endnotes are: the marker stays inline as [[1]](#footnote-1) and the text is appended as a numbered list. Comments are dropped completely — they are a separate part of the file and are not read.

What happens to tracked changes?

Insertions are kept and deletions are dropped, with no markup and no warning: you get the accepted version. For a document under review, accept or reject the changes in Word first.

Can I get the images out of a DOCX?

Not through this converter — a picture becomes ![](data:image/png;base64...), a placeholder with the alt text and no image data. Unzip the .docx and take the files from word/media/ if you need them.

What about the old .doc format?

Not supported: the binary format is read as raw bytes and dumped into a code block. Open the file in Word, LibreOffice or Google Docs and save it as .docx — that takes a second and the conversion then works normally.

What are the limits, and can I automate this?

Free: 5 files a day from one IP, up to 25 MB. Pro is not being sold yet. Automation works today via POST /api/convert, /api/convert/batch and /api/convert/zip.

Convert a file

Drag a document into the window and get Markdown you can read, diff and feed to a model. Free: 5 files a day, up to 25 MB.