# 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](/docs/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. ```ts check import { 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")); ``` ```python check import base64 from withruntime import Sandbox with 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: ```ts check import { 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"]); ``` ```python check from withruntime import Runtime runtime = 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](/docs/images)). ## Large and private data - **Big files:** `files.write` and `files.upload` send 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 with `diskMiB`. - **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](/docs/storage#mount-your-own-bucket)). - **A database the agent should query:** store its API key as a [secret](/docs/security#secrets-sandboxes-never-see). 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: ``` CPU: 1,000 × 40 s / 3,600 × $0.025 = $0.28 Memory: 1,000 × 180 s / 3,600 × 4 GiB × $0.0075 = $1.50 Total: $1.78 ``` Runtime 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](/docs/pricing)). New accounts get 50 free sandbox hours, no card. ## Start ```bash no-run npx 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 analysis.py --out-dir charts` runs a file as a cell and saves its charts as PNG ([CLI](/docs/cli)). Related: [code interpreter for chatbots](/use-cases/code-interpreter-for-chatbots), [what a code interpreter is](/glossary/code-interpreter), [egress control](/glossary/egress-control), [run untrusted LLM code safely](/use-cases/run-untrusted-llm-code). Facts on this page were checked on 25 September 2026.