# Documents to Markdown/JSON for RAG (PDF, Office, OCR) (`bravona/docs-to-markdown-rag`) Actor

- **URL**: https://apify.com/bravona/docs-to-markdown-rag.md
- **Developed by:** [Bravona](https://apify.com/bravona) (community)
- **Categories:** AI, Developer tools, Automation
- **Security:** Limited permissions
- **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 converteds

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

## Documents to Markdown/JSON for RAG (PDF, Office, OCR)

**Documents to Markdown/JSON for RAG** converts **PDF, Word (DOCX), PowerPoint (PPTX), Excel (XLSX/CSV), HTML pages and images** into clean **Markdown** plus **RAG-ready JSON chunks** with page references, heading paths and tables. Scanned pages are read with **OCR only when a page has no text layer**, so digital documents stay fast and cheap.

Give it public document links or files in your own Apify key-value store, and download the results as JSON, CSV or Excel, or feed them straight into a vector database.

### What does it do?

- **One output for every format.** PDF, DOCX, PPTX, XLSX, XLS, ODS, CSV, HTML, TXT/Markdown, PNG/JPEG/TIFF/WebP/GIF/BMP images.
- **Structure kept.** Headings (`#`, `##`…), bullet and numbered lists, bold/italic (DOCX/HTML), and tables as GitHub-flavored Markdown tables.
- **Page references.** Every page (PDF page, slide, or 3,000-character block for flowing formats) is numbered, and every chunk says which pages it covers (`pageStart`, `pageEnd`).
- **RAG chunks.** Heading-aware chunks of a size you choose (default 1,500 characters, 150 overlap). Each chunk carries its `headingPath` (e.g. `["Part 2", "Deposits"]`). Chunks never cut a table row; split tables repeat their header row in every chunk.
- **OCR only where needed.** In `auto` mode only pages without a usable text layer are OCR'd (Tesseract 5, 7 languages). Each OCR'd page reports a confidence score.
- **Speaker notes** for slides (optional), every sheet of a workbook, document title and author when the file has them.
- **Error records instead of crashes.** A file that is missing, too large, encrypted, unsupported or blocked becomes one error record with a clear `errorCode` and is not charged.

### Who is it for?

**AI / RAG builders** loading documentation, reports, contracts or manuals into a vector database. **Data teams** extracting tables and text from public reports and filings. **Automation builders** who need a single "any document → Markdown" step in a workflow.

### Input

| Field | What it does | Default |
|---|---|---|
| `urls` | Public, logged-out document links | – |
| `keyValueStoreId` / `keyValueStoreKeys` | Convert files you uploaded to your own Apify key-value store (all records if no keys are given; `INPUT` is skipped) | – |
| `ocrMode` | `auto` (pages without text only), `always` (every page), `off` | `auto` |
| `ocrLanguage` | `eng`, `deu`, `fra`, `spa`, `ita`, `por`, `nld`, or combinations like `eng+deu` | `eng` |
| `maxFileSizeMb` | Bigger files are not downloaded (error `FILE_TOO_LARGE`) | 25 (max 100) |
| `maxPagesPerDocument` | Page cap per document | 200 (max 1,000) |
| `onPageLimit` | `truncate` (convert the first pages) or `fail` (error `TOO_MANY_PAGES`, nothing charged) | `truncate` |
| `maxOcrPagesPerDocument` | Pages past this that need OCR are returned empty and flagged `ocrSkipped` (not charged) | 50 (max 500) |
| `maxDocuments` | Documents per run | 100 (max 2,000) |
| `maxConcurrency` | Documents converted in parallel | 2 (max 8) |
| `requestTimeoutSecs` | Download timeout per file | 60 |
| `includeMarkdown` / `includePages` / `includeChunks` | Which outputs to include | all on |
| `chunkSize` / `chunkOverlap` | Chunk size and overlap in characters | 1,500 / 150 |
| `includeSpeakerNotes` | PPTX speaker notes | off |

Example:

```json
{
  "urls": [{ "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" }],
  "chunkSize": 1200,
  "chunkOverlap": 120
}
```

### Output

One dataset record per document (`status: "ok"` or `"error"`):

| Field | Description |
|---|---|
| `status`, `sourceIndex`, `source` | Result and which input it came from (`{url}` or `{keyValueStoreId, key}`) |
| `finalUrl`, `fileName`, `contentType`, `format`, `sizeBytes`, `sha256` | File facts after redirects; `format` is detected from the content, not only the extension |
| `title`, `author` | From document metadata when present |
| `pageUnit` | `pdf-page`, `slide`, `image`, or `chars-3000` (DOCX, HTML, text, sheets: one page per ~3,000 characters) |
| `pageCountInFile`, `pagesProcessed`, `textPages`, `ocrPages`, `ocrSkippedPages`, `truncated` | Page accounting |
| `chars`, `markdown` | Full document Markdown |
| `pages[]` | `{pageNumber, markdown, chars, ocr, ocrConfidence, ocrSkipped}` |
| `chunks[]` | `{chunkIndex, text, chars, pageStart, pageEnd, headingPath}` |
| `chunkCount`, `warnings` | |
| `billable` | `{document, page, ocrPage}` events this document counts for (0 for errors) |
| `fullRecordKey`, `fullRecordUrl` | Set when a record is over Apify's 9 MB item limit: the dataset item keeps the metadata, and `markdown`, `pages` and `chunks` are in that record of the run's key-value store |
| `errorCode`, `errorMessage` | For errors: `INVALID_URL`, `BLOCKED_ADDRESS`, `ROBOTS_DISALLOWED`, `ROBOTS_UNAVAILABLE`, `AUTH_REQUIRED`, `CAPTCHA_OR_BOT_WALL`, `HTTP_ERROR`, `NETWORK_ERROR`, `TIMEOUT`, `FILE_TOO_LARGE`, `TOO_MANY_PAGES`, `UNSUPPORTED_FORMAT`, `ENCRYPTED_OR_CORRUPT`, `NO_TEXT`, `OCR_FAILED`, `KV_RECORD_NOT_FOUND`, `CONVERSION_FAILED` |
| `processingMs`, `convertedAt` | |

Dataset views: **Overview**, **Chunks** (one row per chunk, ready for embedding) and **Errors**. A run summary (counts, totals, errors by code, per-document status, events charged) is saved as the `OUTPUT` record of the run's key-value store.

### How fetching works (responsible access)

- Only public, logged-out URLs. No cookies, no logins, no proxies. Links that need a login (HTTP 401/407, or credentials in the URL) get `AUTH_REQUIRED`, other refusals such as 403 get `HTTP_ERROR`; CAPTCHA or bot-check pages get `CAPTCHA_OR_BOT_WALL`. The Actor never tries to get past them.
- `robots.txt` is read for every host and followed (user agent `Bravona-DocsToMarkdown`, RFC 9309 rules, `Crawl-delay` honored up to 10 s).
- At most 1 request per second per host; up to 5 redirects; private, loopback and cloud metadata addresses are refused (`BLOCKED_ADDRESS`).
- Only convert documents you have the right to process.

### Limits and known gaps

- Heading detection in PDFs uses font sizes, so heading levels are a best guess; figure labels set in a large font can appear as headings.
- PDF tables come out as text lines (DOCX, PPTX, XLSX and HTML tables are real Markdown tables).
- Encrypted/password-protected files are not opened (`ENCRYPTED_OR_CORRUPT`).
- OCR quality depends on the scan; low `ocrConfidence` (below ~80) means the text needs review.
- Old binary formats (`.doc`, `.ppt`) are not supported; `.xls` is.

### Memory

The default is **2 GB**. OCR is CPU-bound, and Apify gives CPU in proportion to memory, so 2 GB finishes OCR about twice as fast as 1 GB for the same cost ($0.0156 vs $0.0150 for a 10-page scan; 137 s vs 263 s). Peak memory in tests was 265 MB. The minimum is 512 MB, which is enough for text-only documents. 4 GB is the maximum, and it costs more ($0.0188) without being faster per CPU-second.

### Pricing

Pay per event:

| Event | Price |
|---|---|
| Actor Start | $0.00005 per run |
| Document converted (primary) | $0.003 per document converted |
| Page processed | $0.0003 per page (PDF page, PPTX slide; for HTML, DOCX, text and spreadsheets, 1 page = 3,000 characters) |
| OCR page processed | $0.006 per OCR page, charged instead of the page price, not in addition to it |

Failed documents are not charged. See [PRICING.md](PRICING.md) for details.

Works on Apify's free plan; you pay a tiny start fee plus only for results returned (each converted document and its pages). Charges come out of your Apify account's prepaid usage; on the Free plan, once that monthly usage is used up, runs stop until the next billing cycle.

# Actor input Schema

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

Public, logged-out links to PDF, DOCX, PPTX, XLSX, CSV, HTML, plain-text or image files. The format is detected from the file itself. robots.txt is respected and each host is fetched at most once per second; pages behind a login, paywall or captcha are reported as errors, never bypassed.

## `keyValueStoreId` (type: `string`):

Convert files you uploaded to one of your Apify key-value stores. Every record in the store is converted unless you list keys below.

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

Record keys in the store above. Leave empty to convert every record (except INPUT).

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

auto: OCR only pages that have no text layer (scans, photos). off: never OCR (such pages come back empty and are not charged). force: OCR every page, even ones with text.

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

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

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

Larger files are not downloaded; they get an error record and are not charged. Maximum 100.

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

PDF pages, slides, or 3,000-character blocks for formats without pages. Maximum 1,000.

## `onPageLimit` (type: `string`):

truncate: convert and charge only the first pages up to the cap. fail: skip the document with an error record (not charged).

## `maxOcrPagesPerDocument` (type: `integer`):

Pages past this that need OCR are returned empty, flagged ocrSkipped, and not charged. 0 disables OCR. Maximum 500.

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

Documents after this are not processed. Maximum 2,000.

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

How many documents are converted at once. More needs more memory (OCR uses about 300 MB per document in flight).

## `requestTimeoutSecs` (type: `integer`):

How long to wait for one file to download before giving up on it (error record <code>TIMEOUT</code>, not charged).

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

The whole document as one Markdown string.

## `includePages` (type: `boolean`):

One entry per page with its page number, text and OCR flags.

## `includeChunks` (type: `boolean`):

Heading-aware chunks with pageStart/pageEnd and the heading path, ready to embed.

## `chunkSize` (type: `integer`):

Target maximum characters per chunk. Chunks break at headings, paragraphs and table rows; split tables repeat their header row.

## `chunkOverlap` (type: `integer`):

Text repeated from the end of the previous chunk; at most half the chunk size.

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

For PowerPoint files, add each slide's speaker notes under the slide text.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    }
  ],
  "ocrMode": "auto",
  "ocrLanguage": "eng",
  "maxFileSizeMb": 25,
  "maxPagesPerDocument": 200,
  "onPageLimit": "truncate",
  "maxOcrPagesPerDocument": 50,
  "maxDocuments": 100,
  "maxConcurrency": 2,
  "requestTimeoutSecs": 60,
  "includeMarkdown": true,
  "includePages": true,
  "includeChunks": true,
  "chunkSize": 1500,
  "chunkOverlap": 150,
  "includeSpeakerNotes": true
}
```

# Actor output Schema

## `results` (type: `string`):

All records in the default dataset, one per document: Markdown, per-page text, RAG chunks with page ranges, or an error record.

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

The OUTPUT record in the default key-value store: resolved input, per-document status, totals, billable and charged events.

# 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": [
        {
            "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("bravona/docs-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 = { "urls": [{ "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("bravona/docs-to-markdown-rag").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": [
    {
      "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    }
  ]
}' |
apify call bravona/docs-to-markdown-rag --silent --output-dataset

```

## MCP server setup

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

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/gu7EisoGpW2EcIrWJ/builds/9em6LrOklnJOrIYKl/openapi.json
