# Word, PowerPoint, Excel, EPUB & Email to Markdown + RAG Chunks (`bongo-seakhoa/office-to-markdown-rag-chunks`) Actor

Convert DOCX, PPTX, XLSX, CSV, EPUB, HTML and EML files to clean Markdown and RAG chunks with heading paths. Tables to CSV. Offline, no LLM, no API key.

- **URL**: https://apify.com/bongo-seakhoa/office-to-markdown-rag-chunks.md
- **Developed by:** [Bongo Seakhoa](https://apify.com/bongo-seakhoa) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.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

## Word, PowerPoint, Excel, EPUB & Email to Markdown + RAG Chunks

Turn Word documents, PowerPoint decks, Excel and OpenDocument spreadsheets, CSV files, EPUB books, HTML pages and saved emails into clean Markdown and retrieval-ready chunks. Every chunk says where it came from in the original file: the slide and shape, the sheet and cell range, the Word paragraphs, the EPUB chapter or the part of the email. Tables and sheets also come out as CSV files, and each sheet gets a short schema summary. Everything runs offline inside the Actor: no language model, no API key, nothing sent to a third party.

### What it outputs

- **Markdown** per file, with headings, nested lists, tables as GitHub-flavoured Markdown, footnotes and speaker notes. Each slide of a deck is a section, and so is each sheet of a workbook and each chapter of a book (in spine order, titled from the book's table of contents).
- **RAG chunks** sized in characters, split at headings, paragraphs and table rows. Each chunk carries `heading_path`, exact `char_start`/`char_end` offsets into the Markdown, a stable `chunk_id`, and `locator_start`/`locator_end`: where its first and last characters are in the source file (see [Source locators](#source-locators)). A chunk that starts in the middle of a long table repeats the table's header row in `table_header`.
- **Tables and sheets** as dataset rows and as CSV files. A sheet's Markdown table stops at 1,000 rows by default; its CSV file always has every row.
- **A schema summary per sheet**: each column's inferred type (int, float, date, bool or text), share of empty cells, number of distinct values over the first 10,000 rows, and three sample values.
- **Formula cells** of XLSX and XLSM workbooks, listed with their formula text next to the value saved in the file. Formulas are never recalculated.
- **Email attachments as documents.** The attachments of an `.eml` file that are in a supported format are converted as their own documents, linked to the email by `parent_document_id`. PDF attachments are listed with a pointer to the PDF Actor below.
- **Nothing left out silently.** Pictures, charts, grouped shapes, empty slides, empty sheets and skipped hidden sheets appear as bracketed placeholders such as `[image]` or `[empty sheet]` where they were, and the document's `warnings` list them. Text skipped without a placeholder (Word page headers and footers, a web page's `<nav>` and `<footer>`) gets a warning too.
- **An estimate mode** that reads up to 100 files and reports how many documents would be charged and the projected price, without writing outputs. It charges none of this Actor's events.
- **Honest failures.** Broken, encrypted, unsupported, oversized or unreachable files get a status row with an error code and are never charged.
- **No AI service.** Everything runs inside the Actor's container with open-source parsers (mammoth, python-pptx, python-calamine, Beautiful Soup, lxml and Python's own email and CSV readers). Nothing is sent to a language model or a third-party API.

Typical uses: feeding company documents, slide decks and spreadsheets into a vector database for retrieval-augmented generation, giving an LLM pipeline sheets it can reason about (schema summary plus the rows), and turning saved email threads with their attachments into searchable text.

### How to use it

1. Paste links to your files into **File URLs**. The format is detected from the file's content, not its name, so a link without an extension works too.
2. Or pick a **key-value store** that holds your files (upload them in Apify Console under Storage), optionally naming the record keys to use. Without keys, the run reads the records the store holds when the run starts; records added later are not included.
3. Leave the defaults or adjust the options, then start the run.
4. Open the **Output** tab. The *Documents*, *Chunks* and *Tables and sheets* views each list every row of the run, so rows of the other types show empty cells; sort or filter on `record_type`, or download the `.md` and `.csv` files from the run's key-value store.

Starting the Actor with no input converts three small bundled synthetic files (a Word report, a spreadsheet of orders, and an email that carries the report as an attachment) so you can see the output format first. The sample is charged none of this Actor's events; Apify's standard Actor start fee still applies, as it does to every run.

### Input

```json
{
  "sources": [
    { "url": "https://example.com/files/handbook.docx" },
    { "url": "https://example.com/files/q3-figures.xlsx" }
  ],
  "mode": "convert",
  "outputs": ["markdown", "chunks", "tables_csv"],
  "formats": ["docx", "pptx", "xlsx", "xls", "ods", "csv", "epub", "html", "eml"],
  "chunkSize": 1500,
  "chunkOverlap": 150,
  "includeSpeakerNotes": true,
  "maxRowsPerSheetMarkdown": 1000,
  "schemaSummary": true,
  "headerRow": 1,
  "skipHiddenSheets": true,
  "convertEmailAttachments": true,
  "maxAttachmentsPerEmail": 20,
  "maxFileSizeMb": 100,
  "perFileTimeoutSecs": 120
}
```

| Field | Default | What it does |
|---|---|---|
| `sources` | `[]` | File URLs (http or https), as `{"url": ...}` objects or plain strings. |
| `keyValueStoreId`, `keyValueStoreKeys` | none | Read files from a key-value store. Without keys, every record whose key ends in a supported extension (or whose content type matches) is used, up to 1,000. |
| `mode` | `convert` | `estimate` reads the files and projects the price; it charges none of this Actor's events. It takes at most 100 files per run and gives each at most 30 seconds. |
| `outputs` | all three | Any of `markdown`, `chunks`, `tables_csv`. |
| `formats` | all nine | Convert only these formats; other files fail with `UNSUPPORTED_FORMAT` and are not charged. Also limits which email attachments are converted. |
| `chunkSize`, `chunkOverlap` | 1500, 150 | Characters (about 4 per token). Overlap is at most half the chunk size. |
| `includeSpeakerNotes` | `true` | Adds each slide's speaker notes as a `Notes:` block. |
| `maxRowsPerSheetMarkdown` | 1000 | Rows of a sheet shown in the Markdown (10 to 20,000). The CSV file has all rows. |
| `schemaSummary` | `true` | Starts each sheet with its schema summary. |
| `headerRow` | 1 | Row of the sheet's used range that holds the column names. Rows above it stay as text. `0`: no header, columns are named `column_1`, `column_2` ... A report-style sheet with a title in row 1, a blank row and the column names in row 3 needs `headerRow: 3`. |
| `skipHiddenSheets` | `true` | Hidden sheets get an `[hidden sheet: not converted]` placeholder. Untick to convert them. |
| `convertEmailAttachments` | `true` | Convert supported attachments of `.eml` files as their own documents (one level deep). |
| `maxAttachmentsPerEmail` | 20 | At most this many attachments per email are converted (0 to 20); the rest are listed. |
| `maxFileSizeMb` | 100 | Larger files fail with `FILE_TOO_LARGE`. Never more than a quarter of the run's memory. |
| `perFileTimeoutSecs` | 120 | Time limit per file, shared by an email and its attachments. |

### Use it from code

Call the Actor through the Apify API with your API token (Apify Console > Settings > API & Integrations). This request waits for the run and returns the dataset rows:

```bash
curl -X POST "https://api.apify.com/v2/acts/bongo-seakhoa~office-to-markdown-rag-chunks/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sources": [{"url": "https://example.com/files/handbook.docx"}], "outputs": ["markdown", "chunks"]}'
```

The synchronous endpoint waits at most 5 minutes. For bigger batches, start the run (`POST /v2/acts/bongo-seakhoa~office-to-markdown-rag-chunks/runs`) and read its dataset when it has finished.

In Python, with `pip install apify-client` (version 3):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("bongo-seakhoa/office-to-markdown-rag-chunks").call(
    run_input={
        "sources": [{"url": "https://example.com/files/q3-figures.xlsx"}],
        "chunkSize": 1500,
    }
)
for item in client.dataset(run.default_dataset_id).iterate_items():
    if item["record_type"] == "chunk":
        print(item["heading_path"], item["locator_start"], item["text"][:80])
```

The Actor can also be called through Apify's MCP server, like any other Store Actor.

### Output

The dataset holds three kinds of rows, told apart by `record_type`. Filter on it when you read the dataset through the API. The examples below are rows of the bundled sample, trimmed where marked with `...`.

A **document** row, one per file and one per converted email attachment:

```json
{
  "record_type": "document",
  "document_id": "4c825b69d851745f",
  "document_index": 2,
  "source": "bundled-sample:sample.eml",
  "status": "ok",
  "error_code": null,
  "file_name": "sample.eml",
  "format": "eml",
  "title": "Field report attached (synthetic sample)",
  "author": "Synthetic Sender <sender@example.com>",
  "parent_document_id": null,
  "markdown": "# Field report attached (synthetic sample)\n\n- **From:** Synthetic Sender ...",
  "markdown_url": "https://api.apify.com/v2/key-value-stores/.../records/doc0003-4c825b69d851745f.md",
  "chunk_count": 2,
  "table_count": 0,
  "attachments": [
    {
      "file_name": "field-report.docx",
      "content_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "size_bytes": 7799,
      "disposition": "child",
      "document_id": "e499c8a9209a8d2e"
    }
  ],
  "warnings": [],
  "charged_events": {}
}
```

The attachment `field-report.docx` gets its own document row with `parent_document_id: "4c825b69d851745f"` and `attachment_name: "field-report.docx"`, plus its own chunks, tables and Markdown file. `document_id` comes from the file's content, so the same file has the same ID wherever it appears.

A **chunk** row, here one that continues the sample spreadsheet's table:

```json
{
  "record_type": "chunk",
  "document_id": "c7c904a37137f76e",
  "chunk_id": "f92b2e7e1af75a251a029e2d",
  "chunk_index": 2,
  "text": "| 1022 | South | Gizmo | 10 | 6.25 | 2026-01-22 | TRUE |\n| 1023 | East | Gadget | 17 | 7.5 | 2026-01-23 | TRUE |\n...",
  "char_start": 1849,
  "char_end": 3300,
  "heading_path": ["Orders"],
  "page_start": null,
  "page_end": null,
  "approx_tokens": 363,
  "locator_start": { "sheet": "Orders", "cell_range": "A23:G23" },
  "locator_end": { "sheet": "Orders", "cell_range": "A47:G47" },
  "table_ids": ["t1"],
  "table_header": "| order_id | region | product | quantity | unit_price | order_date | shipped |\n|---|---|---|---|---|---|---|",
  "format": "xlsx"
}
```

A **table** row, one per Word, slide, HTML or EPUB table and one per sheet or CSV file:

```json
{
  "record_type": "table",
  "document_id": "c7c904a37137f76e",
  "table_id": "t1",
  "kind": "sheet",
  "title": "Orders",
  "locator": { "sheet": "Orders", "cell_range": "A1:G51" },
  "header": ["order_id", "region", "product", "quantity", "unit_price", "order_date", "shipped"],
  "n_rows": 50,
  "n_cols": 7,
  "rows_in_markdown": 50,
  "schema_summary": {
    "rows_total": 50,
    "rows_profiled": 50,
    "columns": [
      { "name": "order_id", "type": "int", "null_rate": 0.0, "distinct_count": 50, "samples": ["1001", "1002", "1003"] },
      "..."
    ]
  },
  "formula_count": 0,
  "formulas": [],
  "csv_url": "https://api.apify.com/v2/key-value-stores/.../records/doc0002-c7c904a37137f76e-t1.csv"
}
```

Fields by row type. Every row also has `record_type`, `document_id` and `document_index` (the position of its file in the input).

| Row type | Field | What it holds |
|---|---|---|
| document | `source`, `source_type` | The URL or `kvs://store/key` it came from, and `url`, `kvs` or `sample`. |
| document | `status`, `error_code`, `error_message` | `ok`, `partial`, `failed` or `skipped`, and why (codes below). |
| document | `file_name`, `file_size_bytes`, `sha256`, `format` | The file as read, and its detected format. |
| document | `title`, `author`, `slide_count`, `sheet_count`, `chapter_count` | From the file's own metadata and structure, where it has them. |
| document | `markdown`, `markdown_chars`, `markdown_url` | The Markdown: in the row while it fits, always in the `.md` file. |
| document | `chunk_count`, `table_count` | Chunk and table rows written for this document. |
| document | `parent_document_id`, `attachment_name`, `attachments` | Links between an email and its attachments, and the email's attachment list. |
| document | `warnings`, `charged_events`, `mode`, `projected_cost_usd`, `elapsed_ms` | Reason codes, what was charged, and the estimate-mode price. |
| document | `fields_omitted` | Large fields left out to fit the platform's row size limit; the files still hold them. |
| chunk | `chunk_id`, `chunk_index`, `text`, `char_start`, `char_end`, `approx_tokens` | The chunk, its offsets in the Markdown and a rough token count (characters divided by 4). |
| chunk | `heading_path`, `locator_start`, `locator_end`, `page_start`, `page_end` | Where it is: headings, source locators, and pages (always `null` here). |
| chunk | `table_ids`, `table_header`, `format` | Tables it overlaps, and the header row of the table it continues. |
| table | `table_id`, `table_index`, `kind`, `title`, `locator` | Which table or sheet, and where it is in the file. |
| table | `header`, `header_generated`, `n_rows`, `n_cols`, `rows_in_markdown` | Its shape, and how many rows the Markdown shows. |
| table | `schema_summary`, `formula_count`, `formulas`, `csv_url` | Sheet profile, formula cells, and the CSV file with every row. |

Every chunk row has `document_id`, `chunk_id`, `chunk_index`, `text`, `char_start`, `char_end`, `heading_path`, `page_start`, `page_end` and `approx_tokens`, the same fields as the other Actors listed below, so their chunks can share one vector index. `markdown[char_start:char_end]` is exactly the chunk's `text`. The same file with the same settings on the same Actor build always gives the same chunk IDs, so re-indexing is idempotent.

Row `status` is `ok`, `partial` (some content may be missing: the warnings say why), `failed` (nothing converted; `error_code` says why) or `skipped` (not started: `CHARGE_LIMIT` when your maximum charge was reached, `TIMEOUT` when the run was about to time out). If Apify restarts a run while an item is being charged, that item is not processed or charged again: its row carries a `BILLING_UNCERTAIN_AFTER_RESTART` warning, and its `charged_events` may not list every charge made before the restart. A restart just after an item's chunk and table rows were saved can repeat those rows with the same `chunk_id` (the item is charged once and has one document row), so dedupe chunks by `chunk_id`. The run's key-value store also gets a `SUMMARY` record with the counts for the whole run.

| Error code | Meaning |
|---|---|
| `UNSUPPORTED_FORMAT` | Not a supported format, or excluded by `formats`. The message names the format and, for PDFs, the PDF Actor. |
| `FILE_ENCRYPTED` | A password-protected or encrypted Office file. |
| `DRM_PROTECTED` | An EPUB book with DRM. |
| `CORRUPT_FILE` | A damaged or unreadable file, or an XML part that declares a DTD. |
| `ARCHIVE_TOO_LARGE` | The Office or EPUB container has more than 2,000 entries or unpacks to too much data. |
| `NO_CONTENT` | Readable, but no text: only empty, hidden or image content (the warnings say which). |
| `INVALID_INPUT` | A key-value store or record could not be used as given, or an estimate run reached its 100 files. |
| `DOWNLOAD_FAILED` | The URL or record could not be fetched (an HTTP error, a network error, too slow). |
| `BLOCKED_DESTINATION` | The link, or a redirect, points to a private, local or cloud-metadata address, or carries a user name or password. |
| `FILE_TOO_LARGE` | Over `maxFileSizeMb`, or over a quarter of the run's memory. |
| `TIMEOUT` | Still converting at `perFileTimeoutSecs`. On a `skipped` row: the run was about to reach its own timeout. |
| `OUT_OF_MEMORY` | The file needed more memory than the run has; try 2048 MB. |
| `WORKER_CRASHED` | The converter process stopped unexpectedly on this file. |
| `INTERNAL_ERROR` | A bug or an unexpected library error on this file. Please report it. |
| `CHARGE_LIMIT` | On a `skipped` row: your maximum charge was reached before this file. |

None of these rows is charged. Warning codes such as `ROWS_TRUNCATED`, `TABLES_TRUNCATED`, `CHUNKS_TRUNCATED`, `ENCODING_GUESSED`, `HEADERS_FOOTERS_SKIPPED` and `BOILERPLATE_SKIPPED` come with a message saying what happened; they do not change the price.

#### Source locators

Page numbers are never invented: Word, PowerPoint, Excel, EPUB and email files have no fixed pages, so `page_start` and `page_end` are always `null`. Instead `locator_start` and `locator_end` give the place in the source file in its own terms:

| Format | Locator fields |
|---|---|
| DOCX | `paragraph_start`/`paragraph_end`: positions among the document body's paragraphs outside tables, counting headings and list items and skipping paragraphs with no text at all. Table rows: `table` (1-based, every table in reading order) and `row`. Footnotes: `footnote`. |
| PPTX | `slide`, plus `shape_id` and `shape_name` of the text box, table or picture; table rows add `row`; speaker notes have `notes: true`. |
| XLSX, XLSM, XLS, ODS | `sheet` and `cell_range` in A1 notation, down to the single row of a table line or the single cell of a formula. |
| CSV, TSV | `cell_range` in A1 notation (record numbers, as a spreadsheet would show them). |
| EPUB | `spine_index` and `spine_item` (the chapter file); the chunk's `heading_path` starts with the chapter title. Table rows add `table` and `row`. |
| EML | `part`: `headers`, `body` or `attachments` (with `attachment` naming the file in the attachment list). |
| HTML | the chunk's `heading_path`; table rows have `table` and `row`. |

Table rows carry the same kind of object in `locator`, for the whole table.

### Pricing

This Actor is priced per event: you pay for each document that produced content.

| Event | Price | When it is charged |
|---|---|---|
| `document-processed` | $0.01 ($10 per 1,000 documents) | Once per file that was converted with content (status `ok` or `partial`), and once per email attachment converted as its own document. |

Apify adds the Actor start fee to every run ($0.00005 per run at up to 1 GB of memory, once more for each extra GB). Pages, slides, rows, chunks, files and dataset rows are not charged separately: this Actor does not use the `apify-default-dataset-item` event.

Worked examples:

- One Word file at the default 1 GB: one `document-processed` and one start event, $0.01 + $0.00005 = $0.01005.
- 20 Word, PowerPoint and Excel files in one run, all converted: 20 x $0.01 + $0.00005 = $0.20005.
- One email with three attachments, a DOCX, an XLSX and a PDF: the email and the two Office files are charged, 3 x $0.01 = $0.03 plus the start fee. The PDF is listed, not converted, not charged.
- A 300-page manual and a one-page memo cost the same: one `document-processed` each, within the per-document output limits below.

Never charged by this Actor: files that fail (unsupported, encrypted, corrupt, empty, too large, unreachable), files skipped at your maximum charge or the run timeout, attachments that are only listed, the bundled sample, and estimate mode. Only Apify's start fee applies to those runs.

An email's row shows the charge for the email and its converted attachments together in `charged_events`; the attachments' own rows show `{}`. On a run without pay-per-event pricing (a private copy, for example), charged rows show a count of 0.

**Your maximum charge is respected.** Before each file the Actor checks what is left of your run's maximum total charge and converts only what it can charge for, keeping one document's price unspent below your limit. Email attachments the budget cannot cover are listed as not converted (`charge_limit`), and the email is marked `partial`. A file that cannot be afforded, and every file after it, is listed as `skipped` with `CHARGE_LIMIT` and is not charged; the run normally finishes as *Succeeded*. Rerun the skipped files with a higher limit.

### Limits: read before you buy

- **Formats**: DOCX and DOCM, PPTX and PPTM, XLSX and XLSM, XLS, ODS, CSV and TSV, EPUB 2 and 3 without DRM, HTML files and EML. Not supported: MSG, MBOX, RTF, ODT, ODP, legacy DOC and PPT, XLSB, password-protected or encrypted files (`FILE_ENCRYPTED`) and DRM-protected books (`DRM_PROTECTED`).
- **PDFs are not converted**, including PDF email attachments. Use the [PDF to Markdown & RAG Chunks](https://apify.com/bongo-seakhoa/pdf-to-markdown-rag-chunks) Actor for them.
- **Images, charts, diagrams and SmartArt** are not described or OCR'd: they appear as `[image]`, `[chart]` or `[diagram]` placeholders. A file that holds only pictures fails with `NO_CONTENT` and is not charged.
- **PowerPoint**: grouped shapes are not converted; they appear as `[grouped shapes: text not converted]` with a `SHAPES_SKIPPED` warning. Hidden slides are converted like the others.
- **Spreadsheets**: cells show the value saved in the file, and formulas are never recalculated. A file saved by a tool that stores no values (some generators do) shows those cells empty. Formula text is listed for XLSX and XLSM only: ODS files get a count and a `FORMULA_TEXT_UNAVAILABLE` warning, and XLS files show saved values only. Error values such as `#DIV/0!` are shown for XLSX and XLSM. One header row is assumed (choose it with `headerRow`, for example `3` for a sheet with a title row and a blank row above the column names); multi-row headers are not detected. The Markdown is capped per sheet, with the full data in the CSV file.
- **Outputs per document**: one charge covers at most 30 tables or sheets as table rows and CSV files, and at most 500 chunk rows. The Markdown file always has everything, and a `TABLES_TRUNCATED` or `CHUNKS_TRUNCATED` warning says what was left out of the rows. At the default chunk size, 500 chunks hold about 750,000 characters, a long book. Split bigger documents, or chunk the Markdown file yourself.
- **Merged table cells**: in Word, web page and EPUB tables, a cell merged down several rows is repeated in each of those rows; a cell merged across columns keeps its text in the first column. In spreadsheets, merged cells are not detected (one value, the other cells empty).
- **Text encodings**: CSV, HTML and email text that is not UTF-8 and declares no encoding is guessed, with an `ENCODING_GUESSED` warning. When several Western code pages fit, Windows-1252 (an Excel "CSV" export on Western Windows) is chosen, so Central European text saved without an encoding (Windows-1250) can come out with wrong letters. Save such files as UTF-8.
- **Word headers and footers** and a web page's `<nav>` and `<footer>` are not converted; a warning says so. Drop-down lists and frames in web pages are left out.
- **Chinese and Japanese text**: chunks end at line breaks or at sentence ends (`。`, `！`, `？`); a single sentence longer than the chunk size is cut mid-sentence.
- **Large spreadsheets**: a plain export of about 150,000 rows (an 8 MB XLSX) converts at the default 1 GB of memory. For bigger workbooks choose 2048 MB and raise `perFileTimeoutSecs`; a file that runs out of memory or time fails with `OUT_OF_MEMORY` or `TIMEOUT` and is not charged.
- **Word**: tracked changes and comments are not reproduced as such; fields and embedded objects mammoth cannot read are counted in an `UNSUPPORTED_CONTENT` warning.
- **Email files contain personal data.** You must have the right to process them; the outputs are stored in your Apify storage. Only one level of attachments is converted: attachments of an attached email are listed.
- **Macros are never run**, and archives are never unpacked beyond the Office or EPUB container itself (a `.zip` attachment is listed, not opened). Containers are checked before parsing: at most 2,000 entries and an uncompressed size of at most 500 MB and 50 times the file size; any XML part that declares a DTD is refused as `CORRUPT_FILE`.
- **Layouts**: text boxes, multi-column sections and complex slides may come out in a different reading order. Slides are read top to bottom, then left to right.
- **CSV files**: a cell that starts with `=`, `+`, `-`, `@`, TAB or CR gets a leading single quote (`'`), unless it is a plain number, so a spreadsheet does not run it as a formula. JSON rows and Markdown keep the raw value.
- **Links to web pages**: HTML is a supported format, so a link that returns a web page instead of the file (a document viewer, a sign-in page) is converted as an HTML document and charged. Use direct download links, or remove `html` from `formats` so such pages fail with `UNSUPPORTED_FORMAT` and are not charged.
- **Links**: only public http(s) addresses are fetched, and every redirect is checked the same way. Links to private, local or cloud-metadata addresses, and links carrying a user name or password, fail with `BLOCKED_DESTINATION` and are not charged. No cloud-drive sign-in: links must be downloadable without logging in.
- **Per run**: at most 1,000 listed sources plus up to 1,000 records read from a key-value store; files up to `maxFileSizeMb` but never more than a quarter of the run's memory.

### FAQ

**Is my file sent to an AI service?** No. The conversion runs inside the Actor's container with open-source parsers; nothing is sent to a language model or any other outside service.

**Why is a document `partial`?** Some content may be missing. The `warnings` list gives the reason codes, for example an unreadable EPUB chapter or email attachments over your maximum charge. A partial document is charged like a complete one, because it produced content.

**Why did a file fail with `NO_CONTENT`?** It was readable but had no text: only pictures, empty slides, empty or hidden sheets. Its warnings say which. It is not charged.

**Are my spreadsheet formulas recalculated?** No. The table shows the values saved in the file, and for XLSX and XLSM the formula text is listed after the sheet's table and in the table row's `formulas`.

**Can I read the output from code?** Yes. See [Use it from code](#use-it-from-code): filter the dataset items on `record_type`, or download the `.md` and `.csv` files from the run's key-value store.

**Something does not work.** Open an issue on the Actor's Issues tab with the run link and, if you can share it, a file that shows the problem. Issues are read on a best-effort basis; there is no guaranteed response time.

### Privacy and data retention

The Actor reads only the URLs and key-value store records you give it. Your files are processed inside your run and are not sent anywhere else. The log contains counts, status and error codes, never file content, email addresses or URLs. The Actor keeps nothing after the run ends: the outputs live in your run's dataset and key-value store under your Apify account, and Apify's storage retention applies to them.

### More Actors from the same developer

These Actors share one output format for chunks, so their results can go into the same index:

- [PDF to Markdown & RAG Chunks](https://apify.com/bongo-seakhoa/pdf-to-markdown-rag-chunks): PDFs, tables to CSV, offline OCR.

### About the sample and the tests

The bundled sample and every test file used to develop this Actor are synthetic: generated for the purpose and labelled synthetic (see `sample/PROVENANCE.md` in the source). No third-party files were used.

### Licences

The Actor is proprietary software by Bongo Seakhoa. It is built on open-source components under permissive licences (mammoth, markdownify, Beautiful Soup, lxml, python-pptx, python-calamine, charset-normalizer and defusedxml, among others). certifi (MPL-2.0) is included unmodified. No GPL, LGPL or AGPL library is used. The full list and licence texts are in the `THIRD_PARTY_NOTICES` file shipped with the Actor image.

# Actor input Schema

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

Direct links (http or https) to the files to convert. Only public internet addresses are fetched. The format is detected from the file's content, not its name. A link that returns a web page (a viewer or a sign-in page) is converted as HTML and charged: use direct download links, or remove HTML from the formats below.

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

Pick a key-value store that holds your files (upload them in Apify Console under Storage). Without record keys below, every record whose key ends in a supported extension (.docx, .pptx, .xlsx, .xls, .ods, .csv, .tsv, .epub, .html, .eml ...) or has a matching content type is converted, up to 1,000.

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

Keys of the records to convert from the store picked above. Leave empty to use every supported record.

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

'convert' produces Markdown, chunks and tables. 'estimate' reads every file, counts the documents that would be charged (email attachments included) and shows the projected price, without writing outputs. It reads at most 100 files per run, each for at most 30 seconds, and charges none of this Actor's events (Apify's standard Actor start fee still applies).

## `outputs` (type: `array`):

Which outputs to produce. Markdown goes to the document row and to a .md file; chunks become their own rows; tables and sheets become table rows plus one CSV file each. Per document, at most 500 chunk rows and 30 tables or sheets are written as rows and CSV files; everything is in the Markdown.

## `formats` (type: `array`):

Only files of these formats are converted; others fail with UNSUPPORTED_FORMAT and are not charged. Also limits which email attachments are converted.

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

Maximum characters per RAG chunk. Chunks follow headings, paragraphs and table rows, and a chunk inside a long table repeats the table's header row in table_header.

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

Characters repeated from the end of the previous chunk, at most half the chunk size. No overlap is added across a heading.

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

Adds each slide's speaker notes as a 'Notes:' block after the slide's content.

## `maxRowsPerSheetMarkdown` (type: `integer`):

A sheet's Markdown table shows at most this many rows; the CSV file always has every row.

## `schemaSummary` (type: `boolean`):

Starts each sheet with its columns: inferred type (int, float, date, bool, text), share of empty cells, distinct values over the first 10,000 rows, and 3 sample values.

## `headerRow` (type: `integer`):

Which row of a sheet's used range holds the column names (1 = the first row). Rows above it are kept as text. 0 means the sheet has no header row: columns are named column_1, column_2 ...

## `skipHiddenSheets` (type: `boolean`):

Hidden sheets are listed with a placeholder but not converted. Untick to convert them too.

## `convertEmailAttachments` (type: `boolean`):

Attachments of an EML file in a supported format become their own document rows (one level deep), each charged as a document. PDF attachments are listed, not converted.

## `maxAttachmentsPerEmail` (type: `integer`):

At most this many attachments of one email are converted; the rest are listed.

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

Larger files fail with FILE_TOO_LARGE and are not charged. The limit is also capped at a quarter of the run's memory.

## `perFileTimeoutSecs` (type: `integer`):

A file still converting after this time fails with TIMEOUT and is not charged. An email's attachments share their email's limit. Estimate mode uses at most 30 seconds.

## Actor input object example

```json
{
  "sources": [],
  "keyValueStoreKeys": [],
  "mode": "convert",
  "outputs": [
    "markdown",
    "chunks",
    "tables_csv"
  ],
  "formats": [
    "docx",
    "pptx",
    "xlsx",
    "xls",
    "ods",
    "csv",
    "epub",
    "html",
    "eml"
  ],
  "chunkSize": 1500,
  "chunkOverlap": 150,
  "includeSpeakerNotes": true,
  "maxRowsPerSheetMarkdown": 1000,
  "schemaSummary": true,
  "headerRow": 1,
  "skipHiddenSheets": true,
  "convertEmailAttachments": true,
  "maxAttachmentsPerEmail": 20,
  "maxFileSizeMb": 100,
  "perFileTimeoutSecs": 120
}
```

# Actor output Schema

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

One row per document (record_type 'document', email attachments included), per chunk ('chunk') and per table or sheet ('table').

## `files` (type: `string`):

Full Markdown per document and every table or sheet as CSV, in the run's key-value store, plus the SUMMARY record.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("bongo-seakhoa/office-to-markdown-rag-chunks").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("bongo-seakhoa/office-to-markdown-rag-chunks").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 '{}' |
apify call bongo-seakhoa/office-to-markdown-rag-chunks --silent --output-dataset

```

## MCP server setup

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

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/c4xSdFQGJy6ZqUErv/builds/HnN43LOeqOcQE3eCW/openapi.json
