# 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, then download the result. **On Runtime, 2,000 workbook analyses cost about $1.92 a month** on 2 vCPU, 4 GiB sandboxes. This is at the rates in force on 26 October 2026. The sandbox is billed only for the CPU the model's code uses. pandas 2.1.4 and NumPy come in the default image. With `network: { internet: false }`, the data 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. The model can therefore 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 more than a table. These are the cases that cause agents problems, and how to handle each one 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. Then it never reports the text `=SUM(B2:B40)` as a figure. Keep the formulas when it writes the workbook back. ## Why a sandbox and not your own server Spreadsheets arrive from customers, and a model writes the code that reads them. Both are untrusted. In the sandbox: - **The code runs in a Firecracker microVM with its own kernel.** It does not run in a container that shares your server's kernel. - **The data stays in.** `internet: false` refuses every outbound connection. 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 as a hung worker. - **Nothing is left behind.** The sandbox is deleted at the end of the block, and with it every copy of the workbook. ## Large workbooks and many files `files.write` sends large files in parallel 1 MiB chunks. Each chunk is checked by SHA-256, and an upload 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 the bucket 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 | A new microVM per file, from a machine started ahead of time for the common shapes | | Code that loops forever | `timeoutMs` per cell; `timeoutSeconds` bounds the sandbox | ## 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. 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 about a tenth of a cent per workbook. The `sheets` image is stored free as one of the first three images an account without credit keeps free. Otherwise it costs $0.08 per GB a month. Runtime charges $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory. It stores images at $0.08 per GB per 30-day month ([pricing](/docs/pricing#snapshots-images-and-volumes)). ## Start ```bash no-run npx withruntime sandboxes run -- python3 -c 'import pandas; print(pandas.__version__)' ``` The first run opens a browser page to approve the connection. After that, `runtime sandboxes 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 26 October 2026. - [openpyxl tutorial](https://openpyxl.readthedocs.io/en/stable/tutorial.html): `data_only` returns the formula or the value Excel last stored, read 26 October 2026. Facts on this page were checked on 26 October 2026.