# How to give an LLM a code execution tool with function calling Declare a run-command tool in your model's function-calling format, run each call in a sandbox, and return exit code and output. **On Runtime the tool definitions are already written.** `withruntime/tools` gives four tools bound to one sandbox, each as a name, a description, a JSON Schema and a function: run a command, read a file, write a file, list a directory. Integrations for eleven agent frameworks wrap them, and each ran in its framework's own agent loop against real sandboxes on 23 September 2026 ([frameworks](/docs/frameworks)). The sandbox behind them is a Firecracker microVM billed on the CPU it uses, $0.025 per vCPU-hour. ## How tool use with a sandbox works Function calling is a contract between your application and the model. You list tools, each with a name, a description and a JSON Schema for its arguments. The model answers with a tool call instead of text. Your application runs the call and sends the result back, and the model continues. The model never runs anything itself; your code does, and where it runs is your choice. Running the call in a sandbox means the command the model chose executes on a separate machine with its own kernel, disk and network rules. The split of control stays clean: | Your application chooses | The model chooses | | ------------------------------------ | -------------------------------- | | Which sandbox, its size and lifetime | The command to run | | Its network rules and secrets | The file paths to read and write | | The API key and its daily limit | The `cwd` and a per-call timeout | | When the sandbox stops | When it has finished | ## The short answer Bind the tools to a sandbox, send their schemas to your model, and dispatch each tool call by name: ```ts check import { Sandbox } from "withruntime"; import { sandboxTools } from "withruntime/tools"; type ToolCall = { id: string; name: string; arguments: string }; await using sbx = await Sandbox.create({ network: { internet: false }, timeoutSeconds: 1800 }); const tools = sandboxTools(sbx); // What your model's API needs: a name, a description and a JSON Schema each. const declarations = tools.map((t) => ({ name: t.name, description: t.description, schema: t.inputSchema, })); console.log(declarations.map((d) => d.name)); async function dispatch(call: ToolCall) { const tool = tools.find((t) => t.name === call.name); if (!tool) return { id: call.id, output: `No tool named ${call.name}` }; // The model's arguments are untrusted JSON: the tool checks the fields it uses. const result = await tool.execute(JSON.parse(call.arguments) as never); return { id: call.id, output: JSON.stringify(result) }; } // One call, as a model would send it: console.log( await dispatch({ id: "c1", name: "runtime_exec", arguments: '{"command":"python3 -c \\"print(6 * 7)\\""}', }), ); ``` ```python check import json from withruntime import Sandbox from withruntime.tools import sandbox_tools with Sandbox.create(network={"internet": False}, timeout_seconds=1800) as sbx: tools = {tool.__name__: tool for tool in sandbox_tools(sbx)} print(sorted(tools)) # runtime_exec, runtime_list_files, runtime_read_file, runtime_write_file def dispatch(name: str, arguments: str) -> str: if name not in tools: return f"No tool named {name}" return json.dumps(tools[name](**json.loads(arguments))) print(dispatch("runtime_write_file", '{"path": "hello.py", "content": "print(6 * 7)\\n"}')) print(dispatch("runtime_exec", '{"command": "python3 hello.py"}')) ``` Rename the `schema` field to what your provider's API calls it. In Python the tools are plain functions with type hints and docstrings, which is what most Python frameworks turn into schemas themselves. ## What the four tools do | Tool | Arguments | Returns | | -------------------- | ---------------------------------------------- | ------------------------------------------ | | `runtime_exec` | `command`, optional `cwd` and `timeoutSeconds` | `exitCode`, `stdout`, `stderr`, `timedOut` | | `runtime_read_file` | `path` | The file's text | | `runtime_write_file` | `path`, `content` | How many bytes it wrote, and where | | `runtime_list_files` | optional `path` and `depth` | Entries with path, type and size | Relative paths are under `/workspace`, and output is capped so one noisy command cannot fill the model's context. Tell the model to check `exitCode`: a call that returned is not a command that worked. ## If you use a framework The same four tools come prepared for most agent frameworks ([frameworks](/docs/frameworks)): | Framework | What you add | | ---------------------------- | ----------------------------------------------------------- | | Vercel AI SDK, Mastra | `runtimeTools(sbx)` from `withruntime/ai` | | Claude Agent SDK | `runtimeMcpServer(sbx)` from `withruntime/claude-agent-sdk` | | OpenAI Agents SDK | A sandbox client for `SandboxAgent`, or four function tools | | LangChain, LangGraph, CrewAI | `sandbox_tools(sbx)` wrapped with the framework's `tool` | | Google ADK, Pydantic AI | `sandbox_tools(sbx)` as the agent's tools | | LlamaIndex | `FunctionTool.from_defaults` over `sandbox_tools(sbx)` | | Deep Agents | `RuntimeSandbox(sbx)` as the agent's backend | A framework's own local shell and file tools keep running on your machine, so switch those off when the sandbox tools replace them. ## Guard the tool, not the prompt A system prompt that says "never delete files" is a request. The settings on the sandbox are enforced on the host, whatever the model writes: - **`network: { internet: false }`**, or an `allow` list of the hosts the task needs, binds root inside the sandbox too. - **A secret** gives code an API key it can use but never read: the sandbox holds a placeholder, and the proxy adds the value on HTTPS requests to that key's own hosts ([security](/docs/security#secrets-sandboxes-never-see)). - **`timeoutSeconds`** with `onLeaseEnd: "stop"` bounds how long the machine lives, even if your process crashes mid-loop. - **`maxCostMicros`** refuses a create whose first lease would cost more, and a daily spending limit on the key caps a whole day. ## What it costs Take 5,000 agent tasks a month. Each keeps a 2 vCPU, 4 GiB sandbox running for 4 minutes, and its 25 tool calls use 30 CPU-seconds in all: ``` CPU: 5,000 × 30 s / 3,600 × $0.025 = $1.04 Memory: 5,000 × 240 s / 3,600 × 4 GiB × $0.0075 = $10.00 Total: $11.04 ``` About two tenths of a cent a task. While the model thinks, the sandbox is billed its memory and a CPU floor of a twentieth of a vCPU, $0.03125 an hour at this size ([pricing](/docs/pricing)). Model tokens are billed by your model provider. New accounts get 50 free sandbox hours, no card. ## Start ```bash no-run npm install withruntime npx withruntime sandbox run --trial -- python3 -c 'print(6 * 7)' ``` The first run prints a link to approve in your browser; after that the SDK finds the connection by itself. Related: [a coding agent sandbox](/use-cases/coding-agent-sandbox), [code interpreter for chatbots](/use-cases/code-interpreter-for-chatbots), [run untrusted LLM code safely](/use-cases/run-untrusted-llm-code), [what an agent sandbox is](/glossary/agent-sandbox). Facts on this page were checked on 25 September 2026.