# LangGraph code execution tool in a sandbox Bind Runtime's `sandbox_tools(sbx)` to the model and put them in a `ToolNode`; every tool call then runs in a Linux microVM. **Runtime gives each LangGraph thread a machine of its own that survives between turns, paused at no compute cost.** The four tools are plain Python functions, so LangGraph's prebuilt `ToolNode` runs them as it runs any tool. Name the sandbox after the thread and the next turn gets the same files, memory and processes back. Each sandbox is a Firecracker microVM with its own Linux kernel, and 2 vCPU with 4 GiB costs $0.03125 an hour while the graph waits on the model. `langgraph` 1.2.12 was the current PyPI release on 25 September 2026; the tools ran in a LangGraph `ToolNode` with LangGraph 1.2 on 23 September 2026. ## Install ```bash no-run pip install -U langgraph langchain langchain-anthropic withruntime npx withruntime login ``` ## A graph with a sandbox tool node The classic loop: the model node calls tools, `tools_condition` routes to the tool node while there are tool calls, and the result goes back to the model. ```python check from langchain.chat_models import init_chat_model from langchain_core.tools import tool from langgraph.graph import END, START, MessagesState, StateGraph from langgraph.prebuilt import ToolNode, tools_condition from withruntime import Sandbox from withruntime.tools import sandbox_tools def build_graph(sbx: Sandbox): tools = [tool(f) for f in sandbox_tools(sbx)] model = init_chat_model("claude-sonnet-4-6", temperature=0).bind_tools(tools) def call_model(state: MessagesState): return {"messages": [model.invoke(state["messages"])]} graph = StateGraph(MessagesState) graph.add_node("model", call_model) graph.add_node("tools", ToolNode(tools)) graph.add_edge(START, "model") graph.add_conditional_edges("model", tools_condition) graph.add_edge("tools", "model") return graph.compile() with Sandbox.create(timeout_seconds=900) as sbx: app = build_graph(sbx) out = app.invoke({"messages": [("user", "Write fizzbuzz.py, run it for 1..15 and show the output.")]}) print(out["messages"][-1].content) ``` `tools_condition` ends the run at `END` when the model stops calling tools. By default the tool node hands a call with bad arguments back to the model as an error message. A command that fails is not an exception at all: it returns its `exit_code` and `stderr`, so the model reads the failure and tries again. ## One sandbox per thread A LangGraph checkpointer keeps a conversation's messages under a `thread_id`. Give its sandbox the same name, and `Sandbox.get_or_create` returns that sandbox on every turn, woken if it was paused: ```python check from withruntime import Sandbox def run_turn(thread_id: str, text: str) -> str: sbx = Sandbox.get_or_create(f"thread-{thread_id}", idle_pause_seconds=600) app = build_graph(sbx) # from the example above out = app.invoke({"messages": [("user", text)]}, {"configurable": {"thread_id": thread_id}}) return out["messages"][-1].content ``` - **Between turns** the sandbox pauses after ten idle minutes. Files, memory and running processes are kept and compute billing stops; the next tool call wakes it, usually in about half a second ([pause, wake, extend](/docs/python#pause-wake-extend)). - **A dev server the agent started** is still running when the user comes back, and `sbx.previews.create(port)` gives it a private HTTPS address. - **When the thread ends,** call `sbx.stop()`. Compile the graph with your checkpointer to keep the messages as well; the sandbox keeps the machine. ## Why not an in-process Python sandbox? LangChain's own `langchain-sandbox` ran model code in Pyodide, Python compiled to WebAssembly, under Deno. Its repository was archived on 14 January 2026, and its README now says: "These days we recommend accessing code execution either through sandbox APIs or LLM provider APIs." | Need in a LangGraph agent | `langchain-sandbox` (archived) | Runtime sandbox | | ------------------------- | -------------------------------------------------- | ------------------------------------------------------- | | Start time | "A few seconds of latency" per run, per its README | 351 ms median to the first Python result | | Files the code writes | "Currently not supported" | Read, write and list, any size | | Network | `httpx.AsyncClient`, not `requests` | Any library; allow lists or internet off, host-enforced | | Languages | Python under Pyodide | Anything Ubuntu 24.04 runs, with `sudo` | | State between turns | A stateful mode | Paused machine, 1 to 365 days | The 351 ms figure is the median of 20 runs on 24 September 2026, from the create request to the first Python result ([speed](/docs/speed)). ## Fan out with forks A graph that tries several fixes in parallel can fork the prepared sandbox instead of reinstalling everything. `sbx.fork(count=3)` returns three running copies, each with the parent's files, memory and processes; build one graph per copy and run them as parallel branches. Each copy is billed as its own sandbox, and the snapshot the fork took is deleted when the fork finishes ([snapshots and forks](/docs/javascript#snapshots-and-forks)). ## Keep keys and spending safe - **The model key** stays in the graph's process. Tools send the sandbox commands, never credentials. - **`max_cost_micros`** on the create refuses a sandbox whose first lease would cost more than that many microdollars. - **A daily spending limit** on the Runtime key caps what every thread's sandboxes can cost together in 24 hours; past it, a create or wake fails with `spending_limit_reached` ([daily limits](/docs/security#read-only-keys-and-daily-limits)). - **A read-only key** lets a monitor list every thread's sandbox and its cost without the power to start one. - **Network rules** such as `network={"internet": False}` bind root inside the sandbox, because the host enforces them ([turn off sandbox internet](/how-to/turn-off-sandbox-internet)). - **Secrets** reach the sandbox as placeholders; the host adds the real value only on HTTPS requests to the hosts you name. ## What it costs Measured CPU at $0.025 per vCPU-hour, floor 50 millicores, plus $0.0075 per reserved GiB-hour while running. A paused thread pays only for parked storage ([pricing](/docs/pricing#paused-storage)). New accounts get 50 free sandbox hours, no card. For the simpler prebuilt agent, see [LangChain](/integrations/langchain); for LangChain's planning harness, see [Deep Agents](/integrations/deep-agents). Why a microVM rather than a container is in [run untrusted LLM code](/use-cases/run-untrusted-llm-code). ## Sources Checked 25 September 2026. - [LangGraph quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart): `init_chat_model("claude-sonnet-4-6", temperature=0)` and `bind_tools` - [langgraph 1.2.12 on PyPI](https://pypi.org/project/langgraph/) and `langgraph-prebuilt` 1.1.0, whose `langgraph.prebuilt` exports `ToolNode` and `tools_condition` - [langchain-ai/langchain-sandbox](https://github.com/langchain-ai/langchain-sandbox): archived 14 January 2026; its README's recommendation and limitations Facts on this page were checked on 25 September 2026.