---
title: "Ingest XLSX into LangChain using EnConvert Ingest"
description: "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"
canonical: "https://www.enconvert.com/solutions/ingest-xlsx-into-langchain-using-enconvert-ingest"
locale: "en"
---

# How to Ingest XLSX into LangChain with EnConvert

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])
```

## Output

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 started

[Get free API key — 500 ops per month, no card required.](https://www.enconvert.com/auth?mode=signup)

## Related solutions

- [How to Ingest Markdown Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-files-into-llamaindex-with-enconvert.md)
- [How to Ingest Web Pages into Vertex AI with EnConvert](https://www.enconvert.com/solutions/ingest-web-pages-into-vertex-ai-with-enconvert.md)
- [How to Ingest Markdown into OpenAI File Search with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-into-openai-file-search-enconvert.md)
- [How to Ingest HTML Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-html-files-into-llamaindex-with-enconvert.md)
- [Best Browserless Alternative for Document Parsing](https://www.enconvert.com/solutions/browserless-alternative-for-document-parsing-enconvert.md)
- [How to Ingest DOCX into CrewAI with EnConvert](https://www.enconvert.com/solutions/ingest-docx-into-crewai-with-enconvert.md)
