# How to let an AI agent analyze Excel spreadsheets safely Upload the workbook to a sandbox with the internet off, edit it with pandas and openpyxl in a stateful interpreter, and download it. **On Runtime 2,000 workbook analyses cost about $1.92 a month** on 2 vCPU, 4 GiB sandboxes, at the rates in force on 25 September 2026, because the sandbox is billed for the CPU the model's code uses. pandas 2.1.4 and NumPy come in the default image, a new sandbox answered its first Python command in 351 ms at the median on 24 September 2026 ([speed](/docs/speed)), and with `network: { internet: false }` the figures in a customer's workbook cannot leave the machine. ## The short answer Build a small image with openpyxl once, then give each workbook a sandbox of its own: ```ts check import { readFile, writeFile } from "node:fs/promises"; import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "sheets", recipe: { pip: ["openpyxl"] } }); // once await using sbx = await runtime.sandboxes.create({ image: "sheets", network: { internet: false } }); await sbx.files.write("/workspace/in/budget.xlsx", await readFile("budget.xlsx")); // Every sheet at once, as a dict of data frames the model can inspect. await sbx.interpreter.run( "import pandas as pd\nbook = pd.read_excel('in/budget.xlsx', sheet_name=None)", ); const shapes = await sbx.interpreter.run("{name: df.shape for name, df in book.items()}"); console.log(shapes.results[0]?.data["text/plain"]); // ... the model's cells run here, each one sees `book` ... await sbx.interpreter.run( "import os\nos.makedirs('out', exist_ok=True)\n" + "with pd.ExcelWriter('out/summary.xlsx') as w:\n" + " for name, df in book.items():\n" + " df.describe().to_excel(w, sheet_name=name[:31])", ); await writeFile("summary.xlsx", await sbx.files.read("/workspace/out/summary.xlsx")); ``` ```python check from withruntime import Runtime runtime = Runtime() runtime.images.build(name="sheets", recipe={"pip": ["openpyxl"]}) # once with runtime.sandboxes.create(image="sheets", network={"internet": False}) as sbx: with open("budget.xlsx", "rb") as file: sbx.files.write("/workspace/in/budget.xlsx", file.read()) sbx.interpreter.run("import pandas as pd\nbook = pd.read_excel('in/budget.xlsx', sheet_name=None)") shapes = sbx.interpreter.run("{name: df.shape for name, df in book.items()}") print(shapes["results"][0]["data"]["text/plain"]) sbx.interpreter.run( "import os\nos.makedirs('out', exist_ok=True)\n" "with pd.ExcelWriter('out/summary.xlsx') as w:\n" " for name, df in book.items():\n" " df.describe().to_excel(w, sheet_name=name[:31])" ) with open("summary.xlsx", "wb") as file: file.write(sbx.files.read("/workspace/out/summary.xlsx")) ``` pandas reads `.xlsx` files with openpyxl, which is why the image adds it. The interpreter keeps `book` in memory between cells, so the model can look at the columns, clean them and summarize them without reading the file again. A CSV needs nothing extra: `pd.read_csv` works in the default image. ## Formulas, values and what the model sees A spreadsheet is not only a table. The things that trip up an agent, and how to handle each in the sandbox: | In the workbook | What to do | | ---------------------------- | --------------------------------------------------------------------------------------- | | Several sheets | `sheet_name=None` returns every sheet as a data frame, keyed by name | | Header rows above the table | `header=` and `skiprows=` in `read_excel`; let the model look at the first rows first | | Formulas | openpyxl's `load_workbook` returns the formula text by default | | The computed value of a cell | `load_workbook(path, data_only=True)` returns the value Excel stored when it last saved | | Merged or styled cells | Edit the workbook with openpyxl instead of pandas, so formatting is kept | | A chart for the user | Plot with matplotlib; the interpreter returns it as a PNG | Tell the model which of the two it is reading, so it never reports the text `=SUM(B2:B40)` as a figure, and keep the formulas when it writes the workbook back. ## Why a sandbox and not your own server Spreadsheets arrive from customers, and the code that reads them is written by a model. Both are untrusted. In the sandbox: - **The code runs in a Firecracker microVM with its own kernel,** not in a container sharing your server's kernel. - **The data stays in:** `internet: false` refuses every outbound connection, and root inside the sandbox cannot turn it back on. - **Each cell has a time limit:** pass `timeoutMs` to `interpreter.run`; a timeout returns as a result, not a hung worker. - **Nothing is left behind:** the sandbox stops at the end of the block, and with it every copy of the workbook. ## Large workbooks and many files `files.write` sends big files in parallel 1 MiB chunks, each checked by SHA-256, and resumes after a dropped connection; the only size limit is the disk, which `diskMiB` raises. A folder of files goes in one call with `files.upload(localDir, "/workspace/in")`. For workbooks kept in a bucket, mount it read-only as a directory; the sandbox never holds the bucket's key ([mount your own bucket](/docs/storage#mount-your-own-bucket)). ## What a spreadsheet agent needs | Need | How Runtime covers it | | ------------------------------ | ---------------------------------------------------------------------- | | Read `.xlsx` and `.csv` | pandas 2.1.4 in the default image; openpyxl in a one-line recipe image | | State across the model's steps | Interpreter variables persist between cells | | Return an edited workbook | `files.read` gives the bytes of any file the code wrote | | Charts | matplotlib figures come back as PNG results | | Customer data kept private | Internet off per sandbox, enforced on the host | | A fresh machine per file | 351 ms median to the first Python result, measured 24 September 2026 | | Code that loops forever | `timeoutMs` per cell; the sandbox's lease bounds the rest | ## What it costs Take 2,000 workbooks a month. Each keeps a 2 vCPU, 4 GiB sandbox running for 90 seconds while the model works through it, and the cells use 30 CPU-seconds: ``` CPU: 2,000 × 30 s / 3,600 × $0.025 = $0.42 Memory: 2,000 × 90 s / 3,600 × 4 GiB × $0.0075 = $1.50 Total: $1.92 ``` That is a tenth of a cent a workbook. The `sheets` image is stored free as one of the trial's first three images, and otherwise at $0.08 per GB a month. Runtime charges $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory, and stores images at $0.08 per GB per 30-day month ([pricing](/docs/pricing#snapshots-images-and-volumes)). ## Start ```bash no-run npx withruntime sandbox run --trial -- python3 -c 'import pandas; print(pandas.__version__)' ``` The first run opens a browser page to approve the connection. After that, `runtime sandbox run-code clean.py` runs a script as an interpreter cell from a terminal ([CLI](/docs/cli)). Related: [data analysis agent](/use-cases/data-analysis-agent), [chart generation](/use-cases/chart-generation), [SQL analysis agent](/use-cases/sql-analysis-agent), [turn off sandbox internet](/how-to/turn-off-sandbox-internet). ## Sources - [pandas.read_excel](https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html): openpyxl is the engine for `.xlsx` files, read 25 September 2026. - [openpyxl tutorial](https://openpyxl.readthedocs.io/en/stable/tutorial.html): `data_only` returns the formula or the value Excel last stored, read 25 September 2026. Facts on this page were checked on 25 September 2026.