# 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, and show the text, tables and charts. **On Runtime a conversation's interpreter costs $0.03125 an 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. A new sandbox ran its first Python command 351 ms after the request at the median on 24 September 2026 ([speed](/docs/speed)), so it can start when the conversation does. ## The short answer One sandbox per conversation, found again by name on every turn. The interpreter keeps variables between cells, and 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)`, and pass what it returns back as the tool result. The first call in a conversation creates the sandbox; every later call gets the same one, 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 land there. pandas, NumPy and matplotlib are in the default image ([the sandbox environment](/docs/sandbox-environment)). Large files go up 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, usually in about half a second | | A runaway bill | `maxCostMicros` per create and a daily spending limit per key | | Many chats at once | 100 sandboxes at once on a paid account to start, eight on the free trial | R, Java and Go install the first time a sandbox uses them, about 35 to 90 seconds once; bake them into a [custom image](/docs/images) with `apt` so every conversation starts with them. ## When the model needs a package Leave 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 at once, to connections already open too ([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 at $0.025 per vCPU-hour, with a floor of a twentieth of a vCPU, and memory at $0.0075 per GiB-hour ([pricing](/docs/pricing)). Take 10,000 conversations a month, each keeping its sandbox running for 5 minutes and using 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. 20 CPU-seconds over 300 seconds is above the floor, so the floor adds nothing. Stop the sandbox when the conversation closes; a sandbox you keep paused instead is billed as paused storage at $0.08 per GB per 30-day month. New accounts get 50 free sandbox hours, no card. ## Start ```bash no-run npx withruntime sandbox run --trial -- python3 -c 'print(6 * 7)' ``` The first run prints a link to approve in your browser; there is no API key to copy. Then use the [JavaScript](/docs/javascript#code-interpreter) or [Python](/docs/python) SDK as 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 25 September 2026.