TL;DR
anydoc is Firecrawl’s MIT-licensed Rust library that converts fourteen office document formats — plus text-based PDFs — into one consistent flavour of GitHub-Flavored Markdown. It is the “stop stitching five parsers together” library. Key facts:
- 17.1K GitHub stars, 982 forks, MIT license, pure Rust — repo opened August 3, 2026, latest release v0.1.9 (August 13, 2026)
- Median conversion: 4.4ms per document. No ML models, no GPU, no API key, no system dependencies
- 14 formats in one binary:
.doc,.docx,.docm,.xls,.xlsx,.xlsm,.xlsb,.ppt,.pptx,.odt,.ods,.odp,.rtf,.epub,.csv— plus PDF via pdf-inspector - Four runtimes: Rust crate, Node.js (
@firecrawl/anydoc), Python (firecrawl-anydoc), and browser WebAssembly - Ships as an Agent Skill —
npx skills add firecrawl/anydocand Claude Code, Codex, or Cursor can read any office document it stumbles into - The honest catch: the headline benchmark is vendor-run and LLM-judged, it does zero OCR, encrypted files hard-fail, and spreadsheet number formatting is currently dropped (open issue #27)
If your ingestion pipeline currently routes .docx through mammoth, .xlsx through openpyxl, .pptx through python-pptx, and shells out to LibreOffice for the 2009 .xls exports, anydoc is a plausible one-dependency replacement for all four. Below: real code in three languages, the benchmark with its caveats attached, where it breaks, and who should stay put.
What anydoc actually is
Firecrawl — the web-scraping-for-LLMs company — hit the same wall every document pipeline hits: no single library reliably converts every common format to clean Markdown. You end up with four or five tools, each with its own dependency tree, its own output shape, and its own failure modes. A table that escapes correctly in your docx path renders broken in your rtf path, because different libraries wrote those serializers.
anydoc’s answer is architectural rather than clever: every format gets its own parser, but all parsers emit into one shared document model, and that model renders through one Markdown serializer.
document bytes
│
├─► format detection → content markers, not the extension
│
├─► format parser → one per format (doc, docx, ppt, pptx, xls,
│ xlsx, odt/ods/odp, rtf, epub, csv)
│ │
│ └─► Document → shared model: blocks, inlines, tables,
│ footnotes, assets
│ │
│ └─► GFM serializer → Markdown
│
└─► PDF → pdf-inspector → Markdown directly
The payoff is stated plainly in the README: “A table-escaping fix for docx is automatically a table-escaping fix for rtf, odt, and everything else.” That is the whole thesis. Heading anchors, footnotes, list numbering, and table escaping behave identically whether the input was a .doc from 2003 or a .pptx from yesterday.
It was released alongside — and depends on — pdf-inspector, Firecrawl’s PDF classifier. Both libraries already power Firecrawl’s /parse and /scrape endpoints in production, which is a meaningfully better signal than a benchmark table: the vendor eats its own dog food at commercial scale.
Why it’s trending now
Two reasons converged in August 2026.
First, RAG pipelines finally admitted that parsing is the bottleneck. Chunking strategies and reranking got most of the 2025 attention, but teams kept discovering that retrieval quality was capped by garbage extraction upstream. A merged table cell that flattens into gibberish poisons every chunk downstream, and no reranker fixes it.
Second, the “just throw it at a vision model” era got expensive. Routing every upload through a multimodal endpoint at GPU prices, when most are structurally readable .docx files, eventually shows up on a CFO’s radar. anydoc plus pdf-inspector is the deterministic, CPU-only path for everything that does not genuinely need pixels.
A note on the hype curve, because honesty is cheap here: anydoc did not blow up on Hacker News. The two submissions of the repo scored 4 points and 3 points respectively, with essentially no discussion. Its 17K stars came from X (Firecrawl’s launch thread, amplified by Garry Tan), Reddit’s r/AIDeveloperNews, and developer newsletters — not from a front-page HN thread. Star velocity from a well-connected YC company is not the same as broad practitioner validation, and you should weight it accordingly.
Getting started
As an agent skill (the interesting path)
anydoc ships as an Agent Skill, which is the deployment mode most relevant to anyone running coding agents:
npx skills add firecrawl/anydoc
That teaches Claude Code, Codex, Cursor, OpenCode, or any compatible agent to invoke the anydoc CLI when it encounters a document it cannot natively read. In practice this closes a genuinely annoying gap — an agent asked to “summarise the requirements in spec.docx” otherwise either fails, or hallucinates from the filename.
CLI
npx @firecrawl/anydoc report.docx # Markdown to stdout
npx @firecrawl/anydoc slides.pptx -o slides.md # or to a file
npx @firecrawl/anydoc - --format csv < data.csv # read stdin
npx pulls the prebuilt binary for your platform on first run. For a permanent command, npm install -g @firecrawl/anydoc.
Node.js
npm install @firecrawl/anydoc
import { toDocument, toMarkdown, toMarkdownBytes } from '@firecrawl/anydoc';
// From a file path:
const markdown = await toMarkdown('report.docx');
// From bytes, with the format detected from the content:
const fromBytes = await toMarkdownBytes(bytes);
// Signature-less formats (CSV) need an explicit name:
const fromCsv = await toMarkdownBytes(bytes, 'csv');
// Or stop at the document model, which also carries embedded assets:
const document = await toDocument(bytes);
Conversion runs on the libuv thread pool, so it never blocks the event loop — a detail that matters if you are converting uploads inside an Express handler.
Python
pip install firecrawl-anydoc
import anydoc
markdown = anydoc.to_markdown("report.docx")
markdown = anydoc.to_markdown_bytes(data) # format from content
markdown = anydoc.to_markdown_bytes(data, "csv") # explicit format
document = anydoc.to_document(data) # model + embedded assets
The Python binding releases the GIL during conversion, so a thread pool ingesting a thousand documents actually parallelises. That is not true of most pure-Python parsers, and on a bulk backfill it is the difference between minutes and hours.
Browser (WebAssembly)
import init, { toMarkdownBytes, toDocument } from '@firecrawl/anydoc-wasm';
await init();
const markdown = toMarkdownBytes(bytes);
The WASM build is why Firecrawl’s demo page can convert your files without uploading them. For any product handling confidential contracts or HR documents, client-side conversion is a compliance story worth having.
Format detection that ignores the extension
A quietly excellent design decision: anydoc reads the format from the bytes themselves — the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype — not the filename.
Format::from_bytes(&bytes); // Some(Format::Docx), or None
Format::from_extension("pptm"); // Some(Format::Pptx)
Format::from_path(Path::new("report.odt")); // Some(Format::Odt)
Anyone who has run a user-upload pipeline knows why this matters: users rename .docx to .doc, export tools emit .xls files that are secretly XML, and browsers mangle content types. Mislabeled files still convert correctly. The lone exception is CSV, which has no signature, so it needs the extension or an explicit format.
Error handling built for batch jobs
ConvertError names exactly what went wrong, which lets you distinguish “skip this file” from “abort the run”:
| Variant | Meaning |
|---|---|
Unsupported | Unknown format, or unconvertible (an image-only PDF) |
Malformed | Structurally unusable — no meaningful content extractable |
Encrypted | Encrypted or password-protected |
ResourceLimit | Crossed a fixed safety limit (decompression, nesting, node count) |
MissingPart | A required part is absent |
Io | File could not be read |
match anydoc::to_markdown(path) {
Ok(markdown) => Some(markdown),
Err(error @ (ConvertError::Encrypted | ConvertError::Unsupported(_))) => {
unconverted.push((path, error)); // record and move on
None
}
Err(error) => return Err(error), // genuinely abort
}
The ResourceLimit variant deserves a callout. Zip bombs and deeply nested OOXML are real attack vectors when you accept user uploads, and anydoc enforces fixed decompression, nesting, and node-count limits rather than trusting the file. The repo also carries cargo-fuzz targets per format and mutation tests over every committed fixture — unusually serious hardening for a two-week-old project.
The benchmark, and what to subtract from it
Firecrawl benchmarked anydoc against six converters on 100 real-world documents spanning fourteen formats:
| tool | formats | median ms | score | completeness | structure | formatting | cleanliness |
|---|---|---|---|---|---|---|---|
| anydoc | 14/14 | 4.4 | 81 | 87 | 79 | 78 | 81 |
| libreoffice | 12/14 | 1129.5 | 40 | 59 | 42 | 40 | 24 |
| unstructured | 8/14 | 572.9 | 63 | 76 | 59 | 51 | 63 |
| markitdown | 6/14 | 134.8 | 65 | 78 | 66 | 60 | 52 |
| pandoc | 5/14 | 102.1 | 56 | 74 | 57 | 56 | 38 |
| docling | 4/14 | 513.6 | 57 | 60 | 60 | 57 | 51 |
| mammoth | 1/14 | 52.5 | 70 | 84 | 71 | 75 | 51 |
Per format, like for like, anydoc scored highest on all fourteen — 88 on docx (next best: markitdown at 71), 74 on pptx (markitdown 66), 72 on xlsx (unstructured 66), 88 on rtf (libreoffice 53).
Now the caveats, which the README itself is refreshingly upfront about:
- The vendor ran it. Firecrawl benchmarked its own library. That is not disqualifying — the harness lives in
bench/and is inspectable — but the corpus “is not redistributable and is not in the repo,” so nobody can reproduce the exact numbers. - The judge is an LLM. Quality is scored by Claude Sonnet 5 comparing outputs blind against LibreOffice-rendered page images. They did control for position bias (every pair judged twice with outputs swapped, 482 verdicts total), which is more rigour than most vendor benchmarks bother with. But an LLM judge measures plausible-looking Markdown, not ground truth.
- The rows are not comparable. Each tool’s score averages only the formats it supports. mammoth’s 70 is docx alone; anydoc’s 81 spans all fourteen. The per-format table is the fair comparison, and the README says so.
- Speed excludes process spawn for libraries but includes it for CLIs. Defensible — that is how each is used — but it flatters the library-mode numbers.
Even discounting generously, the coverage claim is the durable one: 14/14 formats in one dependency is a structural advantage no LLM judge can inflate.
Honest limitations
- Zero OCR. Image-only PDFs return
Unsupported. Scanned documents need a vision pipeline; anydoc will not pretend otherwise. Firecrawl’s hosted/parsefills this gap commercially, which is the obvious business motive behind the open-source release. - Spreadsheet number formats are dropped (issue #27): a cell displaying
7.5%converts as0.075. For financial documents that is a silent correctness bug, not a cosmetic one. Validate before trusting spreadsheet output. - No password support for encrypted OOXML (issue #102) — encrypted files terminate at
Encryptedwith no way to supply a key. - Images become alt text only. Raw bytes stay on the document model tagged with media type, but the Markdown itself carries no inline image content. If figures matter, you are writing that handling yourself.
- No Go bindings yet (issue #71 proposes pure-Go via wazero).
- Version 0.1.9 with 74 open issues. Two weeks old. The API surface is small and stable-looking, but pin your version.
How it compares
| anydoc | MarkItDown | Docling | Unstructured | |
|---|---|---|---|---|
| Language | Rust | Python | Python | Python |
| License | MIT | MIT | MIT | Apache-2.0 |
| Median speed | 4.4ms | 134.8ms | 513.6ms | 572.9ms |
| OCR | ❌ | ✅ via plugin | ✅ built-in | ✅ built-in |
| Audio/images/YouTube | ❌ | ✅ | ❌ | ❌ |
| Layout models | ❌ none | ❌ | ✅ | ✅ |
| Browser/WASM | ✅ | ❌ | ❌ | ❌ |
Choose anydoc when you need deterministic, fast, CPU-only conversion of structurally-readable office documents at volume — and especially when you want it inside a Node or browser runtime.
Choose MarkItDown when your inputs include audio, images, or YouTube URLs, or when you are already deep in Python and 135ms is irrelevant to you.
Choose Docling or Unstructured when layout understanding genuinely matters — scientific papers with two-column layouts, forms, scanned archives. These carry ML models for a reason.
The realistic production answer is a router, not a winner: anydoc (with pdf-inspector) handles everything structurally readable in single-digit milliseconds, and only the residue — genuinely scanned pages — goes to an expensive vision pipeline. Firecrawl reports that routing makes its own Fire-PDF engine 3.5–5× faster; on a 200-page report where 150 pages are pure text, 150 pages skip the GPU entirely.
FAQ
Is anydoc free for commercial use?
Yes. MIT license, no attribution obligations beyond the license text, no AGPL strings. Firecrawl monetises the hosted /parse API, not the library.
Does anydoc handle scanned PDFs?
No. Text-based PDFs convert locally through pdf-inspector. Image-only PDFs return ConvertError::Unsupported — pair it with an OCR or vision service for those.
Can I use anydoc from Go, Java, or Ruby?
Not with official bindings today. Rust, Node.js, Python, and WASM ship first-party. Issue #71 tracks pure-Go bindings via wasm32-wasip1 + wazero. Everyone else shells out to the CLI, which is fast enough that process spawn dominates.
How does anydoc compare to pdf-inspector — do I need both? Just anydoc. It depends on pdf-inspector internally and routes PDFs through it automatically. Install pdf-inspector directly only if you specifically want per-page OCR-classification metadata rather than Markdown.
Is the 4.4ms number real?
Median, one warm conversion per document, process spawn excluded, on a Ryzen 9 9950X3D. Your cold-start npx invocation will not hit it. But even at 10× the claim, it is an order of magnitude ahead of the Python alternatives.
Can I run it in the browser?
Yes — @firecrawl/anydoc-wasm runs conversion client-side, so files never leave the user’s machine. Firecrawl’s demo page is the reference implementation.
Verdict
anydoc is a narrow tool that does its narrow thing extremely well. It will not read your scanned invoices, it drops spreadsheet number formatting today, and its headline benchmark is vendor-run and LLM-judged. But 14 formats, one dependency, one consistent Markdown output, four runtimes, MIT is a genuinely useful shape that nothing else in this space currently has — and the fuzzing, mutation tests, and resource limits suggest people who have run this in production against hostile input.
If you maintain a document ingestion pipeline held together by four parsers and a LibreOffice subprocess, spend an afternoon pointing anydoc at your real corpus. Check the spreadsheets carefully. Pin the version.
Sources
- firecrawl/anydoc on GitHub — README, benchmark methodology, format table, error variants
- Introducing AnyDoc and pdf-inspector — Firecrawl’s launch post
- anydoc browser demo — WebAssembly conversion, runs locally
- anydoc releases — v0.1.9, August 13, 2026