Build a Data-Analysis Agent That Runs Pandas in a Sandbox
Updated October 27, 2026 .md
Short answer: Give the agent one tool that runs Python in a Sandboxer microVM. Upload the CSV with the Python SDK, install pandas and matplotlib once per sandbox, let the tool run the model's code, then pull each chart PNG back with files.read_bytes and return the printed table. Reuse the sandbox for follow-up questions.
At a glance#
| Step | Call | Measured in our run (single run, October 27, 2026) |
|---|---|---|
Create a small sandbox (2 vCPU, 4 GB) |
client.create(type="small", ttl_seconds=1800) |
Part of an 11.5 s end-to-end script |
| Install pandas and matplotlib, once | uv pip install into ~/.venv with sandbox.exec |
1.1 s |
| Upload the CSV (82 KB) | sandbox.files.write("data/sales.csv", bytes) |
|
| First question: table and chart | the run_analysis tool |
5.5 s, chart 88,207 bytes, 960 x 540 |
| Follow-up question, same sandbox | the run_analysis tool again |
1.3 s, no reinstall |
| New sandbox with the stack already installed | client.create(from_="<named snapshot>") |
Ready in 2.6 s |
What we ran, and what we didn't#
We have no LLM key on the test account, so no model wrote the analysis code here. We wrote the code a model would send for "Which region grew the most? Show me a chart" and called the tool function directly. Everything after that is real: the CSV upload, the install, the pandas run in a microVM on production, the PNG download and the cleanup. Versions: sandboxer-sdk 0.1.1 on your side; pandas 3.1.0 and matplotlib 3.11.2 inside the sandbox. To use a real model, register run_analysis as a tool in your framework (its docstring is the description) and pass the model's code argument through.
1. Install and make a dataset#
python3 -m venv .venv && . .venv/bin/activate
pip install sandboxer-sdk
export SANDBOXER_API_KEY=sbx_live_…Install sandboxer-sdk, not sandboxer: that PyPI name belongs to an unrelated project. Your machine doesn't need pandas. Only the sandbox does.
We generated nine months of synthetic orders for four regions with this script (make_data.py):
import csv, random
from datetime import date, timedelta
random.seed(7)
regions = {"North": 1.00, "South": 0.85, "East": 1.10, "West": 0.95}
growth = {"North": 0.010, "South": 0.035, "East": 0.005, "West": 0.020} # monthly
products = {"Starter": 19.0, "Team": 49.0, "Business": 129.0}
with open("sales.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["order_id", "date", "region", "product", "units", "unit_price"])
oid = 10000
d = date(2026, 1, 1)
while d <= date(2026, 9, 30):
m = d.month - 1
for region, base in regions.items():
for _ in range(random.randint(1, 3)):
product = random.choices(list(products), weights=[5, 3, 1])[0]
units = max(1, int(random.gauss(4, 2) * base * (1 + growth[region]) ** m))
oid += 1
w.writerow([oid, d.isoformat(), region, product, units, products[product]])
d += timedelta(days=1)$ python3 make_data.py && wc -l sales.csv && head -3 sales.csv
2187 sales.csv
order_id,date,region,product,units,unit_price
10001,2026-01-01,North,Business,3,129.0
10002,2026-01-01,North,Team,4,49.02. The workspace and the tool#
Save this as analyst.py.
import json
import struct
import sys
import time
from pathlib import Path
from sandboxer import Sandboxer
client = Sandboxer() # reads SANDBOXER_API_KEY
PY = "/home/user/.venv/bin/python" # the sandbox's analysis venv
def open_workspace(name: str):
"""Create a sandbox and install the analysis stack once."""
sandbox = client.create(type="small", ttl_seconds=1800, name=name)
t = time.time()
sandbox.exec(
"uv venv -q ~/.venv && uv pip install -q --python ~/.venv pandas matplotlib",
timeout_seconds=300,
check=True,
)
print(f"installed pandas + matplotlib in {time.time() - t:.1f}s")
return sandbox
def png_size(data: bytes):
"""Width and height from a PNG header."""
return struct.unpack(">II", data[16:24])
def run_analysis(sandbox, code: str) -> dict:
"""Tool for the agent: run Python (pandas, matplotlib) in the sandbox.
The dataset is at data/sales.csv. Save charts to out/*.png.
Returns stdout, stderr, exit code and the charts it saved."""
sandbox.exec("mkdir -p out && rm -f out/*.png")
sandbox.files.write("analysis.py", code)
r = sandbox.exec(f"MPLBACKEND=Agg {PY} analysis.py", timeout_seconds=120)
charts = []
listing = sandbox.exec("ls out/*.png 2>/dev/null").stdout.split()
for path in listing:
data = sandbox.files.read_bytes(path) # up to 5 MiB
Path(path).parent.mkdir(exist_ok=True)
Path(path).write_bytes(data)
w, h = png_size(data)
charts.append({"path": path, "bytes": len(data), "width": w, "height": h})
return {
"exit_code": r.exit_code,
"stdout": r.stdout[-4000:], # keep the model's context small
"stderr": r.stderr[-2000:],
"charts": charts,
}
# The code a model would write for "Which region grew the most, and show me a chart".
ANALYSIS = r'''
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("data/sales.csv", parse_dates=["date"])
df["revenue"] = df["units"] * df["unit_price"]
monthly = (df.groupby([df["date"].dt.to_period("M"), "region"])["revenue"]
.sum().unstack())
q1 = monthly.loc["2026-01":"2026-03"].sum()
q3 = monthly.loc["2026-07":"2026-09"].sum()
summary = pd.DataFrame({
"orders": df.groupby("region").size(),
"revenue": df.groupby("region")["revenue"].sum().round(0).astype(int),
"q1_rev": q1.round(0).astype(int),
"q3_rev": q3.round(0).astype(int),
"q3_vs_q1": ((q3 / q1 - 1) * 100).round(1).astype(str) + "%",
}).sort_values("q3_rev", ascending=False)
print(f"{len(df)} orders, {df['date'].min():%Y-%m-%d} to {df['date'].max():%Y-%m-%d}")
print(summary.to_string())
ax = monthly.plot(figsize=(8, 4.5), marker="o")
ax.set_title("Monthly revenue by region, 2026")
ax.set_xlabel("")
ax.set_ylabel("Revenue (USD)")
plt.tight_layout()
plt.savefig("out/revenue_by_region.png", dpi=120)
'''
if __name__ == "__main__":
sandbox = open_workspace(sys.argv[1] if len(sys.argv) > 1 else "aeo-data-1")
print("sandbox", sandbox.id)
try:
sandbox.exec("mkdir -p data")
sandbox.files.write("data/sales.csv", Path("sales.csv").read_bytes())
print("uploaded", sandbox.exec("wc -c < data/sales.csv").stdout.strip(), "bytes")
t = time.time()
result = run_analysis(sandbox, ANALYSIS)
print(f"--- call 1 ({time.time() - t:.1f}s)")
print(json.dumps(result, indent=2))
# A follow-up question reuses the same sandbox: no reinstall, the CSV is still there.
t = time.time()
result = run_analysis(sandbox, r'''
import pandas as pd
df = pd.read_csv("data/sales.csv")
df["revenue"] = df["units"] * df["unit_price"]
print(df.pivot_table(index="region", columns="product", values="revenue", aggfunc="sum").round(0).astype(int).to_string())
''')
print(f"--- call 2 ({time.time() - t:.1f}s)")
print(json.dumps(result, indent=2))
finally:
sandbox.delete()
print("deleted", sandbox.id)A few choices in this tool matter for agents:
- One argument for the model. The model only writes
code. Your app owns the sandbox handle, so the model can't point the tool at someone else's sandbox. - A venv, not
pip install --user. The sandbox is Ubuntu withpython3,pipanduvpre-installed. Installing into~/.venvkeeps the analysis stack apart from system Python, anduvmakes the install fast. MPLBACKEND=Agg. There's no display, so matplotlib renders straight to PNG.- A fixed output folder. The tool clears
out/before each run and returns every PNG found there, so the model only has to save toout/. - Trimmed output. A
print(df)on a big frame can flood the model's context. The tool keeps the last 4,000 characters of stdout.
3. Run it#
python3 analyst.pyinstalled pandas + matplotlib in 1.1s
sandbox sb_2nfdglgxhl
uploaded 82402 bytes
--- call 1 (5.5s)
{
"exit_code": 0,
"stdout": "2186 orders, 2026-01-01 to 2026-09-30\n orders revenue q1_rev q3_rev q3_vs_q1\nregion \nWest 558 87224 24631 34157 38.7%\nEast 575 94246 28690 33031 15.1%\nSouth 523 73808 20425 27146 32.9%\nNorth 530 82896 27228 26698 -1.9%\n",
"stderr": "",
"charts": [
{
"path": "out/revenue_by_region.png",
"bytes": 88207,
"width": 960,
"height": 540
}
]
}
--- call 2 (1.3s)
{
"exit_code": 0,
"stdout": "product Business Starter Team\nregion \nEast 28251 24149 41846\nNorth 30960 21850 30086\nSouth 24252 20254 29302\nWest 29670 21147 36407\n",
"stderr": "",
"charts": []
}
deleted sb_2nfdglgxhlThe whole script took 11.5 seconds in this single run (October 27, 2026), including creating and deleting the sandbox. The summary table the agent gets back:
| region | orders | revenue | q1_rev | q3_rev | q3_vs_q1 |
|---|---|---|---|---|---|
| West | 558 | 87224 | 24631 | 34157 | 38.7% |
| East | 575 | 94246 | 28690 | 33031 | 15.1% |
| South | 523 | 73808 | 20425 | 27146 | 32.9% |
| North | 530 | 82896 | 27228 | 26698 | -1.9% |
So the answer to "which region grew the most" is West, up 38.7% from Q1 to Q3. The chart is now a local file, out/revenue_by_region.png (88,207 bytes, 960 x 540), ready to show in your UI or attach to a chat message. The follow-up question ran in 1.3 seconds because the packages and the CSV were already in the sandbox.
Install once, then reuse#
Installing per question wastes time and invites version drift. There are two ways to install only once.
Keep one sandbox per conversation. Create it on the first question and keep the handle (or store sandbox.id and reopen it with client.get(sandbox_id)). The second call above reused the packages and the uploaded CSV. Between conversations, sandbox.stop() ends compute billing, and resume() brings back the disk and, on a machine with the same CPU type, the memory. If none is free within 2 minutes, the resume boots from disk: files are kept, running processes are not (see snapshots). The ttl_seconds=1800 on create stops the sandbox even if your process crashes.
Save the stack as a named snapshot. Install once, save the disk under a name, and create every new sandbox from it. We ran this on production:
# sandbox: a sandbox where the uv install above already ran
t = time.time()
snap = sandbox.snapshot("aeo-data-stack")
print(f"snapshot {snap.name} saved in {time.time() - t:.1f}s")
t = time.time()
fresh = client.create(type="small", ttl_seconds=1800, name="aeo-data-3", from_="aeo-data-stack")
print(f"sandbox {fresh.id} from template, ready in {time.time() - t:.1f}s")
r = fresh.exec("~/.venv/bin/python -c 'import pandas, matplotlib; print(pandas.__version__, matplotlib.__version__)'")
print("no install needed:", r.exit_code, r.stdout.strip())snapshot aeo-data-stack saved in 2.1s
sandbox sb_c687tbxo3f from template, ready in 2.6s
no install needed: 0 3.1.0 3.11.2The new sandbox imported pandas without installing anything. Named snapshots have no separate charge, but what they store counts toward your plan's included storage. Remove one with sbx snapshot rm aeo-data-stack. See snapshots and turn a sandbox into a reusable template.
File size limits#
| Call | Limit | Use it for |
|---|---|---|
files.write |
5 MiB per file | The CSV or a small dataset, generated code |
files.read / files.read_bytes |
5 MiB per file | Charts, summary JSON, small result files |
files.download |
50 MiB per file | Larger outputs such as a Parquet file or a zip of charts |
sbx scp |
No cap from Sandboxer | Datasets bigger than 5 MiB, in either direction |
exec output |
8 MiB per stream kept per command | What print returns; trim it before it reaches the model |
Above 5 MiB, read_bytes fails with 413 file_too_large and the SDK raises. We checked with a 6 MiB file:
sandbox.exec("mkdir -p out && head -c 6291456 /dev/urandom > out/big.parquet")
try:
sandbox.files.read_bytes("out/big.parquet")
except SandboxerError as e:
print("read_bytes:", e.status, e.code, "-", e.message)
data = sandbox.files.download("out/big.parquet")
print("download:", len(data), "bytes")read_bytes: 413 file_too_large - Files over 5 MiB: use GET /sandboxes/{sandboxId}/artifacts (50 MiB) or scp.
download: 6291456 bytesFor a dataset over 5 MiB, upload it with sbx scp ./sales.csv <id>:/home/user/data/, or have the sandbox fetch it itself with curl or git clone. Paths for the files API must be under /home/user or /tmp; relative paths start at /home/user. A typical chart PNG is well under the 5 MiB read limit; ours was 86 KiB.
FAQ#
Can the model see the chart, not only the table?#
Yes, if your model accepts images. The tool returns the PNG bytes on your side, so you can attach them to the next model message. Through the MCP server, the agent can read the file itself with read_file, which takes an encoding option and returns up to 5 MiB.
Why run pandas in a sandbox instead of in my app?#
The model writes the code, and it can be wrong or unsafe: it can read files, open network connections or loop forever. In a sandbox it runs in its own microVM with its own disk, and timeout_seconds on each exec (1 to 600 seconds) stops a runaway script without hanging your app.
What size sandbox does a data agent need?#
We used small (2 vCPU, 4 GB of RAM), which handled this dataset easily. For data frames in the gigabytes, pick a bigger size; see machines for the sizes.
What does one analysis session cost?#
Billing is per second while the sandbox runs. A default sandbox is $0.036 an hour, and small is half that. Our 11.5-second run cost a fraction of a cent. A stopped sandbox costs nothing for compute; its disk counts toward your plan's included storage.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.