Convert documents into clean Markdown with Microsoft's markitdown. It keeps structure — headings, tables, lists, links, slide notes, sheet names — instead of scraping flat text, which is what makes it good LLM input.
It extracts text that is already in the file. It does not OCR: scanned PDFs and photos come back empty (see "Image-only content" below).
Setup
markitdown --version # expect 0.1.x or later
If it's missing, install it as an isolated tool. It needs Python 3.10+:
uv tool install "markitdown[all]" # preferred
pipx install "markitdown[all]" # alternative
Avoid bare pip install with the macOS system Python (3.9): pip silently resolves the
ancient markitdown 0.0.1a1, which has no [all] extra and no working CLI. If an install
reports 0.0.1a1 or "does not provide the extra 'all'", the interpreter is too old — use
uv tool install or uv venv -p 3.12.
[all] pulls every optional converter. Lean installs can pick extras: pdf, docx,
pptx, xlsx, xls, outlook, audio-transcription, youtube-transcription,
az-doc-intel, az-content-understanding. A MissingDependencyException names the
extra that's missing.
If the environment forbids global installs, use a throwaway venv and call
<venv>/bin/markitdown directly.
Single file
markitdown report.pdf -o report.md # write to a file
markitdown report.pdf # stdout — good for piping into a prompt
curl -sL "$URL" | markitdown -x html # stdin: add -x/-m hints for reliable detection
Useful flags (full list: markitdown --help):
| Flag | Purpose |
|---|---|
-o FILE |
Output file instead of stdout |
-x EXT / -m MIME / -c CHARSET |
Format hints — mainly for stdin |
--keep-data-uris |
Keep inline base64 images (truncated by default) |
-p / --list-plugins |
Use / list installed 3rd-party plugins |
-d -e URL |
Azure Document Intelligence (cloud OCR) — see references |
--use-cu --cu-endpoint URL |
Azure Content Understanding — see references |
Exit status is non-zero for missing files and unsupported formats, but 0 with empty output for image-only content — so always check the result isn't blank.
Batch conversion
For a directory, use the bundled script instead of hand-rolling a loop:
bash <skill-dir>/scripts/batch_convert.sh SRC_DIR [OUT_DIR]
# MARKITDOWN=/path/to/markitdown if it isn't on PATH
# EXTENSIONS="pdf docx" to narrow the file types
It recurses, mirrors the tree into OUT_DIR (or writes beside each source), names
outputs <file>.<ext>.md so report.pdf and report.docx can't collide, skips
up-to-date outputs on reruns, and ends with a summary that flags EMPTY (needs OCR),
FAILED (with the error), and LEGACY (.doc/.ppt) files. Relay those flagged
files to the user — they are the ones that need a decision. If the user wants a different
naming scheme (e.g. report.md), adapt the script's dest= line rather than rewriting it.
What converts well
| Input | Result |
|---|---|
.docx |
Headings, lists, tables, links — a table whose first row isn't marked as a Word header row gets an empty | | | header, with the real headers as the first data row; fix it up if the table matters |
.pptx |
One section per slide (<!-- Slide number: N -->), speaker notes under ### Notes: |
.xlsx / .xls |
One ## SheetName section with a table per sheet |
| PDF (digital) | Text plus tables recovered via pdfplumber; complex multi-column layouts can scramble |
| HTML, RSS/Atom, Wikipedia, Bing results | Main content as Markdown |
CSV / JSON / XML / .ipynb |
Tables / text / notebook cells |
EPUB, Outlook .msg |
Chapters / headers + body |
| ZIP | Every supported member, each under ## File: name |
Audio (.wav .mp3 .m4a .mp4) |
Metadata plus a transcript via Google's free Web Speech API — uploads the audio, needs network |
| YouTube URL | Title, description and transcript when available |
Images (.jpg .png) |
Only EXIF metadata (needs exiftool) and an optional LLM caption — no OCR |
Legacy .doc / .ppt are unsupported (UnsupportedFormatException). Convert first:
textutil -convert docx memo.doc on macOS, or
soffice --headless --convert-to docx memo.doc with LibreOffice, then run markitdown.
Image-only content (scans, photos)
When a PDF or image yields empty or near-empty output, the text is pixels, not characters. Tell the user plainly, then choose a route:
- Read it yourself. If you can view images and PDFs (e.g. the Read tool), open the file and transcribe it — often the quickest path for a few pages. Say that this is your transcription, not markitdown output.
- Local OCR, if installed:
ocrmypdf scan.pdf out.pdfthen markitdownout.pdf, ortesseract scan.png -for images. - Cloud OCR: Azure Document Intelligence (
-d -e ...) — sends the document to Azure; only with the user's consent.
Never hand back an empty .md as if the conversion worked.
Python API
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("report.pdf") # path, URL, or binary file-like object
text = result.markdown # the Markdown string (alias: .text_content)
title = result.title # may be None
For streams, error handling, LLM image captions, Azure and plugins, read references/python-api.md.
Before handing results over
- Look at the output.
headit or read it. Check that tables came through and the text isn't garbled or blank; say so when it is. - Privacy. Offline conversion stays local; audio transcription, YouTube, LLM captions
and Azure send content out. Ask before routing sensitive files through them, and keep
keys (
OPENAI_API_KEY, Azure credentials) in the environment, never in code. - Large outputs. A long PDF can produce megabytes of Markdown. Write to a file and read or grep the parts you need rather than dumping it all into the conversation.