# How to add a code interpreter to a chatbot Give each conversation its own sandbox with a stateful interpreter. Run the model's code there. Show the text, tables and charts. **On Runtime, a conversation's interpreter costs $0.03125 per hour while it waits for the next message.** Runtime bills the CPU the code uses, not the CPUs the sandbox holds. So a chat that runs a cell every few minutes pays for the seconds of work and the memory. ## The short answer Use one sandbox per conversation, and find it again by name on every turn. The interpreter keeps variables between cells. The sandbox pauses itself when the chat goes quiet. ```ts check import { Sandbox } from "withruntime"; export async function runCell(conversationId: string, code: string) { const sbx = await Sandbox.getOrCreate(`chat-${conversationId}`, { vcpu: 1, memoryMiB: 2048, idlePauseSeconds: 600, // pause after ten quiet minutes network: { internet: false }, }); const cell = await sbx.interpreter.run(code, { timeoutMs: 60_000 }); return { status: cell.status, // "ok", "error", "timeout" ... stdout: cell.stdout, error: cell.error?.value, results: cell.results.map((result) => result.data), // text/plain, image/png, tables }; } ``` ```python check from withruntime import Sandbox def run_cell(conversation_id: str, code: str) -> dict: sbx = Sandbox.get_or_create( f"chat-{conversation_id}", vcpu=1, memory_mib=2048, idle_pause_seconds=600, # pause after ten quiet minutes network={"internet": False}, ) cell = sbx.interpreter.run(code, timeout_ms=60_000) return { "status": cell["status"], "stdout": cell["stdout"], "error": (cell["error"] or {}).get("value"), "results": [result["data"] for result in cell["results"]], } ``` Expose `runCell` to the model as a tool, for example `run_python(code)`. Pass its return value back as the tool result. The first call in a conversation creates the sandbox. Every later call gets the same sandbox, woken if it paused. ## What comes back Each cell returns its status, its printed output, an error with its traceback if it raised, and a list of results. Each result is a bundle keyed by MIME type: | You run | You get back | | --------------------------------- | --------------------------------------------------------- | | A pandas data frame such as `df` | A table (`application/vnd.runtime.table+json`) | | A matplotlib chart | A PNG image (base64 in `image/png`) | | `print(...)` | `stdout` | | An exception | `status: "error"` and `error` with name, value, traceback | | A cell that runs too long | `status: "timeout"` | | `display` of a file (.csv, .html) | The file as a result | Render the PNG in your chat UI as an image, the table as a table, and send the text to the model so it can read its own output. ## Let users upload files A user's CSV goes into the sandbox as a file. The next cell reads it: ```ts check import { Sandbox } from "withruntime"; const sbx = await Sandbox.getOrCreate("chat-42", { idlePauseSeconds: 600 }); await sbx.files.write("/workspace/sales.csv", "month,revenue\nJan,120\nFeb,135\n"); const cell = await sbx.interpreter.run( "import pandas as pd\ndf = pd.read_csv('sales.csv')\ndf.describe()", ); console.log(cell.results[0]?.data["text/plain"]); ``` ```python check from withruntime import Sandbox sbx = Sandbox.get_or_create("chat-42", idle_pause_seconds=600) sbx.files.write("/workspace/sales.csv", "month,revenue\nJan,120\nFeb,135\n") cell = sbx.interpreter.run("import pandas as pd\ndf = pd.read_csv('sales.csv')\ndf.describe()") print(cell["results"][0]["data"]["text/plain"]) ``` `/workspace` is the working directory, so relative paths resolve there. pandas, NumPy and matplotlib are in the default image ([the sandbox environment](/docs/sandbox-environment)). Large files upload in checked chunks. There is no size limit beyond the disk. ## What a chatbot interpreter needs | Need | How Runtime covers it | | ----------------------------- | --------------------------------------------------------------------------------------------------- | | State between turns | Variables persist between cells; a paused sandbox keeps its memory and processes | | Several languages | Python, JavaScript, TypeScript, R, Java, Bash and Go | | Charts and tables | matplotlib and R plots as PNG; pandas and R data frames as tables | | One user cannot reach another | A Firecracker microVM with its own kernel for every conversation | | Code that sends data out | `network: { internet: false }`, enforced on the host, root included | | An infinite loop | `timeoutMs` ends the cell and reports `timeout` | | Quiet conversations | `idlePauseSeconds` pauses; the next call wakes it | | A runaway bill | `maxTotalCostMicros` per sandbox and a daily spending limit per key | | Many chats at the same time | 100 sandboxes at the same time on a paid account, four without credit | R, Java and Go install the first time a sandbox uses them. That takes about 35 to 90 seconds, once. To avoid that wait, bake them into a [custom image](/docs/images) with `apt`, so every conversation starts with them. ## When the model needs a package Keep the internet off by default. To let a cell install from PyPI, allow only the registry. Then close it again: ```ts check import { Sandbox } from "withruntime"; const sbx = await Sandbox.getOrCreate("chat-42", { network: { internet: false } }); await sbx.network.set({ internet: true, allow: ["pypi.org", "*.pythonhosted.org"] }); await sbx.exec("pip install --quiet tabulate", { check: true, timeoutMs: 120_000 }); await sbx.network.set({ internet: false }); ``` A rule applies immediately, including to connections that are already open ([turn off sandbox internet](/how-to/turn-off-sandbox-internet)). ## What it costs A conversation's sandbox of 1 vCPU and 2 GiB is billed while it runs. Measured CPU costs $0.025 per vCPU-hour, with a floor of a twentieth of a vCPU. Memory costs $0.0075 per GiB-hour ([pricing](/docs/pricing)). Assume 10,000 conversations a month. Each keeps its sandbox running for 5 minutes and uses 20 CPU-seconds of work: ``` CPU: 10,000 × 20 s / 3,600 × $0.025 = $1.39 Memory: 10,000 × 300 s / 3,600 × 2 GiB × $0.0075 = $12.50 Total: $13.89 ``` That is about $0.0014 a conversation. The usage, 20 CPU-seconds over 300 seconds, is above the floor. So the floor adds nothing. Stop the sandbox when the conversation closes. A sandbox that you keep paused instead is billed as paused storage at $0.08 per GB per 30-day month. Every account gets 100 free hours of a 1 GB machine in its first 30 days, with no card. ## Start ```bash no-run printf '%s\n' 'print(sum([125, 250, 375]))' > invoice.py npx withruntime sandboxes run -- python3 - < invoice.py ``` The first run prints a link. Open it in your browser to approve the run. You do not need an API key to copy. Then use the [JavaScript](/docs/javascript#code-interpreter) or [Python](/docs/python) SDK as shown above. Related: [what a code interpreter is](/glossary/code-interpreter), [run untrusted LLM code safely](/use-cases/run-untrusted-llm-code), [a data analysis agent](/use-cases/data-analysis-agent), [pause and resume a sandbox](/how-to/pause-and-resume-a-sandbox). Facts on this page were checked on 26 October 2026.