# Documents to Markdown for LLMs: PDF, Office, HTML, OCR (`rado-dev/documents-to-llm-markdown`) Actor

Convert PDF, Word, PowerPoint, Excel, HTML, text and scanned documents into clean Markdown with tables, token counts and RAG-ready chunks with page numbers. Tesseract OCR for scans. Failed documents are free.

- **URL**: https://apify.com/rado-dev/documents-to-llm-markdown.md
- **Developed by:** [RADO](https://apify.com/rado-dev) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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

## Documents to Markdown for LLMs

Turn PDF, Word, PowerPoint, Excel, HTML and scanned documents into clean Markdown (or plain text) that you can feed to an LLM, a vector database or a RAG pipeline. Each document comes back with its title, page count, table count and token estimate. You can also have it split into chunks sized in tokens, with the section heading and page numbers attached to each chunk.

It is built for batch jobs and AI agents: give it a list of URLs and get one structured dataset item per document, plus one item per chunk if you turn chunking on.

### What it does

- **PDF with a text layer.** Text is read in reading order, including two-column layouts. Headings are detected from font size and weight, and bullet lists are kept. Ruled tables become Markdown tables. Bare page numbers and rotated margin stamps are removed.
- **Scanned PDFs and images (PNG, JPG, TIFF, WebP, BMP, GIF).** OCR runs with Tesseract. In `auto` mode only pages without a text layer are OCR'd, so a mixed PDF is handled page by page.
- **DOCX, PPTX, XLSX, HTML, CSV, JSON, XML, TXT, MD.** Headings, lists, tables, slide titles and speaker notes are kept. Each spreadsheet sheet becomes a Markdown table.
- **Chunking for RAG.** Chunks follow section boundaries. A table is only split if it is larger than the chunk size, and then the header row is repeated in every part. Each chunk carries `headingPath`, `pageStart` and `pageEnd`. An optional token overlap is added when a long section has to be split.
- **Clear errors.** Password-protected, corrupt, empty and unsupported files each get a stable error code. Nothing is charged for them.

### Input

| Field | Default | Notes |
|---|---|---|
| `sources` | – | List of `{ "url": "https://..." }` direct file links |
| `keyValueStore` | – | Optional: pick one of your key-value stores holding the files (read-only access to that store only) |
| `keyValueStoreKeys` | `[]` | Keys to convert from that store; empty = all records (except INPUT/OUTPUT) |
| `outputFormat` | `markdown` | `markdown` or `text` |
| `ocr` | `auto` | `auto` (scanned pages only), `always`, `never` |
| `ocrLanguages` | `["eng"]` | Any of eng, pol, deu, fra, spa, ita, por, nld, ces, ukr |
| `chunkingEnabled` | `false` | Also output chunk items |
| `chunkMaxTokens` / `chunkOverlapTokens` | `800` / `80` | Counted with the cl100k_base tokenizer |
| `includeContentWithChunks` | `true` | Keep full text on the document item as well |
| `includeTables` | `true` | Convert tables to Markdown tables |
| `includePageMarkers` | `false` | Insert `<!-- page: N -->` before each page/slide |
| `maxPagesPerDoc` | `300` | Pages beyond this are skipped and not billed |
| `maxFileSizeMb` | `50` | Larger files are rejected without charge |
| `docTimeoutSecs` | `600` | Slow documents are stopped and reported (not billed) |
| `maxConcurrency` | `2` | Parallel documents; more helps only with more memory |

Default memory is 1 GB. For long documents (100+ pages) or OCR-heavy batches you can raise it to 4 GB (1 full CPU core) for faster runs. Compute is included in the per-event price, so the memory setting does not change what you pay.

Example:

```json
{
  "sources": [
    { "url": "https://arxiv.org/pdf/1706.03762v7" },
    { "url": "https://example.com/contract.docx" }
  ],
  "ocr": "auto",
  "ocrLanguages": ["eng", "deu"],
  "chunkingEnabled": true,
  "chunkMaxTokens": 800
}
```

### Output

The dataset has one `document` item per source. If chunking is on, it also has one `chunk` item per chunk. Use the **Documents** and **Chunks** views in the Output tab, or filter by `recordType`.

```json
{
  "recordType": "document",
  "docId": "349f7d2cc5034659",
  "sourceUrl": "https://example.com/report.pdf",
  "status": "ok",
  "fileType": "pdf",
  "title": "Quarterly report",
  "pages": 1,
  "billedPageUnits": 1,
  "ocrPages": 0,
  "tables": 1,
  "contentFormat": "markdown",
  "content": "# Quarterly report\n\nThe quick brown fox ...\n\n| Name | Qty | Price |\n|---|---|---|\n| Item 1 | 3 | 1.50 PLN |",
  "wordCount": 247,
  "tokenEstimate": 368,
  "chunkCount": 4,
  "warnings": []
}
```

```json
{
  "recordType": "chunk",
  "docId": "349f7d2cc5034659",
  "chunkIndex": 1,
  "chunkCount": 4,
  "headingPath": ["Quarterly report", "Section A"],
  "pageStart": 1,
  "pageEnd": 1,
  "tokens": 93,
  "text": "## Section A\n\nThe quick brown fox ..."
}
```

A failed document looks like this, and is not charged:

```json
{ "recordType": "document", "sourceUrl": "https://example.com/protected.pdf", "status": "error",
  "error": { "code": "PASSWORD_PROTECTED", "message": "The PDF is password-protected. Remove the password and try again." } }
```

Error codes: `EMPTY_FILE`, `CORRUPT_FILE`, `PASSWORD_PROTECTED`, `UNSUPPORTED_TYPE`, `FILE_TOO_LARGE`, `HTTP_<status>`, `DOWNLOAD_FAILED`, `NOT_FOUND`, `INVALID_SOURCE`, `TIMEOUT`, `OUT_OF_MEMORY`, `OCR_DISABLED`, `OCR_FAILED`, `SPENDING_LIMIT_REACHED`, `CONVERSION_FAILED`.

Warnings (the document is still delivered): `PAGE_LIMIT`, `SPENDING_LIMIT`, `NO_TEXT_PAGES`, `NO_TEXT`, `PAGE_ERROR`.

If the content is larger than about 4 MB, the full text is saved to the run's key-value store. The item then gets `contentTruncated: true` and a `contentUrl`. A run summary is saved as `OUTPUT` in the key-value store.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| Document converted | $0.002 | Once per document that produced text |
| Page converted | $0.0005 | Per PDF page with a text layer or per slide. For DOCX/HTML/XLSX/TXT and similar formats, per started 3,000 characters of output |
| Page converted with OCR | $0.008 | Per scanned page or image where OCR found text. Charged instead of the page event, not on top of it |

Examples: a 20-page text PDF costs $0.012. A 10-page scan costs $0.082. A short Word document costs $0.0025. Failed documents, empty pages and pages above your limits are free.

The run never charges more than the maximum cost per run that you set. When the budget runs low, the Actor converts only the pages it can still bill, adds a `SPENDING_LIMIT` warning and skips the remaining sources.

### Limitations (please read)

- **Tables without ruling lines** in PDFs (common in academic papers) usually come out as plain text, not as Markdown tables. Ruled tables, and all tables in DOCX/XLSX/HTML/PPTX, are converted.
- **Math formulas, charts and figures** are not reconstructed. Formulas come out as approximate text, and images inside PDF/DOCX are not OCR'd. Only scanned pages and standalone images are.
- **OCR quality** depends on the scan. Clean 200–300 dpi scans work well. Photos of documents, handwriting and very small print do not. Choose only the languages you need, because each extra language slows OCR down.
- **Legacy formats** (.doc, .xls, .ppt, .rtf, .odt) and archives (.zip) are not supported.
- **HTML** is converted as a whole page, including navigation and footer text. There is no main-content extraction yet.
- **Password-protected PDFs** are rejected. Passwords are not accepted as input.
- The Actor downloads what you give it. It does not crawl websites or log in anywhere.

### Privacy

Documents are processed inside your own Actor run. The Actor keeps no copy outside your run's storages. The converted text sits in your run's dataset and key-value store, under your account's data retention settings. Only process documents you have the right to process.

### Open-source components

pdfplumber and pdfminer.six (MIT), pypdfium2 (Apache-2.0 / BSD-3-Clause), mammoth (BSD-2-Clause), python-pptx, openpyxl, markdownify and Beautiful Soup (MIT), Tesseract OCR and pytesseract (Apache-2.0), tiktoken (MIT), Apify SDK (Apache-2.0). AGPL-licensed PDF libraries are not used.

### Feedback

Found a document that converts badly? Open an issue in the Issues tab and include a public link to the file if you can. Real failing files are the fastest way to improve the converter.

# Changelog

This Actor's version history is a separate document: https://apify.com/rado-dev/documents-to-llm-markdown/changelog.md

# Actor input Schema

## `sources` (type: `array`):

Direct links to the files (PDF, DOCX, PPTX, XLSX, HTML, TXT, MD, CSV, JSON, XML, PNG/JPG/TIFF). Each source is converted separately.

## `keyValueStore` (type: `string`):

Optional. Pick one of your key-value stores that contains the documents (e.g. files you uploaded or another Actor produced). The Actor gets read-only access to this store only.

## `keyValueStoreKeys` (type: `array`):

Keys of the records to convert from the selected store. Leave empty to convert every record in the store (except INPUT/OUTPUT), up to 'Max documents per run'.

## `outputFormat` (type: `string`):

Markdown keeps headings, lists and tables. Plain text strips Markdown syntax.

## `ocr` (type: `string`):

auto: OCR only PDF pages that have no text layer (scans) and images. always: OCR every page (slower, billed as OCR pages). never: no OCR, scanned pages come back empty.

## `ocrLanguages` (type: `array`):

Languages present in the scanned documents. Fewer languages = faster and more accurate OCR.

## `chunkingEnabled` (type: `boolean`):

Also output token-sized chunks (one dataset item per chunk) that respect headings, tables and pages.

## `chunkMaxTokens` (type: `integer`):

Counted with the cl100k_base tokenizer (OpenAI GPT-4 family); other models' tokenizers differ slightly.

## `chunkOverlapTokens` (type: `integer`):

Tokens repeated from the end of the previous chunk when a section has to be split.

## `includeContentWithChunks` (type: `boolean`):

If off, document items carry only metadata and the text is delivered in chunk items.

## `includeTables` (type: `boolean`):

Detect tables and output them as Markdown tables.

## `includePageMarkers` (type: `boolean`):

Insert  comments before each PDF page / slide.

## `maxPagesPerDoc` (type: `integer`):

Pages beyond this limit are skipped (and not billed). For non-paginated formats one page = 3,000 characters.

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

Bigger files are rejected without charge.

## `docTimeoutSecs` (type: `integer`):

A document that takes longer is stopped and reported as an error (not billed).

## `maxConcurrency` (type: `integer`):

Documents converted at the same time. More only helps with more memory (Apify gives 1 CPU core per 4 GB).

## `maxDocuments` (type: `integer`):

Safety cap on the number of sources processed.

## Actor input object example

```json
{
  "sources": [
    {
      "url": "https://arxiv.org/pdf/1706.03762v7"
    }
  ],
  "keyValueStoreKeys": [],
  "outputFormat": "markdown",
  "ocr": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "chunkingEnabled": false,
  "chunkMaxTokens": 800,
  "chunkOverlapTokens": 80,
  "includeContentWithChunks": true,
  "includeTables": true,
  "includePageMarkers": false,
  "maxPagesPerDoc": 300,
  "maxFileSizeMb": 50,
  "docTimeoutSecs": 600,
  "maxConcurrency": 2,
  "maxDocuments": 1000
}
```

# Actor output Schema

## `documents` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "sources": [
        {
            "url": "https://arxiv.org/pdf/1706.03762v7"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rado-dev/documents-to-llm-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 = { "sources": [{ "url": "https://arxiv.org/pdf/1706.03762v7" }] }

# Run the Actor and wait for it to finish
run = client.actor("rado-dev/documents-to-llm-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 '{
  "sources": [
    {
      "url": "https://arxiv.org/pdf/1706.03762v7"
    }
  ]
}' |
apify call rado-dev/documents-to-llm-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rado-dev/documents-to-llm-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/d2ng0C7YsHHrjfG19/builds/vyV278mGbyflkCn08/openapi.json
