Skip to content
Guides · Use cases

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.0

2. 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 with python3, pip and uv pre-installed. Installing into ~/.venv keeps the analysis stack apart from system Python, and uv makes 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 to out/.
  • 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.py
installed 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_2nfdglgxhl

The 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.2

The 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 bytes

For 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.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

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.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

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.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

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.

Read the guide
Best E2B alternatives in 2026, ranked

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.

Read the guide