# DOCX to Text & Markdown Converter (`plainpipe/docx-to-text`) Actor

Convert Word .docx files to Markdown, plain text, HTML or JSON, with tables as rows, images, properties and RAG chunks. Use when you need Word content as text.

- **URL**: https://apify.com/plainpipe/docx-to-text.md
- **Developed by:** [Plain Pipe](https://apify.com/plainpipe) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 results

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

## DOCX to Text & Markdown Converter

Convert Word .docx files to Markdown, plain text, HTML or a JSON block structure, with tables as structured rows, embedded images, document properties, headers and footers, footnotes, review comments, and optional page, section or RAG-chunk splitting.

**Use this when:** you need the contents of Word .docx files as clean text or Markdown, for example to feed an LLM, a RAG pipeline, a search index or a wiki.

### Why this one

- **$3.00 per 1,000 results**, one result per document (or per page, section or chunk if you turn splitting on). Files that fail, are not .docx or hold no text are not charged.
- **More than text:** tables come out as rows of cells (`tables`), footnotes become real Markdown footnotes (`[^1]`), pictures are saved and linked, and you can add document properties, headers/footers and review comments, each with one checkbox.
- **Clear answers for bad files:** the file type is checked from its bytes, so an old .doc, an encrypted/protected .docx, an Excel or PowerPoint file, a PDF, an RTF or a web page each gets its own message telling you what to do, and the run fails with 0 results if nothing could be converted.
- **Fast and light:** pure Node.js (mammoth plus a small ZIP reader), no browser, no proxy, no Word install. Four files are fetched and converted in parallel; each test file in this README converts in under 2 seconds per run. The prefilled input is test-run daily.

### What it does

For every Word file you give it, the Actor:

1. Fetches the file (links, an uploaded file, links listed in an Apify dataset, or files stored in a key-value store), up to 50 MB each.
2. Checks what the file really is. Only real .docx/.docm/.dotx files are converted; everything else is skipped with a reason.
3. Converts it with the open-source mammoth library and builds its own block list: headings (from Word heading styles), paragraphs, bulleted and numbered lists (also nested), tables, footnotes and endnotes.
4. Writes one dataset row per document, or per page, section or chunk, in the formats you picked: **Markdown**, **plain text**, **HTML** and/or a **JSON structure** of blocks, plus word, character, heading and table counts.

### Use cases

- Turn Word policies, specs and manuals into Markdown chunks with heading paths for a RAG pipeline or vector database (`chunkWords`).
- Pull every table out of a batch of Word reports as rows you can load into a spreadsheet or database (`tables`).
- Move .docx documentation into a wiki, static site or Git repository as Markdown with its images (`extractImages`).
- Index Word files for search with their title, subject, keywords and dates (`includeMetadata`).
- Convert the .docx links found by another Actor in one step by pointing at its dataset (`datasetId`).

### How to use it

1. Paste direct .docx links into **DOCX URLs**, upload a file, or fill **Dataset ID** or **Key-value store ID** to convert files in bulk.
2. Pick the **Output formats** and turn on the extras you need (tables, images, properties, headers/footers, comments).
3. Optional: choose **Split each document** (by page or by heading) and/or a **Chunk size (words)**.
4. Click **Start**. Export the rows as JSON, CSV or Excel, or read them through the Apify API.

### Input

| Field | What it is | Default | Example |
|---|---|---|---|
| `docxUrls` | Direct links to .docx files, one per line. Dropbox share links with `?dl=0` are fixed automatically. | none | `["https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"]` |
| `docxFile` | One uploaded .docx file, instead of or in addition to links. | none | a file picked in the input form |
| `datasetId` | Batch input: ID or name of an Apify dataset whose items hold .docx links. | none | `"aBcD1234efGh5678"` |
| `datasetUrlField` | Field of each dataset item that holds the link. Dot paths and lists of links work. | `url` | `"file.url"` |
| `keyValueStoreId` | Batch input: ID or name of a key-value store with .docx files as records. Keys ending in .docx are used; if none do, every record is tried. | none | `"my-word-files"` |
| `keyValueStoreKeys` | Only these record keys from that store. | all .docx keys | `["contract.docx"]` |
| `outputFormats` | Any of `markdown`, `text`, `html` (whole documents only), `structure` (JSON blocks). | `["markdown","text"]` | `["markdown","structure"]` |
| `extractTables` | Add `tables`: every table as rows of cell text, merged cells padded so columns line up. | `true` | `false` |
| `extractImages` | Save pictures to the run's key-value store and link them from the Markdown. | `false` | `true` |
| `includeMetadata` | Add `metadata` from the file's own properties (title, subject, keywords, description, author, language, dates, revision, saved page/word counts, application). Off by default because author is a document property. | `false` | `true` |
| `includeHeadersFooters` | Add `headers` and `footers` (distinct text of each). | `false` | `true` |
| `includeFootnotes` | Keep footnotes and endnotes. Off drops them and their reference marks. | `true` | `false` |
| `includeComments` | Add `comments`: text of each review comment and the passage it marks. Comment authors are never output. | `false` | `true` |
| `splitBy` | `document` (one row per file), `heading` (one row per Heading 1-3 section) or `page` (one row per page). | `document` | `"page"` |
| `chunkWords` | 0 = off. Otherwise cut each document, section or page into chunks of about this many words, between paragraphs and never right after a heading. | `0` | `300` |
| `maxItems` | Stop after this many rows in total. | `100` | `10` |

### Output

One row per document by default. Real output of the prefilled input:

```json
{
  "source": "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx",
  "fileName": "test.docx",
  "markdown": "# python-docx was here!\n\npython-docx was here too!",
  "text": "python-docx was here!\n\npython-docx was here too!",
  "words": 7,
  "characters": 48,
  "headings": 1,
  "tableCount": 0,
  "tables": [],
  "fileSizeBytes": 31773
}
```

Below are real results for each option, from the test files in `tests/inputs` (public sample files of the mammoth, python-docx and Apache POI projects). Long values are shortened with `…`.

**Tables as rows** (`extractTables`, on by default), a small file with one table:

```json
{
  "markdown": "Above\n\n| Top left | Top right |\n| --- | --- |\n| Bottom left | Bottom right |\n\nBelow",
  "text": "Above\n\nTop left\tTop right\nBottom left\tBottom right\n\nBelow",
  "tableCount": 1,
  "tables": [{ "index": 0, "rowCount": 2, "columnCount": 2, "rows": [["Top left", "Top right"], ["Bottom left", "Bottom right"]] }]
}
```

**Footnotes and the JSON structure** (`"outputFormats": ["markdown", "html", "structure"]`):

```json
{
  "markdown": "Ouch[^1].[^2]\n\n[^1]: A tachyon walks into a bar.\n\n[^2]: Fin.",
  "html": "<p>Ouch<sup><a href=\"#footnote-1\" id=\"footnote-ref-1\">[1]</a></sup>.…</p><ol><li id=\"footnote-1\">…</li></ol>",
  "blocks": [
    { "type": "paragraph", "text": "Ouch[1].[2]" },
    { "type": "footnote", "label": "1", "text": "[1] A tachyon walks into a bar." },
    { "type": "footnote", "label": "2", "text": "[2] Fin." }
  ]
}
```

With `"includeFootnotes": false` the same file gives `"markdown": "Ouch."`.

**Split by page plus headers and footers** (`"splitBy": "page", "includeHeadersFooters": true`), a two-page file. The first row also carries the document-level fields:

```json
[
  { "page": 1, "partIndex": 0, "parts": 2, "markdown": "This is a fairly simple word document, over two pages, with headers and footers.\n\n…\n\n# Molière\n\n…\n\nThat’s it for page one", "words": 65,
    "headers": ["This is a simple header, with a € euro symbol in it."],
    "footers": ["The footer, with Molière, has Unicode in it."] },
  { "page": 2, "partIndex": 1, "parts": 2, "markdown": "This is page two. *Les Précieuses ridicules.* The end.", "words": 9 }
]
```

**Document properties** (`"includeMetadata": true`; the author value is left out here):

```json
{
  "metadata": {
    "title": "Test Doc",
    "subject": "Testing is about all",
    "keywords": "Test word",
    "description": "These are our document's comments",
    "created": "2010-09-14T16:04:00Z",
    "modified": "2010-09-14T16:04:00Z",
    "revision": 2,
    "pagesSaved": 1,
    "wordsSaved": 32,
    "application": "Microsoft Office Word"
  }
}
```

**Review comments** (`"includeComments": true`):

```json
{ "markdown": "Ouch.", "comments": [{ "id": "0", "text": "A tachyon walks into a bar.", "quotedText": "Ouch" }, { "id": "2", "text": "Fin." }] }
```

**Images** (`"extractImages": true`). On Apify the link points at the record in the run's key-value store, `https://api.apify.com/v2/key-value-stores/<storeId>/records/doc0001-image-1.png`:

```json
{
  "markdown": "![](…/records/doc0001-image-1.png)",
  "text": "[image]",
  "images": [{ "key": "doc0001-image-1.png", "url": "…/records/doc0001-image-1.png", "contentType": "image/png", "bytes": 110 }]
}
```

**Split by heading** (`"splitBy": "heading"`) on a 1.3 MB public Word demo file gave 16 rows; one of them:

```json
{ "partIndex": 4, "parts": 16, "section": "Tables", "headingPath": ["Tables"],
  "markdown": "# Tables\n\n| ITEM | NEEDED |\n| --- | --- |\n| Books | 1 |\n| Pens | 3 |\n…", "words": 367, "tableCount": 5 }
```

**Chunks for RAG** (`"chunkWords": 150`) on the same file gave 13 rows of 37 to 150 words; one of them:

```json
{ "chunkIndex": 1, "chunks": 13, "headingPath": ["Text Formatting", "Inline formatting"],
  "text": "Here is some bold, italic, bold-italic, underlined and struck out text. Then, we have a superscript and a subscript. …", "words": 138 }
```

| Field | Meaning |
|---|---|
| `source` / `fileName` | The link or `key-value-store:<id>/<key>` the document came from, and its file name. |
| `markdown` / `text` / `html` / `blocks` | The content in the formats you picked. |
| `page`, `section`, `headingPath`, `chunkIndex`, `chunks`, `partIndex`, `parts` | Where this row sits in the document when splitting or chunking. |
| `words`, `characters`, `headings`, `tableCount` | Counts for this row. |
| `tables` | Tables in this row as rows of cell text. |
| `metadata`, `headers`, `footers`, `comments`, `images`, `fileSizeBytes` | Document-level fields, on the first row of each document only. |
| `truncated`, `markdownUrl`, `textUrl`, `htmlUrl` | Only for very large documents: a row near the dataset size limit keeps the first 100,000 characters and links the whole text in the key-value store. |

### Pricing

Pay per result: **$3.00 per 1,000 results** ($0.003 per result). You only pay for rows the Actor returns. Set a maximum total charge on the run to cap your spend.

One result is one dataset row: a whole document by default, or one page, section or chunk when you split. Skipped files are not charged.

### Limits

- Only .docx (Word 2007 and newer, also .docm and .dotx). Old .doc, .odt, .rtf, PDF and encrypted/protected files are detected and skipped with a message, not charged.
- Headings come from Word heading styles; text that only looks like a heading (big bold font) stays a paragraph.
- Page split uses the page breaks stored in the file: manual page breaks and the layout Word recorded at the last save. Files written by other tools often store no layout, so they come out as one page.
- Tables lose colours and layout; merged cells are padded with empty cells. Text boxes and shapes may be missed.
- HTML output is only given for whole documents (not with splitting or chunking).
- Files up to 50 MB. Links must return the file itself; redirects are followed and failing servers are retried once.

### FAQ

**How much does it cost to convert 1,000 documents?**
$3.00 with the default one row per document. With page, section or chunk splitting you pay per row, so check `maxItems`.

**Why was my file skipped with "legacy Word 97-2003 .doc"?**
The file uses the old binary Word format. Open it in Word or LibreOffice, choose Save as > Word Document (.docx) and run again.

**Why "encrypted/protected"?**
The document can only be opened with a secret in Word. Remove the protection (File > Info > Protect Document) and save a normal copy.

**Can I get tables as data rather than text?**
Yes. `tables` holds each table as an array of rows, each row an array of cell strings, next to the Markdown table.

**Is personal data in the output?**
Only what is in your documents. Document properties (including author) are off by default, the last-modified-by property is never output, and comment authors are never output.

**Can I convert the results of another Actor?**
Yes. Put its dataset ID into `datasetId` and the name of the link field into `datasetUrlField`.

**Can I run it on a schedule or from my own code?**
Yes. Use Apify schedules, the API, the JavaScript or Python client, or integrations such as Make, Zapier and webhooks.

### Legal and data

This Actor processes only what you give it. It does not log in anywhere, does not collect personal data, and does not bypass paywalls, CAPTCHAs or access controls. Document properties such as author are only read when you turn on `includeMetadata`. You are responsible for having the right to process the inputs you provide.

### Support

Open an issue on the Issues tab. Issues are answered within 14 days. Please include the run ID and, if you can, a sample .docx that gave the wrong result.

Other Plain Pipe document tools: PDF to Markdown Converter, EPUB to Markdown & Text Converter, PDF Merge & Split Tool, PDF Watermark & Page Number Tool, Markdown to HTML Converter, Markdown Table Converter (CSV, JSON, HTML).

Built and maintained by Plain Pipe.

# Actor input Schema

## `docxUrls` (type: `array`):

Direct links to Word .docx files, one per line. Dropbox share links ending in ?dl=0 are fixed automatically. Example: https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx. Old .doc files are detected and skipped with a hint to save them as .docx.

## `docxFile` (type: `string`):

Or upload one .docx file instead of (or in addition to) the links above, up to 50 MB. Leave empty when you only use DOCX URLs. Example: policy.docx.

## `datasetId` (type: `string`):

Optional. ID or name of an Apify dataset whose items hold .docx links, for example the output of another Actor. Every item's "Dataset URL field" is read. Example: aBcD1234efGh5678.

## `datasetUrlField` (type: `string`):

Field of each dataset item that holds the link (dot path for nested fields; a list of links also works). Default: url. Example: file.url.

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

Optional. ID or name of an Apify key-value store that holds .docx files as records. Records whose key ends in .docx are converted; if none do, every record is tried. Example: my-word-files.

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

Optional. Only convert these record keys from the key-value store above. Leave empty to pick the records automatically. Example: \["contract.docx", "offer.docx"].

## `outputFormats` (type: `array`):

Which versions of the content each row gets: markdown (headings, lists, links, tables), text (plain text), html (mammoth HTML, whole documents only) and structure (JSON list of blocks: heading/paragraph/list/table/footnote). Default: \["markdown", "text"].

## `extractTables` (type: `boolean`):

Add a tables field with every Word table as rows of cell text (array of arrays; merged cells padded so columns line up). Default: true.

## `extractImages` (type: `boolean`):

Save every embedded picture to the run's key-value store and link it from the Markdown as ![alt](link). The first row of each document lists them in images (key, link, type, size). Default: false.

## `includeMetadata` (type: `boolean`):

Add a metadata field from the file's own properties: title, subject, keywords, description, author, language, created and modified dates, revision, saved page and word counts, application. Off by default because author is a property of the document; turn on only if you need it. Default: false.

## `includeHeadersFooters` (type: `boolean`):

Add headers and footers fields with the distinct text of the page headers and footers. Default: false.

## `includeFootnotes` (type: `boolean`):

Keep footnotes and endnotes: references become \[^1] in Markdown and \[1] in text, with the notes listed at the end. Turn off to drop them. Default: true.

## `includeComments` (type: `boolean`):

Add a comments field with the text of each Word review comment and the passage it refers to (quotedText). Comment authors are never output. Default: false.

## `splitBy` (type: `string`):

document = one row per file (default). heading = one row per section, cut at Heading 1-3, with section and headingPath. page = one row per page, using the page breaks Word stored in the file (manual breaks and the layout of the last save).

## `chunkWords` (type: `integer`):

0 = off (default). When set, each document (or section/page) is split into chunks of about this many words, cut between paragraphs and never right after a heading, one row per chunk with chunkIndex and headingPath. Each chunk is one billed result. Example: 300 for RAG embeddings.

## `maxItems` (type: `integer`):

Stop after this many rows in total (documents, or sections/pages/chunks when splitting). You are charged per row. Default 100. Example: 10.

## Actor input object example

```json
{
  "docxUrls": [
    "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"
  ],
  "datasetUrlField": "url",
  "outputFormats": [
    "markdown",
    "text"
  ],
  "extractTables": true,
  "extractImages": false,
  "includeMetadata": false,
  "includeHeadersFooters": false,
  "includeFootnotes": true,
  "includeComments": false,
  "splitBy": "document",
  "chunkWords": 0,
  "maxItems": 10
}
```

# Actor output Schema

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

No description

# 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 = {
    "docxUrls": [
        "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainpipe/docx-to-text").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 = {
    "docxUrls": ["https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("plainpipe/docx-to-text").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 '{
  "docxUrls": [
    "https://raw.githubusercontent.com/python-openxml/python-docx/master/tests/test_files/test.docx"
  ],
  "maxItems": 10
}' |
apify call plainpipe/docx-to-text --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plainpipe/docx-to-text"
        }
    }
}
```

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/PyHD2nUkafro6KXGz/builds/ptixHp6761hXjlFSg/openapi.json
