How to build a data analysis agent that runs Python safely
Upload the user's data to a sandbox, run the model's pandas code in a stateful interpreter there, and return tables and charts.
On Runtime pandas, NumPy and matplotlib are already installed, and the analysis pays for the CPU it uses. A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour while the model thinks and $0.08 an hour with both CPUs busy. A new sandbox ran its first Python command 351 ms after the request at the median on 24 September 2026 (speed), so every analysis can have a machine of its own.
The short answer
The agent loop has three parts: put the data in, run the model's cells, and bring the results out.
TypeScriptimport { writeFile } from "node:fs/promises";import { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ network: { internet: false }, timeoutSeconds: 1800 });await sbx.files.write("/workspace/orders.csv", "region,amount\nwest,120\neast,90\nwest,40\n");// Each cell is code the model wrote; variables carry over between cells.await sbx.interpreter.run("import pandas as pd\ndf = pd.read_csv('orders.csv')");const table = await sbx.interpreter.run("df.groupby('region')['amount'].sum()");console.log(table.results[0]?.data["text/plain"]);const chart = await sbx.interpreter.run( "import matplotlib.pyplot as plt\ndf.groupby('region')['amount'].sum().plot.bar()\nplt.show()",);const png = chart.results.find((r) => r.data["image/png"])?.data["image/png"];if (typeof png === "string") await writeFile("chart.png", Buffer.from(png, "base64"));Pythonimport base64from withruntime import Sandboxwith Sandbox.create(network={"internet": False}, timeout_seconds=1800) as sbx: sbx.files.write("/workspace/orders.csv", "region,amount\nwest,120\neast,90\nwest,40\n") # Each cell is code the model wrote; variables carry over between cells. sbx.interpreter.run("import pandas as pd\ndf = pd.read_csv('orders.csv')") table = sbx.interpreter.run("df.groupby('region')['amount'].sum()") print(table["results"][0]["data"]["text/plain"]) chart = sbx.interpreter.run( "import matplotlib.pyplot as plt\ndf.groupby('region')['amount'].sum().plot.bar()\nplt.show()" ) for result in chart["results"]: if "image/png" in result["data"]: with open("chart.png", "wb") as file: file.write(base64.b64decode(result["data"]["image/png"]))Give the model one tool, run_python(code), that calls interpreter.run and
returns the cell's stdout, its error and its text results. Keep the images
for the user; send the model a line saying a chart was made.
Why the interpreter and not python3 script.py
A script starts from nothing each time, so the agent reloads a large file on every step. The interpreter keeps the data frame in memory between cells, the way a notebook does:
| Step the agent takes | Script per step | Interpreter session |
|---|---|---|
| Load a 500 MB CSV | Every step | Once |
| Inspect columns, then clean them | Two programs, two loads | Two cells on the same df |
| Make a chart | Save to a file, then read it | Returned as a PNG result |
| Show a data frame | Print text | Returned as a table |
| An exception | Exit code and stderr | Error name, value and traceback |
The interpreter also runs R, JavaScript, TypeScript, Java, Bash and Go. R plots come back as PNG and R data frames as tables; R installs the first time a sandbox uses it, about 90 seconds once, or build it into an image.
Bring your own libraries
Build an image once with what your analyses need and start every sandbox from it:
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "analysis", recipe: { pip: ["polars", "duckdb", "scikit-learn", "seaborn"] },});await using sbx = await runtime.sandboxes.create({ image: "analysis", network: { internet: false },});const cell = await sbx.interpreter.run("import duckdb\nduckdb.sql('select 42').fetchall()");console.log(cell.results[0]?.data["text/plain"]);Pythonfrom withruntime import Runtimeruntime = Runtime()runtime.images.build(name="analysis", recipe={"pip": ["polars", "duckdb", "scikit-learn", "seaborn"]})with runtime.sandboxes.create(image="analysis", network={"internet": False}) as sbx: cell = sbx.interpreter.run("import duckdb\nduckdb.sql('select 42').fetchall()") print(cell["results"][0]["data"]["text/plain"])Building an image is free; a stored one is charged at $0.08 per GB per 30-day month, and the free trial stores your first three free (custom images).
Large and private data
- Big files:
files.writeandfiles.uploadsend large files in parallel 1 MiB chunks, each checked by SHA-256, and resume after a dropped connection. There is no size limit beyond the disk, so ask for more withdiskMiB. - Data in a bucket: mount your own S3, R2 or Google Cloud Storage bucket as a directory; the sandbox never holds the bucket's key (mount your own bucket).
- A database the agent should query: store its API key as a secret. The sandbox sees a placeholder, and the host's proxy adds the value only on HTTPS requests to the hosts you name.
- Nothing leaves: with
network: { internet: false }the model's code cannot send the data anywhere, and root in the guest cannot turn it back on.
What a data analysis agent needs
| Need | How Runtime covers it |
|---|---|
| Python data tools | NumPy, pandas and matplotlib in the default image; Python 3.12 |
| State across steps | Interpreter variables persist between cells |
| Charts and tables | matplotlib as PNG; pandas data frames as tables; display for files |
| Isolation per user | A Firecracker microVM with its own kernel for every sandbox |
| Keep the data in | Internet off, or an allow list, enforced on the host |
| Code that runs too long | timeoutMs per cell; the sandbox's lease bounds the rest |
| Coming back tomorrow | Sandbox.getOrCreate(name) and idlePauseSeconds: pause keeps memory, so the data frame is still loaded |
What it costs
Take 1,000 analyses a month, each keeping a 2 vCPU, 4 GiB sandbox running for 3 minutes, with the cells using 40 CPU-seconds:
TextCPU: 1,000 × 40 s / 3,600 × $0.025 = $0.28Memory: 1,000 × 180 s / 3,600 × 4 GiB × $0.0075 = $1.50Total: $1.78Runtime charges $0.025 per vCPU-hour of measured CPU, with a floor of a twentieth of a vCPU, and $0.0075 per GiB-hour of memory (pricing). New accounts get 50 free sandbox hours, no card.
Start
Terminalnpx withruntime sandbox run --trial -- python3 -c 'import pandas; print(pandas.__version__)'The first run prints a link to approve in your browser. From a shell,
runtime sandbox run-code <id> analysis.py --out-dir charts runs a file as a
cell and saves its charts as PNG (CLI).
Related: code interpreter for chatbots, what a code interpreter is, egress control, run untrusted LLM code safely.
Facts on this page were checked on 25 September 2026.