Documents and media

PDF to Markdown for RAG: Headings, Tables, Chunks and OCR

Retrieval pipelines work best on text that keeps a document's structure: headings at the right level, tables as tables, and page numbers you can cite. This Actor turns document URLs into that kind of Markdown and can split it into heading-aware chunks ready to embed.

Open the Actor on Apify Store

PDF to Markdown for RAG — DOCX, OCR & Chunks. Pay per result; see the Store page for current pricing.

The source and its limits

The input is any public document URL: PDF, DOCX, HTML, Markdown or plain text, plus kvs://<storeId>/<key> references to files in your own Apify key-value stores and data: URLs. Conversion runs inside the Actor with open-source libraries (pdf.js, mammoth, turndown and tesseract.js). There is no browser and no external conversion service; the Actor contacts only the URLs you give it and your optional webhook.

For PDFs it rebuilds the layout from the file's own text layer: headings from font-size and bold clusters, tables from column alignment, two-column pages read column by column, and running headers, footers and page numbers stripped. Each document becomes one record with the Markdown and its metadata (title, author, producer, creation date, page count, headings, tables and links).

Chunks and OCR

  • chunk: true adds one chunk record per heading-aware chunk, with startPage, endPage, the headingPath it sits under and a token estimate. Chunk size and overlap are configurable.
  • ocr: true runs the bundled English OCR model on PDF pages that have no text layer. Without it, those pages are listed in needsOcrPages and marked in the Markdown, and nothing is recognized.

Limits worth knowing

  • Files up to 25 MB by default (maxFileSizeMb, at most 50), a 20-second download timeout by default, and 10 minutes of conversion per file. After that, the pages converted so far are returned with a PROCESSING_TIMEOUT warning.
  • Layout reconstruction is heuristic. Complex forms, rotated text, equations and multi-line table cells are best effort, and images are not extracted.
  • OCR is English only and runs at about two to five seconds per page.
  • Password-protected, corrupt or unsupported files return an error item with a clear code instead of failing the run.

Fields you get

FieldWhat it holds
recordTypedocument, or chunk when chunking is on
url, filename, mimeType, bytesThe source file
pages, pagesProcessedPage count and pages converted
title, author, producer, creationDateDocument metadata, where the file has it
markdownThe converted text, with <!-- page N --> markers unless pageBreaks is off
characters, words, headings, tables, linksSize and structure of the result
needsOcrPages, ocrPages, warningsPages without a text layer, pages recognized by OCR, and conversion notes
chunkIndex, chunkCount, startPage, endPage, headingPath, tokensEstimateOn chunk records
kvsKeyThe .md file key when saveToKeyValueStore is on

Example output

One chunk record from the "Attention Is All You Need" paper on arXiv (from the Actor's README; Markdown shortened):

{
  "recordType": "chunk",
  "url": "https://arxiv.org/pdf/1706.03762v7",
  "filename": "1706.03762v7.pdf",
  "chunkIndex": 4,
  "chunkCount": 17,
  "startPage": 3,
  "endPage": 4,
  "headingPath": [
    "Attention Is All You Need",
    "3 Model Architecture",
    "3.2 Attention"
  ],
  "markdown": "### 3.2 Attention\n\nAn attention function can be described as mapping a query and a set of key-value pairs to an output ...",
  "tokensEstimate": 572,
  "characters": 2287,
  "fetchedAt": "2026-10-16T13:20:00.000Z"
}

Use cases

RAG ingestion

Turn on chunk and send the chunk records to your embedding step. startPage, endPage and headingPath let answers cite where a passage came from.

Mixed document folders

Send PDFs, DOCX files and HTML pages in one run and get the same record shape back for each. saveToKeyValueStore also writes each one as a .md file.

Scanned PDFs

Turn on ocr for scans. The Markdown marks which pages were recognized, so you can review them separately.

How to run it

You need an Apify account and its API token (Console → Settings → API & Integrations). Each run is billed per result to your account; set a maximum total charge for the run to cap spend.

With curl

The sync endpoint starts a run, waits up to 300 seconds and returns the dataset items. One arXiv paper as Markdown plus 800-token chunks with an 80-token overlap:

export APIFY_TOKEN=<your Apify API token>

curl -sS -X POST \
  "https://api.apify.com/v2/acts/owlabee~pdf-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer ${APIFY_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "urls": [
    "https://arxiv.org/pdf/1706.03762v7"
  ],
  "chunk": true,
  "chunkTokens": 800,
  "chunkOverlapTokens": 80,
  "ocr": false,
  "saveToKeyValueStore": false,
  "pageBreaks": true,
  "maxFileSizeMb": 25,
  "downloadTimeoutSecs": 20
}'

From an AI agent (MCP)

Apify hosts an MCP server. This config exposes only this Actor to the agent:

{
  "mcpServers": {
    "owlabee-pdf-to-markdown": {
      "url": "https://mcp.apify.com/?tools=owlabee/pdf-to-markdown",
      "headers": {
        "Authorization": "Bearer ${APIFY_TOKEN}"
      }
    }
  }
}

Then ask, for example: Convert this PDF to Markdown and split it into chunks of about 800 tokens, keeping the page numbers and section headings for each chunk.

Python, Node.js and more examples

Runnable curl.sh, python.py, node.mjs and mcp.json files for this Actor are in examples/pdf-to-markdown on GitHub.

FAQ

Does it send my documents to another service?

No. Files are downloaded over HTTPS and converted inside the Actor. Only the URLs you give and your optional webhook are contacted.

How do chunks reference pages?

Each chunk carries startPage and endPage from the page markers, and headingPath, the headings it sits under.

What happens with a scanned PDF?

Pages with no text layer are listed in needsOcrPages and marked in the Markdown. Turn on ocr to have them recognized; they then appear in ocrPages.

How are DOCX and HTML pages counted?

DOCX files use their saved page count. When that is missing, and for HTML, Markdown and text, pages are estimated at one per roughly 3,000 characters (pagesEstimated: true).

I need specific fields, not Markdown.

Use the document to JSON extractor, which runs the same layout engine against a schema you define.