How to Ingest DOCX into LangChain

Use EnConvert's Ingest endpoint to extract DOCX files as clean markdown for LangChain pipelines preserving headings, tables, and lists without custom parsing

Get API key

Building a LangChain document pipeline over Word files means extracting clean text from DOCX files that were authored without extraction in mind: contracts with tracked changes, reports with embedded tables, onboarding documents with mixed heading styles and inline objects. DOCX files store content as XML with style definitions, revision history, and embedded object references interspersed throughout, and most extraction libraries surface some or all of that noise in the output. Tables are the most consistent failure point: the cell content is present but the structure is lost, which breaks any retrieval logic that depends on reading a row as a unit. EnConvert's Ingest endpoint takes any DOCX file and returns clean markdown with headings and table structure preserved, eliminating the need for manual XML traversal or custom rendering before the content reaches your LangChain pipeline.

Example

python
import requests
from langchain_core.documents import Document
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore

ENCONVERT_API_KEY = "sk_your_enconvert_api_key"
OPENAI_API_KEY = "sk-your_openai_api_key"

#Step 1: Extract the DOCX as clean markdown

with open("[•path to your DOCX file]", "rb") as docx:
  response = requests.post(
    "https://api.enconvert.com//v2/ingest/files",
       headers={
         "Authorization": f"Bearer {ENCONVERT_API_KEY}",
       },
       files={
           "file": (
               "[•DOCX filename]",
               docx,
               "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
           ),
       },
)

response.raise_for_status()

# The API returns JSON metadata with a pre-signed URL to the .md file
markdown_response = requests.get(response.json()["presigned_url"])
markdown_response.raise_for_status()
markdown = markdown_response.text

# Optional: preview the extracted markdown before splitting
print(markdown)

# Step 2: Wrap the markdown as a LangChain Document
document = Document(
    page_content=markdown,
    metadata={"source": "[•DOCX filename]"},
# Step 3: Split using a markdown-aware splitter
# from_language loads markdown separators so the splitter
# respects heading boundaries before falling back to
# paragraph and word breaks
splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.MARKDOWN,
    chunk_size=1000,
    chunk_overlap=100,
)

chunks = splitter.split_documents([document])

# Step 4: Embed and add to a vector store
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    api_key=OPENAI_API_KEY,
)

# InMemoryVectorStore ships with langchain-core and needs no setup
# Swap this line for your store — add_documents() is identical across all of them
vector_store = InMemoryVectorStore(embeddings)

vector_store.add_documents(chunks)
print(f"Indexed {len(chunks)} chunks from DOCX")

What you get back

The Ingest endpoint returns DOCX content as structured markdown: headings mapped to ATX heading levels, body paragraphs as plain text, tables rendered as markdown tables with cell boundaries preserved, and list content as markdown lists, with tracked changes, style metadata, revision author information, and embedded object references stripped before the response is returned. python-docx gives programmatic access to the document XML but requires manual traversal of paragraph and table nodes to assemble readable output, custom rendering logic to convert table cells into any structured format including handling for merged cells, and produces raw concatenated text with no heading hierarchy or list formatting by default. The gap between what python-docx returns and what a LangChain splitter can work with cleanly is where the preprocessing burden sits.

Get free API key — 500 ops per month, no card required