# PDF to Markdown (RAG-ready): Scans + Tables (`copy2paste/pdf-to-markdown-rag`) Actor

Scanned PDFs and messy tables actually convert here. Bundled OCR reads image-only pages; tables come out as real Markdown tables. Benchmarked against the leading alternatives — results in the README.

- **URL**: https://apify.com/copy2paste/pdf-to-markdown-rag.md
- **Developed by:** [Dermot O'Brien](https://apify.com/copy2paste) (community)
- **Categories:** AI
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 text page converteds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PDF to Markdown (RAG-ready)

**Turn any PDF — text-native or scanned — into clean Markdown for RAG pipelines, LLM context, and AI agents.**

Most PDF converters stop at plain text and lose table structure on the way, and plenty return nothing at all for scanned pages. This one runs OCR on scanned PDFs, recovers tables as real GFM Markdown tables, and outputs a manifest and page records per document, so one bad document in a batch doesn't stop the rest.

### Why this one

- **Scans actually get OCR'd.** Image-only PDFs run through bundled Tesseract OCR instead of coming back empty.
- **Tables stay tables.** GFM table recovery outputs real Markdown tables, not flattened text — see the benchmark below.
- **One bad file doesn't sink the run.** Only pages that convert successfully turn into records, and every document gets its own manifest, so a bad document doesn't stop the rest of the batch.

### Quickstart

Convert a PDF by URL with the defaults (auto-OCR, Markdown only, 200-page cap):

```json
{ "documents": [{ "url": "https://arxiv.org/pdf/2506.22653" }] }
```

Multiple documents with chunked output, ready for a vector DB:

```json
{
  "documents": [
    { "url": "https://example.com/a.pdf" },
    { "key": "UPLOADED_FILE_KEY" }
  ],
  "output": "markdown+chunks",
  "chunking": { "maxTokens": 1024, "overlapTokens": 64 }
}
```

### What you get

Each run produces one dataset record per converted page (`kind: "page"`), one manifest per document (`kind: "document"`) pointing at the full Markdown in the run's key-value store (`fullMarkdownKey`), and an `OUTPUT` record with the run summary. Add `"output": "markdown+chunks"` to also get fixed-window token chunks sized for a vector DB. Trigger it the same way from the Console, the API, Make/n8n, or an MCP-connected agent.

### Drop it into your RAG pipeline

Each `kind: "document"` manifest points at your Markdown and chunks in the run's key-value store, and big values are split across an ordered list of keys (`fullMarkdownParts`, `chunksParts`). Rebuild any value the same way: join the parts' raw bytes in list order, *then* decode or parse — the split can land anywhere, including mid-character, so parsing part-by-part would fail. Here's the whole path from a run to LangChain documents, metadata and all:

```python
import json
from apify_client import ApifyClient
from langchain_core.documents import Document

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("copy2paste/pdf-to-markdown-rag").call(
    run_input={
        "documents": [{"url": "https://arxiv.org/pdf/2506.22653"}],
        "output": "markdown+chunks",
        "chunking": {"maxTokens": 1024, "overlapTokens": 64},
    }
)

kvs = client.key_value_store(run["defaultKeyValueStoreId"])
docs = []

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("kind") != "document" or not item.get("chunksParts"):
        continue

    # Join the parts' bytes in order, THEN parse the chunk array.
    raw = b"".join(kvs.get_record_as_bytes(key)["value"] for key in item["chunksParts"])
    chunks = json.loads(raw.decode("utf-8"))

    for chunk in chunks:
        docs.append(
            Document(
                page_content=chunk["text"],
                metadata={
                    "sourceRef": item["sourceRef"],
                    "headingPath": chunk["headingPath"],
                    "tokenCount": chunk["tokenCount"],
                },
            )
        )

## docs is ready to hand to any LangChain vector store.
```

On LlamaIndex, swap the last loop for `TextNode`s and keep the same metadata:

```python
from llama_index.core.schema import TextNode

nodes = [
    TextNode(text=chunk["text"], metadata={"sourceRef": item["sourceRef"],
             "headingPath": chunk["headingPath"], "tokenCount": chunk["tokenCount"]})
    for chunk in chunks
]
```

To stay in the shell, one synchronous call runs the actor and returns every dataset row as JSON:

```bash
curl -X POST "https://api.apify.com/v2/acts/copy2paste~pdf-to-markdown-rag/run-sync-get-dataset-items?token=$APIFY_TOKEN" -H "Content-Type: application/json" -d '{"documents":[{"url":"https://arxiv.org/pdf/2506.22653"}],"output":"markdown+chunks"}'
```

**Options**

- **OCR beyond English.** `ocrLanguages` takes up to three of 21 Tesseract languages — `eng, deu, fra, spa, ita, por, nld, pol, rus, ces, tur, swe, dan, fin, ell, ukr, jpn, kor, chi_sim, ara, hin` — with the first as the primary, e.g. `["deu", "eng"]` for German scans with English footnotes. CJK and right-to-left quality depends on layout; horizontal text works best.
- **Force OCR on bad text layers.** `ocr: "force"` re-OCRs every page even when a text layer exists, for scans whose embedded text is garbage. It bills at the OCR page rate and skips geometric table extraction, trading table structure for clean text.
- **Blank pages cost nothing.** Empty pages are classified `EMPTY_PAGE`, skipped, and never charged.

### Measured, not claimed

Tested against the three highest-usage runnable PDF-extraction actors on a frozen 20-document public corpus — government reports, CC-BY papers, true image-only scans, table-heavy statistical documents, and pathological files — with table ground truth labeled before any contender ran. Full protocol, corpus manifest, raw outputs, and scoring code are available on request via the Issues tab.

| Contender | Conversion success | Table-cell F1 | Scanned-page OCR |
|---|---|---|---|
| **This actor** | **17/17** | **0.911** | **exact text (0.0 CER)** |
| Best incumbent A | 15/17 | 0.903 | no output |
| Best incumbent B | 15/17 | 0.000 | no output |
| Best incumbent C | 7/17 (timeouts) | 0.000 | exact on completed |

Honest caveat: one incumbent edges ahead on a single dense-table document class (0.941 vs 0.889 on that page), and none of the others can read a scanned page and hold table structure at the same time this actor does.

### Limits and good citizenship

100 documents per run, 100 MB per input, up to 500 pages per document (200 by default), and OCR in up to 3 of 21 languages per run. DOCX, PPTX, XLSX, and HTML input are on the roadmap, not available yet. The actor also respects whatever spending limit you set for the run and stops picking up new work the moment that limit is hit.

### Privacy and security

Your documents live in Actor memory and an isolated temporary directory for the length of the run; that directory is deleted the moment a document finishes processing, and the actor creator keeps no separate copy. Document content never appears in logs, and raw URL queries, fragments, and credentials are never persisted or logged. What does persist is the derived output — page records, Markdown, chunks, manifests, and the run summary — in storage under your own Apify account, governed by your plan, your retention settings, and Apify's DPA. You're responsible for having the right to process whatever documents you submit; this tool isn't offered for regulated data such as HIPAA-class records.

Converted output is untrusted content, not instructions. If you hand it to a downstream agent, treat it as data to read, not as something to execute, and use the source reference, document index, and page number carried in each record for provenance.

This is an independent project, not affiliated with or endorsed by IBM, Docling, or Apify. It's AI-built and AI-operated under the account owner's supervision, with every release passing an automated test suite and cross-model code review before it ships.

# Actor input Schema

## `documents` (type: `array`):

Array of 1–100 items, each exactly one of {"url": "https://..."} or {"key": "NAME\_IN\_RUN\_KV\_STORE"}. URLs must be http(s) without credentials.

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

auto: OCR pages with no extractable text. force: OCR every non-blank page, billing each converted page at the OCR rate and trading geometric table extraction for clean text; intended for scans with corrupt embedded text. off: skip pages with no extractable text with a warning.

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

Select 1–3 OCR languages in priority order (first = primary). Ignored when OCR mode is off. CJK and RTL quality depends on layout; horizontal text works best with the fixed page segmentation mode.

## `output` (type: `string`):

Store Markdown only, or Markdown plus deterministic token chunks for RAG.

## `chunking` (type: `object`):

Only valid with output = markdown+chunks. {"maxTokens": 128–4096 (default 1024), "overlapTokens": 0–512 (default 64), overlap < maxTokens/2}.

## `maxPagesPerDocument` (type: `integer`):

Convert at most this many pages from each PDF (1–500).

## Actor input object example

```json
{
  "documents": [
    {
      "url": "https://arxiv.org/pdf/2506.22653"
    }
  ],
  "ocr": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "output": "markdown",
  "maxPagesPerDocument": 200
}
```

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

// Run the Actor and wait for it to finish
const run = await client.actor("copy2paste/pdf-to-markdown-rag").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 = { "documents": [{ "url": "https://arxiv.org/pdf/2506.22653" }] }

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

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

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

```

## CLI example

```bash
echo '{
  "documents": [
    {
      "url": "https://arxiv.org/pdf/2506.22653"
    }
  ]
}' |
apify call copy2paste/pdf-to-markdown-rag --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=copy2paste/pdf-to-markdown-rag",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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