How to Ingest XLSX into LangChain with EnConvert

Use EnConvert to ingest XLSX files into LangChain as structured chunks where tables stay whole, ready to load as Document objects and add to a vector store

Get API key

A developer building a LangChain agent or RAG pipeline that reads XLSX files needs clean, structured content from each sheet, not flat row-by-row text that loses column context. With openpyxl, you get cell values row by row with no association to their column headers. With pandas, the headers survive on the DataFrame, but you still decide how to serialise rows into text, how to handle multiple sheets, and where chunk boundaries fall, so the content that reaches LangChain has no structural anchors for retrieval. For an agent reasoning over a financial model, a product catalogue, or a data export, a row of bare values without column names or sheet context is not a useful retrieval unit. EnConvert’s Ingest endpoint takes the XLSX upload, converts the spreadsheet into Markdown, and returns JSONL chunks where each table stays whole rather than being split mid-row, with headings_path and section metadata preserved, so XLSX content is ready for embedding and retrieval through one endpoint.

Example

python
import json
import os
import time

import requests
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings

ENCONVERT_API_KEY = os.environ["ENCONVERT_API_KEY"]
XLSX_PATH = "/path/to/your/financial-model.xlsx"

# Step 1: Upload the XLSX file (multipart/form-data, field name "files")
with open(XLSX_PATH, "rb") as xlsx_file:
    upload = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers={"X-API-Key": ENCONVERT_API_KEY},
        files={
            "files": (
                os.path.basename(XLSX_PATH),
                xlsx_file,
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            )
        },
        timeout=120,
    )
upload.raise_for_status()
job_id = upload.json()["job_id"]

# Step 2: Poll until the ingest job finishes
while True:
    status_response = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers={"X-API-Key": ENCONVERT_API_KEY},
        timeout=60,
    )
    status_response.raise_for_status()
    job = status_response.json()
    if job["status"] == "completed":
        break
    if job["status"] in ("failed", "canceled"):
        raise RuntimeError(
            f"Ingest job {job_id} {job['status']}: {job.get('error_message')}"
        )
    time.sleep(5)

# Step 3: Download the JSONL output, one chunk per line.
# output_url is a signed URL (valid 15 minutes). Plain GET, no API key.
jsonl_response = requests.get(job["output_url"], timeout=60)
jsonl_response.raise_for_status()
chunks = [
    json.loads(line)
    for line in jsonl_response.content.splitlines()
    if line.strip()
]

# Step 4: Turn each chunk into a LangChain Document.
# For an XLSX file, each worksheet becomes a section named after the sheet.
documents = [
    Document(
        id=chunk["id"],
        page_content=chunk["content"],
        metadata=chunk["metadata"],
    )
    for chunk in chunks
]
print("Sections:", sorted({doc.metadata["section"] for doc in documents}))

# Step 5: Embed the Documents into a vector store
# (InMemoryVectorStore needs numpy installed for cosine search)
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large",
    api_key=os.environ["OPENAI_API_KEY"],
)
vector_store = InMemoryVectorStore(embeddings)
vector_store.add_documents(documents)

# Step 6: Search only inside one section (sheet)
SECTION = "Income Statement"  # replace with a sheet name from your own workbook
results = vector_store.similarity_search(
    "What is the projected revenue growth?",
    k=3,
    filter=lambda doc: doc.metadata.get("section") == SECTION,
)

for doc in results:
    print(doc.metadata["headings_path"], doc.page_content[:200])

What you get back

The Ingest endpoint converts the XLSX file to Markdown and returns JSONL chunks where each table stays whole, with headings_path and section metadata on every chunk so retrieval can be filtered by section. Reading the same file with openpyxl or pandas and serialising rows as plain text discards table structure, produces no meaningful chunk boundaries, and populates no LangChain metadata automatically.

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