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

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

Pricing

from $10.00 / 1,000 document converteds

Go to Apify Store
Word, PowerPoint, Excel, EPUB & Email to Markdown + RAG Chunks

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

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.

Pricing

from $10.00 / 1,000 document converteds

Rating

0.0

(0)

Developer

Bongo Seakhoa

Bongo Seakhoa

Maintained by Community

Actor stats

1

Bookmarked

2

Total users

1

Monthly active users

22 days ago

Last modified

Categories

Share

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). 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

{
"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
}
FieldDefaultWhat it does
sources[]File URLs (http or https), as {"url": ...} objects or plain strings.
keyValueStoreId, keyValueStoreKeysnoneRead 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.
modeconvertestimate 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.
outputsall threeAny of markdown, chunks, tables_csv.
formatsall nineConvert only these formats; other files fail with UNSUPPORTED_FORMAT and are not charged. Also limits which email attachments are converted.
chunkSize, chunkOverlap1500, 150Characters (about 4 per token). Overlap is at most half the chunk size.
includeSpeakerNotestrueAdds each slide's speaker notes as a Notes: block.
maxRowsPerSheetMarkdown1000Rows of a sheet shown in the Markdown (10 to 20,000). The CSV file has all rows.
schemaSummarytrueStarts each sheet with its schema summary.
headerRow1Row 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.
skipHiddenSheetstrueHidden sheets get an [hidden sheet: not converted] placeholder. Untick to convert them.
convertEmailAttachmentstrueConvert supported attachments of .eml files as their own documents (one level deep).
maxAttachmentsPerEmail20At most this many attachments per email are converted (0 to 20); the rest are listed.
maxFileSizeMb100Larger files fail with FILE_TOO_LARGE. Never more than a quarter of the run's memory.
perFileTimeoutSecs120Time 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:

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):

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:

{
"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:

{
"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:

{
"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 typeFieldWhat it holds
documentsource, source_typeThe URL or kvs://store/key it came from, and url, kvs or sample.
documentstatus, error_code, error_messageok, partial, failed or skipped, and why (codes below).
documentfile_name, file_size_bytes, sha256, formatThe file as read, and its detected format.
documenttitle, author, slide_count, sheet_count, chapter_countFrom the file's own metadata and structure, where it has them.
documentmarkdown, markdown_chars, markdown_urlThe Markdown: in the row while it fits, always in the .md file.
documentchunk_count, table_countChunk and table rows written for this document.
documentparent_document_id, attachment_name, attachmentsLinks between an email and its attachments, and the email's attachment list.
documentwarnings, charged_events, mode, projected_cost_usd, elapsed_msReason codes, what was charged, and the estimate-mode price.
documentfields_omittedLarge fields left out to fit the platform's row size limit; the files still hold them.
chunkchunk_id, chunk_index, text, char_start, char_end, approx_tokensThe chunk, its offsets in the Markdown and a rough token count (characters divided by 4).
chunkheading_path, locator_start, locator_end, page_start, page_endWhere it is: headings, source locators, and pages (always null here).
chunktable_ids, table_header, formatTables it overlaps, and the header row of the table it continues.
tabletable_id, table_index, kind, title, locatorWhich table or sheet, and where it is in the file.
tableheader, header_generated, n_rows, n_cols, rows_in_markdownIts shape, and how many rows the Markdown shows.
tableschema_summary, formula_count, formulas, csv_urlSheet 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 codeMeaning
UNSUPPORTED_FORMATNot a supported format, or excluded by formats. The message names the format and, for PDFs, the PDF Actor.
FILE_ENCRYPTEDA password-protected or encrypted Office file.
DRM_PROTECTEDAn EPUB book with DRM.
CORRUPT_FILEA damaged or unreadable file, or an XML part that declares a DTD.
ARCHIVE_TOO_LARGEThe Office or EPUB container has more than 2,000 entries or unpacks to too much data.
NO_CONTENTReadable, but no text: only empty, hidden or image content (the warnings say which).
INVALID_INPUTA key-value store or record could not be used as given, or an estimate run reached its 100 files.
DOWNLOAD_FAILEDThe URL or record could not be fetched (an HTTP error, a network error, too slow).
BLOCKED_DESTINATIONThe link, or a redirect, points to a private, local or cloud-metadata address, or carries a user name or password.
FILE_TOO_LARGEOver maxFileSizeMb, or over a quarter of the run's memory.
TIMEOUTStill converting at perFileTimeoutSecs. On a skipped row: the run was about to reach its own timeout.
OUT_OF_MEMORYThe file needed more memory than the run has; try 2048 MB.
WORKER_CRASHEDThe converter process stopped unexpectedly on this file.
INTERNAL_ERRORA bug or an unexpected library error on this file. Please report it.
CHARGE_LIMITOn 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:

FormatLocator fields
DOCXparagraph_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.
PPTXslide, 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, ODSsheet and cell_range in A1 notation, down to the single row of a table line or the single cell of a formula.
CSV, TSVcell_range in A1 notation (record numbers, as a spreadsheet would show them).
EPUBspine_index and spine_item (the chapter file); the chunk's heading_path starts with the chapter title. Table rows add table and row.
EMLpart: headers, body or attachments (with attachment naming the file in the attachment list).
HTMLthe 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.

EventPriceWhen 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 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: 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:

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.