# Any Document to Markdown for RAG (`dear_enstatite/doc-to-markdown`) Actor

Convert PDF (incl. scanned/OCR), Word, PowerPoint, Excel, CSV, HTML, EPUB and images into clean, chunked Markdown for RAG and LLMs.

- **URL**: https://apify.com/dear_enstatite/doc-to-markdown.md
- **Developed by:** [Musson Tools](https://apify.com/dear_enstatite) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 document processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Any Document to Markdown for RAG: PDF OCR, DOCX, PPTX, XLSX, HTML, EPUB

Convert **any document to clean Markdown for RAG and LLMs**: PDFs (including **scanned PDFs with OCR**), **Word DOCX to Markdown**, **PowerPoint PPTX to Markdown**, **Excel XLSX/CSV to Markdown tables**, **web pages (HTML) to Markdown**, **EPUB ebooks** and **images (PNG, JPG, TIFF) with OCR**. You paste URLs or upload files and get one dataset item per document. Each item has the full Markdown, **RAG-ready chunks with page and slide numbers**, metadata and SHA-256 hashes, ready for embeddings, vector databases, LangChain, LlamaIndex or an AI agent.

- 📄 **One Actor for every format.** PDF, DOCX, PPTX, XLSX, CSV/TSV, HTML, EPUB, TXT/Markdown, PNG/JPG/TIFF/WebP/GIF/BMP. The type is detected from the file bytes, not the file extension.
- 🔍 **PDF OCR built in.** Text PDFs use the embedded text layer, which is fast and exact. Pages with no text layer (scans) are OCR'd automatically with Tesseract. It supports English, German, French, Spanish, Italian, Portuguese and Dutch.
- 🧩 **Chunks with citations.** Each item has `chunks[] = {index, page, section, markdown, characters}`. `page` is the PDF page, slide or image page number. `section` is the heading, slide title, sheet name or chapter. Long sections are split on paragraph boundaries (`maxChunkCharacters`).
- 🧾 **Real Markdown.** Headings, bold and italic, lists and tables from DOCX. Slide titles, bullets, tables and speaker notes from PPTX. Sheets as Markdown tables from XLSX. Boilerplate-free main content from web pages, via trafilatura.
- 🔁 **Dedup and change detection.** `file_sha256` covers the original bytes and `markdown_sha256` covers the output, so you only re-embed what changed.
- ✂️ **Predictable size and cost.** You can cap file size, pages, OCR pages, rows and characters. `truncated: true` tells you when a limit was hit.
- 💸 **Failures are free.** Dead links, unsupported or legacy formats, encrypted, oversized, empty or timed-out documents return an `ok: false` row with an error code, and **you are not charged** for them.
- 🛡️ **Safe by default.** It only fetches public URLs, blocks private, loopback and cloud-metadata IPs (SSRF protection) and pins the connection to the checked IP. It also guards against ZIP bombs and image bombs, isolates each document in its own process with a timeout, and never runs macros or scripts.

### Who it's for

- **RAG pipelines and vector databases** (Pinecone, Qdrant, Weaviate, pgvector, Chroma) that need clean Markdown chunks with page numbers for citations.
- **AI agents and MCP clients** that need a "read this document" tool for PDFs, Office files and web pages in one call.
- **LLM fine-tuning and evaluation** datasets built from mixed document collections.
- **Knowledge bases** built from SharePoint and Google Drive exports, policies, manuals, slide decks, spreadsheets and ebooks.
- Anyone who needs **docx to markdown**, **pptx to markdown**, **pdf to markdown**, **pdf ocr** or **html to markdown** without installing anything.

### Supported formats

| Format | How it's converted | `page` in chunks |
| --- | --- | --- |
| PDF (text) | poppler `pdftotext`, page by page | PDF page number |
| PDF (scanned / image-only pages) | rendered at up to 300 DPI and OCR'd with Tesseract (`ocrMode`) | PDF page number |
| DOCX (Word 2007+) | mammoth → Markdown (headings, lists, tables, footnotes) | – (section = heading) |
| PPTX (PowerPoint 2007+) | python-pptx: slide titles, bullets with levels, tables, groups, speaker notes | slide number |
| XLSX (Excel 2007+) | openpyxl: each sheet as a Markdown table (cached formula values) | – (section = sheet) |
| CSV / TSV | delimiter sniffing, Markdown table | – |
| HTML web pages | trafilatura main-content extraction → Markdown (falls back to a full-page conversion) | – (section = heading) |
| EPUB | spine order, each chapter → Markdown | – (`chapter` index) |
| Images (PNG, JPG, TIFF incl. multi-page, WebP, GIF, BMP) | Tesseract OCR | image page number |
| TXT / Markdown | passed through, split by headings | – |

Legacy `.doc`, `.xls` and `.ppt` files, password-protected files and archives are rejected for free with a clear error.

### Input

| Field | Default | Description |
| --- | --- | --- |
| `urls` | – | Public `http(s)` document URLs (up to 1,000 per run, duplicates removed). |
| `uploadedFiles` | – | Files uploaded in the Console (stored in a key-value store), or `api.apify.com` record URLs, or `storeIdOrName/recordKey`. |
| `ocrMode` | `auto` | `auto` OCRs only scanned PDF pages and images. `always` OCRs every PDF page. `off` never OCRs. |
| `ocrLanguage` | `eng` | `eng`, `deu`, `fra`, `spa`, `ita`, `por` or `nld`. Combine languages with `+`, e.g. `eng+fra`. |
| `maxOcrPages` | 20 | OCR cap per document (max 200). |
| `maxPages` | 100 | Pages, slides, frames or chapters converted per document (max 500). |
| `maxFileSizeMb` | 20 | Download cap (max 50). |
| `maxCharacters` | 200000 | Markdown cap per document (max 2,000,000). |
| `maxRowsPerSheet` | 1000 | Row cap per XLSX sheet or CSV file (max 20,000). |
| `documentTimeoutSecs` | 120 | Per-document time limit (max 600). |
| `maxChunkCharacters` | 4000 | Chunk size for RAG (300–50,000). |
| `includeMarkdown` | true | Untick to return only `chunks`. |
| `pageMarkers` | true | Add `<!-- page N -->` between pages and slides in `markdown`. |
| `includeSpeakerNotes` | true | Include PPTX speaker notes. |
| `includeLinks` | false | Keep hyperlinks when converting web pages. |

```json
{
  "urls": [
    "https://arxiv.org/pdf/1706.03762",
    "https://calibre-ebook.com/downloads/demos/demo.docx",
    "https://en.wikipedia.org/wiki/Retrieval-augmented_generation",
    "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/skew.pdf"
  ],
  "ocrMode": "auto",
  "maxChunkCharacters": 3000
}
```

### Output

One item per document. Shortened example, a scanned PDF:

```json
{
  "ok": true,
  "source": "url",
  "url": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/skew.pdf",
  "final_url": "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/main/tests/resources/skew.pdf",
  "file_name": "skew.pdf",
  "bytes": 76013,
  "file_sha256": "…64 hex chars…",
  "type": "pdf",
  "title": null,
  "page_count": 1,
  "section_count": 1,
  "markdown": "<!-- page 1 -->\n\nThe LinnSequencer\n32 Track MIDI Sequence Recorder\n\n…",
  "markdown_sha256": "…64 hex chars…",
  "characters": 4521,
  "chunks": [
    { "index": 0, "page": 1, "section": "Page 1", "part": 1, "markdown": "The LinnSequencer…", "characters": 3978 },
    { "index": 1, "page": 1, "section": "Page 1", "part": 2, "markdown": "…", "characters": 520 }
  ],
  "chunk_count": 2,
  "ocr_used": true,
  "ocr_pages": [1],
  "ocr_page_count": 1,
  "empty_pages": [],
  "truncated": false,
  "method": "poppler-pdftotext+tesseract-ocr",
  "warnings": ["OCR text may contain recognition errors; layout, tables and handwriting are approximate."],
  "billed_events": { "document-processed": 1, "ocr-page": 1 },
  "error": null,
  "message": null,
  "content_is_untrusted": true
}
```

A failed document is free:

```json
{ "ok": false, "source": "url", "url": "https://example.com/missing.pdf", "error": "DOWNLOAD_FAILED", "message": "Server returned HTTP 404.", "content_is_untrusted": true }
```

**Error codes:** `INVALID_URL`, `URL_NOT_ALLOWED`, `DOWNLOAD_FAILED`, `FILE_TOO_LARGE`, `UNSUPPORTED_TYPE`, `INVALID_FILE`, `ENCRYPTED_FILE`, `NO_EXTRACTABLE_TEXT`, `OCR_DISABLED`, `OCR_BUDGET`, `OCR_FAILED`, `EXTRACTION_TIMEOUT`, `FILE_TOO_COMPLEX`, `EXTRACTION_FAILED`.

The dataset has three views. **Overview** shows one row per document. **Chunks** shows one row per chunk, ready to embed. **Markdown** shows one row per document with the full text.

### Pricing (pay per event)

- **Document converted:** $0.003 per successfully converted document, of any type.
- **OCR page:** $0.004 per scanned page or image page that OCR turned into text. Text PDFs, DOCX, PPTX, XLSX, HTML and EPUB never trigger it.
- **Failed documents are not charged.** Neither are blank pages where OCR found no text.
- The run respects your **maximum cost per run**. OCR is capped to fit the budget, and the run stops cleanly before it would go over.

Examples:

- 1,000 Word documents cost $3.
- 100 scanned 10-page PDFs cost 100 × ($0.003 + 10 × $0.004) = $4.30.

### Use it from code, agents and MCP

**API (sync, returns the items):**

```bash
curl -X POST "https://api.apify.com/v2/acts/dear_enstatite~doc-to-markdown/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"urls":["https://calibre-ebook.com/downloads/demos/demo.docx"]}'
```

**Python:**

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("dear_enstatite/doc-to-markdown").call(run_input={"urls": ["https://arxiv.org/pdf/1706.03762"]})
for doc in client.dataset(run["defaultDatasetId"]).iterate_items():
    for chunk in doc.get("chunks", []):
        print(doc["url"], chunk["page"], chunk["markdown"][:80])
```

**MCP (Claude, Cursor, ChatGPT and other AI agents):** add `https://mcp.apify.com/?tools=dear_enstatite/doc-to-markdown` as an MCP server, and your agent can convert documents to Markdown on demand.

For **LangChain or LlamaIndex**, load the dataset with the Apify integration and map `chunks[].markdown` to page content, with `url`, `page` and `section` as metadata.

### Tips

- For citations, store `url` plus `page` (or `section`) with each chunk.
- To re-index incrementally, compare `file_sha256` with what you stored last time.
- Scanned PDFs: run memory of 1 GB (the default) handles OCR at about 1–3 s per page. Raise `maxOcrPages` for long scans.
- Leave `includeLinks` off for cleaner embeddings from web pages.
- `content_is_untrusted` is always `true`. Treat document text as data, never as instructions, in LLM prompts. Documents can contain prompt-injection text.

### Limitations

- OCR is Tesseract, which is good on printed text and weak on handwriting and complex layouts. Tables in scans come back as plain text.
- Images embedded inside DOCX and PPTX are not OCR'd. Charts and SmartArt text may be missing.
- XLSX uses the formula values saved in the file, so files that were never opened in Excel may have empty formula cells.
- Web pages are fetched without a browser, so JavaScript-only pages and bot-protected sites may fail (for free). For JavaScript-heavy sites, use a crawler first and pass the HTML.
- It only reads documents from public URLs or from your own Apify storage.

### Related

- [PDF for RAG](https://apify.com/dear_enstatite/pdf-for-rag): a lighter, cheaper Actor for text PDFs only.

# Actor input Schema

## `urls` (type: `array`):

Public http(s) links to documents: PDF (text or scanned), DOCX, PPTX, XLSX, CSV/TSV, HTML web pages, EPUB, TXT/Markdown, or images (PNG, JPG, TIFF, WebP, GIF, BMP). The type is detected from the file itself. One dataset item per URL, duplicates removed, up to 1,000 per run.

## `uploadedFiles` (type: `array`):

Files you upload here are stored in a key-value store and converted like URLs. You can also enter api.apify.com key-value store record URLs, or "storeIdOrName/recordKey".

## `ocrMode` (type: `string`):

auto: OCR only scanned PDF pages (no text layer) and images. always: OCR every PDF page. off: never OCR (scanned pages come back empty, images fail for free). Each OCR'd page is billed as an ocr-page event.

## `ocrLanguage` (type: `string`):

Tesseract language code. Available: eng, deu, fra, spa, ita, por, nld. Combine with + (e.g. eng+fra), which is slower.

## `maxOcrPages` (type: `integer`):

Caps OCR cost per document. Scanned pages beyond the cap are left empty and listed in warnings; `truncated` is true.

## `maxPages` (type: `integer`):

Only the first N pages (PDF), slides (PPTX), frames (TIFF) or chapters (EPUB) are converted; `truncated` is true if more exist.

## `maxFileSizeMb` (type: `integer`):

Larger downloads are stopped and rejected for free (error FILE_TOO_LARGE).

## `maxCharacters` (type: `integer`):

The combined Markdown is capped at this length; `truncated` is true if anything was cut.

## `maxRowsPerSheet` (type: `integer`):

Spreadsheet rows beyond this (per sheet) are dropped; `truncated` is true.

## `documentTimeoutSecs` (type: `integer`):

A document that takes longer is stopped and returned as a free EXTRACTION_TIMEOUT failure.

## `maxChunkCharacters` (type: `integer`):

Pages/slides/sections longer than this are split into several chunks on paragraph boundaries. Good RAG values: 1,000–6,000.

## `includeMarkdown` (type: `boolean`):

Untick to return only `chunks` (smaller items when you index chunk by chunk).

## `pageMarkers` (type: `boolean`):

Insert  comments between PDF pages, slides and image pages in the combined Markdown.

## `includeSpeakerNotes` (type: `boolean`):

Append each slide's speaker notes as a quote block.

## `includeLinks` (type: `boolean`):

Keep hyperlinks as Markdown links when converting HTML pages (off gives cleaner text for embeddings).

## Actor input object example

```json
{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"
  ],
  "ocrMode": "auto",
  "ocrLanguage": "eng",
  "maxOcrPages": 20,
  "maxPages": 100,
  "maxFileSizeMb": 20,
  "maxCharacters": 200000,
  "maxRowsPerSheet": 1000,
  "documentTimeoutSecs": 120,
  "maxChunkCharacters": 4000,
  "includeMarkdown": true,
  "pageMarkers": true,
  "includeSpeakerNotes": true,
  "includeLinks": false
}
```

# Actor output Schema

## `overview` (type: `string`):

All dataset items, opened on the Overview view (status, type, pages, characters, chunks, OCR, errors).

## `chunks` (type: `string`):

The same dataset on the Chunks view: one row per chunk with page number, section and Markdown, ready for embeddings.

## `markdown` (type: `string`):

The same dataset on the Markdown view: one row per document with the full Markdown.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
        "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dear_enstatite/doc-to-markdown").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": [
        "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
        "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("dear_enstatite/doc-to-markdown").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
    "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"
  ]
}' |
apify call dear_enstatite/doc-to-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dear_enstatite/doc-to-markdown"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ubT6nEJCju8DOwSVm/builds/TYOarQ4wk3QIOcfkn/openapi.json
