# Office Docs to Markdown: DOCX, PPTX, XLSX, ODT & EPUB (`ataism/docs-to-markdown`) Actor

Convert DOCX, PPTX, XLSX, ODT/ODS/ODP and EPUB files to Markdown or text with tables, metadata and RAG chunks. $2.50 per 1,000 documents; failed files are free.

- **URL**: https://apify.com/ataism/docs-to-markdown.md
- **Developed by:** [Kaizoku Data](https://apify.com/ataism) (community)
- **Categories:** Developer tools, AI, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.50 / 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

## Office Docs to Markdown: DOCX, PPTX, XLSX, ODT & EPUB

Convert **Word, PowerPoint, Excel, OpenDocument and EPUB files to Markdown or plain text** in bulk. Give it file links, uploads or a key-value store, and get one row per document: headings, lists, bold/italic, links and tables as Markdown, page/slide/sheet/chapter markers, metadata and optional RAG chunks with page numbers. Use it to feed documents into search, LLM prompts or a vector database. No browser, no LibreOffice, no AI service: plain code that reads the files.

**Price: $2.50 per 1,000 documents ($0.0025 per document), per document, not per page, the same as [PDF to Text & Markdown](https://apify.com/ataism/pdf-to-text).** Only documents that convert are charged; empty, encrypted and failed files are free. No start fee on top of Apify's default.

- **Seven formats, one output shape:** DOCX, PPTX, XLSX, ODT, ODS, ODP and EPUB (also DOCM, XLSM, PPTM, PPSX and templates). The format is read from the file's bytes, not its name.
- **Structure kept:** headings from Word styles and outline levels, bulleted and numbered lists, tables, links, footnotes, slide titles, sheet names, optional speaker notes.
- **RAG-ready:** chunks of about N characters with `pageStart` / `pageEnd` (page, slide, sheet or chapter numbers), the same shape as [PDF to Text & Markdown](https://apify.com/ataism/pdf-to-text), so PDFs and Office files can share one pipeline.
- **Safe with hostile files:** zip-bomb caps (size, ratio, entry count), no XML entity expansion or external entities, macros never run, embedded files never opened, each document converted in an isolated worker with time and memory limits.
- **Quick start:** press *Start*. The prefilled input converts a 2-page sample DOCX and a 3-sheet sample XLSX (Apache POI test files) in a few seconds for $0.005.

### What it does

- Reads the files you list: **direct URLs**, **uploads** from your computer, or **records of one of your key-value stores**. It does **not** crawl websites or look for documents.
- Converts each file in an isolated worker into the `markdown` field (or `text`, if you choose plain text), with a marker per page, slide, sheet or chapter and `---` between them, like PDF to Text.
- Reports what was converted: format, page/slide/sheet/chapter count, words, tables, pictures, which caps cut the document, sheet sizes, and document properties (title, subject, description, keywords, language, dates, application). **Author, last-modified-by, company and manager names are never returned.**

What each format gives:

| Format | Unit | What is kept |
|---|---|---|
| DOCX, DOCM, DOTX | page | Headings (styles "Heading 1–9", "Title", outline levels), paragraphs, bold/italic/strike, links, bulleted and numbered lists, tables (merged cells as empty cells), footnotes and endnotes, text boxes. Tracked changes read as accepted (deleted text left out). Hidden text, comments, headers, footers and field codes are left out. |
| PPTX, PPTM, PPSX, POTX | slide | Slide title as a heading, bullet lists, text boxes, tables, optional speaker notes. Slide-number, date and footer placeholders are left out. |
| XLSX, XLSM, XLTX | sheet | One Markdown table per sheet (first non-empty row as header), saved formula results, ISO dates, TRUE/FALSE. Empty rows dropped, numbers as saved (no number formatting). |
| ODT | page | Headings, paragraphs, lists, tables, links, bold/italic, footnotes. Comments and tracked deletions left out. |
| ODS | sheet | Like XLSX. Huge "repeated" empty rows and columns cost nothing. |
| ODP | slide | Title, outline lists, text frames, tables, optional notes. |
| EPUB 2 and 3 | chapter | Chapters in reading order: headings, paragraphs, lists, quotes, code blocks, tables, links. Scripts and styles skipped. |

DOCX and ODT pages come from the page breaks saved in the file: Word and LibreOffice save where each page ended the last time they laid out the document, plus manual page breaks. A file saved without them counts as one page.

### Input

| Field | What it does |
|---|---|
| **File URLs** (`files`) | Direct `http(s)` links, one per line. Default (prefill): two small public sample files. |
| **Uploaded files** (`uploadedFiles`) | Upload from your computer (the Console stores the file in a key-value store and passes its record URL), or paste record URLs. |
| **Key-value store** (`keyValueStoreId`) | Pick one of your stores. The Actor gets **read access to that store only** (works with Limited permissions). Every record whose key ends in a supported extension is converted, unless you list keys. |
| **Record keys** (`keyValueStoreKeys`) | Only these records of the chosen store. |
| **Output format** (`outputFormat`) | `markdown` (default) or `text` (plain text; tables as tab-separated lines). |
| **Split into chunks for RAG** (`chunkForRag`) | Off by default. On: a `chunks` array of about **Chunk size** (`chunkSize`, default 1,000) characters with **Chunk overlap** (`overlap`, default 100), cut at paragraph, line, sentence or word boundaries. `chunkOverlap` is accepted too. |
| **Include tables** (`includeTables`) | On by default. Off: tables in documents, slides and e-books are left out. Spreadsheet sheets are always kept. |
| **Include speaker notes** (`includeNotes`) | Off by default. On: notes under each slide (PPTX, ODP). |
| **Max pages / slides / sheets** | `maxPages` (DOCX/ODT pages, EPUB chapters; default 200, max 2,000), `maxSlides` (default 200, max 1,000), `maxSheets` (default 20, max 200). Cut documents are marked `truncated`. |
| **Max rows per sheet** (`maxRows`) | Default 10,000, max 50,000. The rest of the sheet is not read. |
| **Max file size (MB)** (`maxFileSizeMb`) | Default 50, max 100. Larger files are free error rows. The run memory also sets a limit (61 MB at 512 MB with 2 parallel files, 92 MB with 1; 100 MB from 1,024 MB); the log says when it applies. |
| **Download timeout** (`downloadTimeoutSecs`) | Per file, default 60 s. Waiting for a site's `Crawl-delay` is not counted. |
| **Parallel files** (`concurrency`) | Downloads at once, 1 to 4 (default 2), at most 2 per host. |

#### Examples (copy-paste inputs)

**1. Knowledge base (RAG): Word manuals and slide decks to chunks with page and slide numbers**

```json
{
  "files": ["https://example.org/handbook.docx", "https://example.org/training.pptx"],
  "chunkForRag": true,
  "chunkSize": 1000,
  "overlap": 150,
  "includeNotes": true
}
```

**2. Spreadsheets to Markdown tables for an LLM prompt: first 2 sheets, at most 200 rows each**

```json
{
  "files": ["https://example.org/budget-2025.xlsx", "https://example.org/inventory.ods"],
  "maxSheets": 2,
  "maxRows": 200
}
```

**3. Your own files from a key-value store, as plain text**

```json
{
  "keyValueStoreId": "YOUR_STORE_ID",
  "keyValueStoreKeys": ["contract-2025-01.docx", "minutes.odt"],
  "outputFormat": "text"
}
```

#### Using your own files

- **Uploaded files:** the field's upload button stores the file in a key-value store of your account and hands the Actor its record URL. The run reads `api.apify.com` key-value-store record URLs with its own token, but only on the first request to that URL (never on a redirect, never to another host). With Limited permissions the run may not be able to read every store: if an upload fails with `http` 401/403, use the **Key-value store** picker instead.
- **Key-value store:** the safest way for private files. Pick the store; the run gets read access to it only. Name records with their extension (`report.docx`) or list their keys.
- For other private files use a signed or temporary link.

### Output

One dataset item per file, in completion order (`inputIndex` is the position in your input):

```json
{
  "inputIndex": 0,
  "url": "https://example.org/report.docx",
  "source": "url",
  "fileName": "report.docx",
  "status": "ok",
  "message": null,
  "format": "docx",
  "unit": "page",
  "pageCount": 2,
  "pagesProcessed": 2,
  "truncated": false,
  "truncatedBy": [],
  "tableCount": 1,
  "imageCount": 0,
  "charCount": 485,
  "wordCount": 85,
  "markdown": "<!-- page 1 -->\n\n# Quarterly Report\n\nRevenue grew **12 percent** and costs were *flat*.[^1]\n\n- Hiring in two regions\n- New office lease\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 120 | 135 |\n\n---\n\n<!-- page 2 -->\n\n## Outlook\n\n...\n\n[^1]: Source: internal survey, 2025.",
  "metadata": { "title": "Quarterly Report", "subject": null, "description": null, "keywords": null, "language": null, "creationDate": "2025-03-01T10:00:00.000Z", "modDate": "2025-03-02T11:00:00.000Z", "application": "Microsoft Office Word", "hasMacros": false, "embeddedObjects": 0 },
  "fileSizeBytes": 4088,
  "processedAt": "2026-10-27T10:00:00.000Z"
}
```

With chunks on, each row also has `chunks: [{ index, text, charStart, charEnd, pageStart, pageEnd }]` (offsets into `markdown` or `text`). Spreadsheet rows also have `sheets: [{ index, name, hidden, rows, columns, rowsTruncated, dimension }]`.

#### Status and error codes (only `ok` is charged)

| `status` / `errorCode` | Meaning | Charged |
|---|---|---|
| `ok` | Converted (also when cut by a cap: see `truncated`) | **Yes, $0.0025** |
| `empty` | No text found (only pictures, charts or empty sheets) | No |
| `encrypted` | Password-protected Office or OpenDocument file, or DRM-protected EPUB | No |
| `unsupported-format` | Not one of the supported formats: old binary `.doc/.xls/.ppt`, `.xlsb`, PDF (use PDF to Text), RTF, other archives | No |
| `not-a-document` | The link returned a web page, or the record is JSON | No |
| `damaged` | The file is broken or its main part is missing | No |
| `zip-bomb` | Too many parts, too large when unpacked, or compressed suspiciously well | No |
| `too-complex` | Nested too deeply, needs more memory than allowed, or used up its work budget before any content | No |
| `too-large` | File or part above the size limits | No |
| `timeout` | Download or conversion took too long | No |
| `row-too-large` | The converted row would exceed the 9 MB dataset item limit | No |
| `http`, `refused` | The server answered an error, or refused (401, 403, 407, 429, 451); refusals are never retried, a host is skipped after 3 | No |
| `robots-blocked`, `robots-unreachable`, `slow-host`, `host-skipped` | robots.txt disallows the file, could not be read, or asks for a Crawl-delay this run cannot afford; or the host refused earlier files | No |
| `platform`, `blocked`, `dns`, `network`, `invalid-url`, `redirects` | Video/social/Google link (e.g. Google Docs or Drive), private address, DNS or network failure, bad URL, too many redirects | No |
| `not-found`, `kv-read` | No record with that key, or the store could not be read | No |

`truncated: true` with `truncatedBy` (`maxPages`, `maxSlides`, `maxSheets`, `maxRows`, `size` or `time`) means the document was converted only up to that cap; it is still `ok` and charged once.

The run summary (key-value store record `RUN_SUMMARY`) has counts per status and error code, documents charged, bytes read, converter time and why the run stopped.

### Pricing

Pay per event: **$0.0025 per document with status `ok`** (event `document-converted`), plus Apify's standard Actor-start event. There is **no additional start fee** from this Actor. The price is per document, not per page, slide, sheet or row.

| Documents converted | Cost |
|---|---|
| 10 | $0.025 |
| 100 | $0.25 |
| 1,000 | $2.50 |
| 10,000 | $25 |

Add Apify's Actor-start event of $0.00005 per GB of run memory, once per run ($0.000025 at the default 512 MB). Empty, encrypted and failed files cost nothing. Set **Max total charge** in the run options to cap spending: the run never fetches a document it could not pay for, and stops cleanly (`stoppedBy: "max-charge"`).

### Limits

- **1,000 files per run** (URLs, uploads and store records together). Files up to 100 MB (default 50 MB). At 512 MB of run memory the largest file is 61 MB with 2 parallel files or 92 MB with 1 (the file bytes and the converter must fit the run's memory); use 1,024 MB or more for 100 MB files.
- **Output per document:** about 3 million characters (1.5 million with chunks), so a row fits the 9 MB dataset item limit; longer documents are cut and marked `truncated` (`size`).
- **Work budget per document:** each document may use about 2.25 CPU-seconds (about 18 s of wall time per converter worker at 512 MB and 1 GB); after that it is returned as far as it was read and marked `truncated` (`time`) and is **charged like any other cut document** (`truncatedBy` says why). A conversion still running at 2.5 times that budget plus 10 s is stopped (free `timeout` row). Normal documents use a fraction of it (a 50-page DOCX about 1.2 s at 512 MB).
- **Sheets:** at most 256 columns and 32,767 characters per cell.
- **ZIP safety caps:** at most 10,000 parts, 1 GB declared unpacked size, 300 MB actually unpacked per document, 120 MB per part, parts above 8 MB compressed more than 400:1 are refused. Nested archives and embedded files are never opened.
- **Not supported:** old binary `.doc`, `.xls`, `.ppt`, `.xlsb`, PDF (use [PDF to Text & Markdown](https://apify.com/ataism/pdf-to-text)), RTF, scanned pages or pictures (no OCR), charts and SmartArt, formulas (their saved results are shown), number formatting, password-protected or DRM files.
- **Free-work caps:** the run stops (`stoppedBy: "unpaid-work-cap"`) after 50 free rows that outnumber the converted ones, 500 MB read for free rows, or 600 s of converter time on free rows, so a list of bad links cannot run for long.
- **Memory:** default and minimum 512 MB (one converter worker); 1,024 MB uses two workers and halves the wall time of a batch. A document that needs more memory than its worker has is a free `too-complex` row; run with more memory for very large spreadsheets. The price does not change with memory.

### Use it via API, n8n or Make

Get an API token in Apify Console → *Settings* → *API & Integrations*, and keep it in an environment variable (`APIFY_TOKEN` below). Never paste it into shared code.

**cURL** (starts a run, waits for it and returns one row per file; this synchronous endpoint waits up to 5 minutes):

```bash
curl -X POST "https://api.apify.com/v2/acts/ataism~docs-to-markdown/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"files": ["https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/document/SampleDoc.docx"], "chunkForRag": true, "chunkSize": 1000}'
```

**JavaScript** (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('ataism/docs-to-markdown').call({
    files: ['https://example.org/handbook.docx', 'https://example.org/prices.xlsx'],
    chunkForRag: true,
    chunkSize: 1000,
    overlap: 150,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const doc of items) console.log(doc.fileName, doc.status, doc.format, doc.pageCount, doc.chunks?.length ?? 0);
```

**Python** (`pip install "apify-client>=3"`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("ataism/docs-to-markdown").call(run_input={
    "files": ["https://example.org/handbook.docx", "https://example.org/deck.pptx"],
    "chunkForRag": True,
    "chunkSize": 1000,
})
for doc in client.dataset(run.default_dataset_id).iterate_items():
    if doc["status"] != "ok":
        print("skipped (not charged):", doc["url"], doc["status"], doc["message"])
        continue
    for chunk in doc.get("chunks", []):
        print(doc["fileName"], doc["unit"], chunk["pageStart"], chunk["text"][:80])
```

(With apify-client versions before 3.0, `call()` returns a dict: use `run["defaultDatasetId"]`.)

**n8n:**

1. Start with any trigger that gives you file links (a form, a sheet row, an email with a link).
2. Add the **Apify** node, operation **Run an Actor and get dataset**, Actor `ataism/docs-to-markdown`, input `{"files": ["{{ $json.fileUrl }}"], "chunkForRag": true}`.
3. Use `markdown` in the next node (an AI summary, a Notion page) or split out `chunks` into your vector store node.

**Make:**

1. Add the **Apify** module **Run an Actor**, choose `ataism/docs-to-markdown`, put your file links in the input JSON and set *Run synchronously* to *Yes*.
2. Add the **Apify** module **Get Dataset Items** and map the run's default dataset ID (one bundle per file).
3. Send `markdown`, `text` or `chunks` to your next module (OpenAI, Google Docs, Airtable, a webhook).

### Privacy: transient processing

- Files are read into memory, converted in an isolated worker, and discarded. **The Actor stores nothing except the dataset items you asked for, the run summary and a progress record with counts** (used to resume after a platform restart). No copy of a file is saved and nothing is kept between runs.
- Document contents are **never written to the log**. Logs show only the host and path of a URL (not its query string), or the record key, and the status.
- Documents can contain personal data (names, contact details, figures). **This Actor outputs what is in your files**: the text is not filtered or redacted. You decide which files to convert and you are responsible for having the right to process them (copyright, licences, confidentiality, data-protection law).
- Metadata is limited to title, subject, description, keywords, language, dates, the saving application and the macro/embedded-file flags. Author, last-modified-by, company and manager names are not returned; comments (which carry author names) and tracked deletions are left out.
- Your dataset belongs to your Apify account and follows your retention settings. The `url` field echoes what you supplied (including a signed query string if you used one), so treat the dataset as sensitive if your URLs are.
- No data is sent to any AI or third-party service. The conversion is plain code running inside the Actor.

### Safety and fair use of other sites

- Only `http` and `https` URLs to **public** addresses are fetched. Private, loopback, link-local and internal addresses and other schemes are blocked, including after redirects (at most 5, each re-checked) and at connection time. Links to video, social and Google services (including Google Docs and Drive) are refused before any request (`platform`): download the file and upload it instead.
- The User-Agent is `docs-to-markdown-actor/0.1 (https://apify.com/ataism/docs-to-markdown)`. robots.txt of each host is checked (exact product-token match, else `*`); a disallowed file is not downloaded. A `Crawl-delay` is honoured in full and never shortened; a host that asks for more than this run can afford to wait (1.8 s at 1 GB, 3.6 s at 512 MB; 600 s in total per run) is skipped as free `slow-host` rows. At most 2 downloads at a time per host, at least 250 ms apart. No cookies, no proxies, no CAPTCHA solving; blocks are reported, not worked around.
- Documents are parsed as data. Macros (VBA projects) are never run, embedded objects and nested archives are never opened, external references and XML entities are never resolved.

### FAQ

**Does it work with old `.doc` or `.xls` files?** No, only the XML-based formats (DOCX, XLSX, PPTX and OpenDocument). Save old files in the new format first. They are reported as `unsupported-format` and not charged.

**Can it read PDFs?** No. Use [PDF to Text & Markdown](https://apify.com/ataism/pdf-to-text): it returns the same row shape, chunks included.

**Is it charged per page?** No, per document. A 300-page report and a 1-page memo cost the same $0.0025 (pages above `maxPages`, default 200, are not converted).

**Why is `pageCount` different from what Word shows?** DOCX/ODT pages come from the page breaks the file saved when it was last laid out. Files written by other programs often have none and count as one page; the text is complete either way.

**Are formulas or charts converted?** Formulas show their last saved result. Charts, pictures and SmartArt are not converted (pictures are counted in `imageCount`).

**Can it find documents on a website?** No. It only reads the URLs, uploads and store records you give it.

### Open-source components

Conversion is our own code on top of **htmlparser2** 12.0.0 (MIT; streaming XML/HTML tokenizer, with its dependencies entities 8.1.0, domhandler 6.0.1, domutils 4.0.2 and domelementtype 3.0.0, BSD-2-Clause, and dom-serializer 3.1.1, MIT) and **yauzl** 3.4.0 (MIT; ZIP reader, with pend 1.2.0, MIT), plus the **Apify SDK** (`apify` 3.7.2, Apache-2.0). No LibreOffice, no browser and no GPL/AGPL code. The sample files are Apache POI test data (Apache-2.0).

# Actor input Schema

## `files` (type: `array`):

Direct http(s) links to document files, one per entry, e.g. \["https://example.org/report.docx"]. Each file becomes one row. Costs $0.0025 per document converted (status ok, also when cut by a cap); empty, encrypted and failed files are free. The format is read from the file's bytes, not its name. Private, local and internal addresses are blocked, robots.txt is respected. At most 1,000 files per run (together with key-value store records). For a file on your computer, use Uploaded files or a key-value store.

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

Optional. Files uploaded from your computer (the Console editor stores them in a key-value store and passes their record URLs), or pasted record URLs, e.g. \[{"url": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/report.docx"}]. Same price and limits as File URLs. The Actor reads each file once and stores nothing.

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

Optional. Pick one of your own key-value stores; the Actor gets read access to that store only (works with Limited permissions). Every record whose key ends in a supported extension (.docx, .pptx, .xlsx, .odt, .ods, .odp, .epub and variants) is converted, unless you list keys below. Same price and limits as File URLs.

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

Optional. Only these record keys of the chosen key-value store, e.g. \["contract-2025-01.docx"]. Empty: every record with a supported file extension (up to the 1,000-file limit).

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

markdown (default): headings, lists, bold/italic, links and tables as Markdown, with  /  /  /  markers and --- between them, in the "markdown" field. text: plain text in the "text" field (tables as tab-separated lines). No change to price.

## `chunkForRag` (type: `boolean`):

true: add a "chunks" array (about Chunk size characters each, cut at paragraph, line, sentence or word boundaries, with page/slide/sheet numbers for citations), same shape as PDF to Text. false (default): no chunks. No extra charge.

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

Target chunk length in characters, e.g. 1000. Default 1000, minimum 200, maximum 20,000. Only used when Split into chunks is on.

## `overlap` (type: `integer`):

Characters repeated at the start of the next chunk, e.g. 150. Default 100, maximum 10,000; capped below half the chunk size. Only used when Split into chunks is on.

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

true (default): tables in documents, slides and e-books are kept (Markdown tables, or tab-separated lines in text output). false: they are left out. Spreadsheet sheets (XLSX, ODS) are tables and are always kept. No change to price.

## `includeNotes` (type: `boolean`):

true: add the speaker notes of each slide (PPTX, ODP) under the slide. false (default): slides only. No change to price.

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

Convert at most this many pages of a DOCX/ODT or chapters of an EPUB, e.g. 50. Default 200, maximum 2,000. Pages come from the page breaks saved in the file (Word and LibreOffice save where each page ended); a file without them counts as one page. Cut documents are marked "truncated": true and are charged once. The price is per document.

## `maxSlides` (type: `integer`):

Convert at most this many slides per presentation, e.g. 30. Default 200, maximum 1,000. The price is per document.

## `maxSheets` (type: `integer`):

Convert at most this many sheets per workbook, in tab order, e.g. 5. Default 20, maximum 200. The price is per document.

## `maxRows` (type: `integer`):

Keep at most this many non-empty rows of each sheet, e.g. 1000. Default 10,000, maximum 50,000; the rest of the sheet is not read. Each document's output is also capped at about 3 million characters (1.5 million with chunks) to fit a 9 MB dataset item. The price is per document.

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

Largest file to read, in MB, e.g. 20. Larger files are skipped with a free error row. Default 50, maximum 100. The run memory also sets a limit: 61 MB at 512 MB with 2 parallel files (92 MB with 1), 100 MB from 1,024 MB.

## `downloadTimeoutSecs` (type: `integer`):

Time allowed to download each file, including redirects, e.g. 60. Waiting for a site's Crawl-delay is not counted. Default 60, minimum 5, maximum 300. Files that time out are free error rows.

## `concurrency` (type: `integer`):

How many files are downloaded at once, e.g. 2. Default 2, minimum 1, maximum 4. At most 2 at a time from one host. Conversion runs in 1 worker below 1 GB of run memory.

## `mode` (type: `string`):

Internal: "selfTest" runs the maintainer health check and saves no items. Leave as "run" (the default).

## Actor input object example

```json
{
  "files": [
    "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/document/SampleDoc.docx",
    "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/spreadsheet/SampleSS.xlsx"
  ],
  "outputFormat": "markdown",
  "chunkForRag": false,
  "chunkSize": 1000,
  "overlap": 100,
  "includeTables": true,
  "includeNotes": false,
  "maxPages": 200,
  "maxSlides": 200,
  "maxSheets": 20,
  "maxRows": 10000,
  "maxFileSizeMb": 50,
  "downloadTimeoutSecs": 60,
  "concurrency": 2,
  "mode": "run"
}
```

# Actor output Schema

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

One item per document with status, Markdown or text, tables, metadata and optional chunks (JSON, CSV, Excel or XML).

## `runSummary` (type: `string`):

Counts per status and error code, documents charged, bytes read, converter time and why the run stopped.

# 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 = {
    "files": [
        "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/document/SampleDoc.docx",
        "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/spreadsheet/SampleSS.xlsx"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ataism/docs-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 = { "files": [
        "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/document/SampleDoc.docx",
        "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/spreadsheet/SampleSS.xlsx",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("ataism/docs-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 '{
  "files": [
    "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/document/SampleDoc.docx",
    "https://raw.githubusercontent.com/apache/poi/5de26e846918e4fbbeadce96ecdef0c92548a8b9/test-data/spreadsheet/SampleSS.xlsx"
  ]
}' |
apify call ataism/docs-to-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ataism/docs-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/QrPlxDVSNr2XdspqA/builds/QzZ2GOGsoaJSQ3WCc/openapi.json
