---
title: "Ingest XLSX into OpenAI Agents SDK with EnConvert"
description: "Use EnConvert to ingest XLSX into OpenAI Agents SDK as Markdown table chunks that stay whole, with section metadata for an agent to retrieve and reason over"
canonical: "https://www.enconvert.com/solutions/ingest-xlsx-into-openai-agents-sdk-with-enconvert"
locale: "en"
---

# How to Ingest XLSX into OpenAI Agents SDK with EnConvert

A developer building an OpenAI Agents SDK agent that reads XLSX files needs structured chunks where each table stays whole, not flat row text that loses the relationship between values and their column headers. Reading an XLSX with openpyxl or pandas and serialising rows as plain text either drops the column names or repeats them on every row, with no meaningful chunk boundaries. Nothing populates metadata the agent can retrieve and filter on. For an agent reasoning over a financial model, a product catalogue, or a data export, bare row values without column context are not useful retrieval units. EnConvert’s Ingest endpoint takes the uploaded XLSX, converts it to Markdown, and returns JSONL chunks where each table stays whole, with headings_path and section metadata on every chunk, so the content is ready to expose to an OpenAI Agents SDK agent as a function tool in one call.

## Example

```python
import json
import os
import re
import time

import requests
from agents import Agent, Runner, function_tool

# The Agents SDK reads OPENAI_API_KEY from the environment.
HEADERS = {"X-API-Key": os.environ["ENCONVERT_API_KEY"]}

# Step 1: Upload the XLSX
# Each sheet becomes a "## SheetName" heading plus a Markdown table, and tables
# are kept whole as chunks.
with open("/path/to/your/product-catalogue.xlsx", "rb") as f:
    upload = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers=HEADERS,
        files={
            "files": (
                "product-catalogue.xlsx",
                f,
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            )
        },
        timeout=60,
    )
upload.raise_for_status()
job_id = upload.json()["job_id"]

# Step 2: Poll until the job completes, failing if it never finishes
deadline = time.monotonic() + 600
while True:
    poll = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers=HEADERS,
        timeout=60,
    )
    poll.raise_for_status()
    job = poll.json()
    if job["status"] == "completed":
        output_url = job["output_url"]
        break
    if job["status"] in ("failed", "canceled"):
        raise RuntimeError(
            job.get("error_message") or f"Ingest job {job_id} {job['status']}"
        )
    if time.monotonic() > deadline:
        raise RuntimeError(f"Ingest job {job_id} still {job['status']} after 10 minutes")
    time.sleep(2)

# Step 3: Download the JSONL output
# The signed URL expires after 15 minutes. Plain GET, no API key.
jsonl = requests.get(output_url, timeout=60)
jsonl.raise_for_status()

# One chunk per line: split on "\n" only, as str.splitlines() also breaks on
# characters like U+2028 that can appear inside a cell value
chunks = [
    json.loads(line)
    for line in jsonl.content.decode("utf-8").split("\n")
    if line.strip()
]
print(f"Loaded {len(chunks)} chunks ready for retrieval")


# Step 4: Function tool the agent calls to search the workbook
@function_tool
def search_workbook(query: str, section: str) -> str:
    """Search the ingested XLSX and return the most relevant tables,
    each labelled with its sheet.

    Args:
        query: Keywords describing the data needed.
        section: Sheet name (or part of it) to restrict the search to, or an
            empty string to search every sheet.
    """
    terms = set(re.findall(r"\w+", query.casefold()))
    section_filter = section.casefold().strip()
    ranked = []

    for chunk in chunks:
        content = chunk["content"]
        metadata = chunk["metadata"]
        breadcrumb = " > ".join(metadata["headings_path"])
        section_name = metadata["section"]

        if section_filter and section_filter not in f"{breadcrumb} {section_name}".casefold():
            continue

        searchable = f"{section_name} {breadcrumb} {content}".casefold()
        score = len(terms & set(re.findall(r"\w+", searchable)))

        if score or section_filter:
            ranked.append((score, chunk))

    ranked.sort(key=lambda item: item[0], reverse=True)

    # Return each table in full: a table may exceed max_words, and cutting it
    # would hide rows from the agent
    results = [
        f"[Section: {c['metadata']['section']} | "
        f"Path: {' > '.join(c['metadata']['headings_path'])}]\n"
        f"{c['content']}"
        for _, c in ranked[:3]
    ]
    return "\n\n---\n\n".join(results) if results else "No matching spreadsheet passages found."


# Replace keyword scoring with embedding search for larger workbooks

# Step 5: Set up the agent
# No model is set, so the Agents SDK uses its default OpenAI model
agent = Agent(
    name="Catalogue Assistant",
    instructions=(
        "You answer questions about a spreadsheet workbook. "
        "Use search_workbook to find relevant tables before answering. "
        "Read the column headers in each table and cite the section a figure came from. "
        "Do not infer values that are not present in the retrieved tables."
    ),
    tools=[search_workbook],
)

result = Runner.run_sync(agent, "Which products cost more than 100 dollars?")
print(result.final_output)
```

## Output

Ingest returns the XLSX as Markdown table chunks where each table stays whole even when it exceeds the 512 word default soft cap, with headings_path and section metadata on every chunk, so the agent can retrieve and reason over structured tabular content. Reading the same file with openpyxl or pandas and serialising rows as plain text gives flat strings with column names either dropped or repeated on every row, with no chunk boundaries and no metadata the agent can filter on.

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