CrewAI code execution after CodeInterpreterTool
Build a CrewAI code execution sandbox tool with PandaStack after the built-in CodeInterpreterTool was deprecated and removed.
CrewAI removed its built-in CodeInterpreterTool from crewai-tools, and the allow_code_execution / code_execution_mode flags on Agent are deprecated too. The recommended path now is a dedicated sandbox service. This recipe wires a CrewAI code execution sandbox tool backed by a PandaStack code interpreter microVM — a drop-in custom tool your agent calls to run code in isolation.
Migrating from CodeInterpreterTool
If you have from crewai_tools import CodeInterpreterTool in your codebase, it no longer imports in current CrewAI versions. The replacement is the same shape it always should have been: a custom tool that delegates to an external sandbox. Instead of E2B or Modal, point it at a PandaStack code-interpreter sandbox. The migration is one tool class — your Agent, Task, and Crew wiring is unchanged, minus the deprecated allow_code_execution=True.
| Before (removed) | After (this recipe) |
|---|---|
from crewai_tools import CodeInterpreterTool | from crewai.tools import BaseTool + your own subclass |
Agent(..., allow_code_execution=True) | Agent(..., tools=[PandaStackCodeTool()]) |
| In-process / Docker fallback | Firecracker microVM, netns-isolated egress |
Install
The PandaStack SDK reads PANDASTACK_API_KEY from the environment — never hardcode it in the tool.
export PANDASTACK_API_KEY=pds_... # your API key
pip install pandastack crewaiThe custom tool
Subclass BaseTool (from crewai.tools, not the separate crewai_tools package). The _run method must return a string — that string is exactly what the agent sees. The key move is to return stderr and any error traceback alongside stdout so the model can read its own failure and self-correct on the next turn.
Because BaseTool is a Pydantic v2 model, the long-lived kernel handle is declared as a PrivateAttr — a plain _ctx: ... = None annotation would be treated as a model field and misbehave.
from typing import Type, Optional
from crewai.tools import BaseTool
from pydantic import BaseModel, Field, PrivateAttr
from pandastack import Sandbox, CodeContext
class RunPythonInput(BaseModel):
"""Input schema for the PandaStack code-execution tool."""
code: str = Field(..., description="Python source to execute in the sandbox.")
class PandaStackCodeTool(BaseTool):
name: str = "Execute Python"
description: str = (
"Run Python code in a secure, isolated sandbox and return the output. "
"State persists across calls within this crew run, so variables and "
"imports from earlier executions are still available. Use this whenever "
"you need to compute, analyze data, or verify a result by running code."
)
args_schema: Type[BaseModel] = RunPythonInput
# Lazily-created persistent kernel, shared across every _run call.
_ctx: Optional[CodeContext] = PrivateAttr(default=None)
def _kernel(self) -> CodeContext:
if self._ctx is None:
sandbox = Sandbox.create(
template="code-interpreter",
ttl_seconds=3600,
metadata={"tool": "crewai-code-exec"},
)
self._ctx = sandbox.create_code_context(language="python")
return self._ctx
def _run(self, code: str) -> str:
ex = self._kernel().run_code(code)
# Hand the agent everything it needs to self-correct.
if ex.error:
return f"ERROR:\n{ex.error}\n\nstdout:\n{ex.stdout}".strip()
parts = []
if ex.stdout:
parts.append(ex.stdout.rstrip())
if ex.stderr:
parts.append(f"[stderr]\n{ex.stderr.rstrip()}")
if ex.text and ex.text not in (ex.stdout or ""):
parts.append(f"[result] {ex.text}")
if ex.png:
# A chart came back as a base64 PNG object — not stdout to parse.
parts.append("[chart] PNG produced (base64 available via ex.png)")
return "\n\n".join(parts) or "(no output)"
def close(self) -> None:
if self._ctx is not None:
self._ctx.close()
self._ctx = NoneCrewAI is Python-only, so there is no TypeScript equivalent for the tool wiring. The underlying PandaStack call maps one-to-one if you build the same pattern in a TS agent framework:
ex = ctx.run_code(code) # Execution: .stdout .stderr .error .text .pngconst ex = await ctx.runCode(code); // Execution: { stdout, stderr, error?, text?, png? }Why a code context, not exec
create_code_context() is a long-lived Jupyter-style kernel. Imports and variables survive across run_code calls, so the agent can build up state over a multi-step Task instead of re-importing pandas every turn. And on the code-interpreter template — which bakes IPython, matplotlib, plotly, kaleido, and pandas — rich outputs are captured automatically: a plt.show() or a bare DataFrame as the last expression comes back as ex.png / ex.results[0].html objects, with no savefig, no save-to-disk, and no second download. See the code interpreter guide for the full result model.
Execution field | What the agent gets |
|---|---|
stdout / stderr | captured streams (feed both back so it can self-correct) |
error | formatted traceback if the cell raised |
text | convenience: the last text repr (a bare expression result) |
png | convenience: first chart as a base64 PNG object |
results | full list of rich outputs (.html, .json, .svg, .markdown, …) |
Wire it into a Crew
The tool is a normal CrewAI tool object — drop it into Agent(tools=[...]). No allow_code_execution flag, no Docker config.
from crewai import Agent, Crew, Task, Process
code_tool = PandaStackCodeTool()
analyst = Agent(
role="Data Analyst",
goal="Answer quantitative questions by writing and running Python.",
backstory=(
"You are a rigorous analyst. You never guess at numbers — you compute "
"them by running code in the sandbox, and you read errors carefully to "
"fix your own mistakes before answering."
),
tools=[code_tool],
verbose=True,
)
task = Task(
description=(
"Generate 1,000 samples from a normal distribution (mean 50, std 8) "
"with numpy. Report the sample mean and the 95th percentile, then plot "
"a histogram of the samples with matplotlib (call plt.show())."
),
expected_output="The computed sample mean, the 95th percentile, and a histogram.",
agent=analyst,
)
crew = Crew(
agents=[analyst],
tasks=[task],
process=Process.sequential,
verbose=True,
)
try:
result = crew.kickoff()
print(result)
finally:
code_tool.close() # tear down the kernel + sandboxThe agent calls Execute Python to run the numpy snippet, reads the printed values back from the tool's return string, and — because the task asks for a plot — its final run_python call produces a chart that comes back as ex.png. The tool reports [chart] PNG produced, and the base64 image is on the Execution ready to render in your UI. If the agent writes broken code, the ERROR: prefix and traceback go straight back into its context and it retries — the self-correction loop the deprecated tool used to give you, now on an isolated Firecracker VM.
Lightweight alternative: the @tool decorator
If you don't need a typed args_schema, the @tool decorator (also from crewai.tools) is the terser form. The docstring becomes the tool description, and the return value is the string the agent receives. Create the kernel lazily so merely importing the module doesn't spin up a live sandbox:
from crewai.tools import tool
from pandastack import Sandbox
_ctx = None
def _kernel():
global _ctx
if _ctx is None:
_ctx = Sandbox.create(template="code-interpreter", ttl_seconds=3600).create_code_context()
return _ctx
@tool("Execute Python")
def run_python(code: str) -> str:
"""Run Python in an isolated sandbox; returns stdout, or the error traceback."""
ex = _kernel().run_code(code)
if ex.error:
return f"ERROR:\n{ex.error}"
return ex.stdout or ex.text or "(no output)"Use this in tools=[run_python] exactly like the class version, and call _ctx.close() when the crew finishes. The BaseTool subclass is preferred when you want typed inputs or to manage the kernel lifecycle explicitly.
Notes
- Isolation. Each sandbox runs in its own Firecracker microVM with netns-isolated egress — a model that writes
rm -rf /or tries to read host files is contained to the VM. - One kernel per crew run. The tool above shares a single
CodeContextacross all_runcalls so state persists within akickoff(). For concurrent crews, create one tool instance per crew, or perAgent, to avoid sharing a kernel. - Always tear down. Call
code_tool.close()(or_ctx.close()) in afinallyso the kernel and sandbox don't linger. Idle kernels are reaped on TTL regardless, but explicit cleanup is cheaper. - Charts as objects. When the agent's code produces a plot,
ex.pngis a ready-to-render base64 PNG — surface it in your UI rather than asking the model to describe it.
Next steps
- Code interpreter — the full code-context result model (charts, DataFrames, JSON) this tool builds on.
- Run code from a LangGraph agent — the same wedge wired into a graph runtime.
- Build a data-analyst agent — upload a CSV and stream charts back to the model.
- Snapshots and forks — fork a warm kernel so each agent branch starts from the same loaded state.
Give a LangChain agent secure code execution
Replace LangChain's insecure PythonREPLTool with a sandboxed langchain code execution tool — a python agent that runs code inside a Firecracker microVM, not on your host.
Secure code execution for AutoGen agents
Give an AutoGen or AG2 agent an autogen code execution sandbox in Python — a custom tool that runs the model's code inside an isolated Firecracker microVM.