Code Execution Agents with E2B

8 min read Module 9 of 10 Topic 26 of 30

What you'll learn

  • Understand the security requirements for running LLM-generated code safely
  • Use E2B to create cloud sandboxes for Python, JavaScript, and shell execution
  • Build a data analysis agent that iteratively writes, runs, and corrects code
  • Implement code execution timeouts, resource limits, and output sanitization
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

The Code Execution Agent Pattern

Code execution transforms an agent from a text processor into a genuine problem solver. Instead of describing how to analyze data, the agent writes Python, runs it, sees the output, and iterates.

flowchart TD
    U(["User: Analyze this CSV\nfind top 5 customers by revenue"]) --> AG["AGENT\ngenerates Python code\nimport pandas; groupby; nlargest"]
    AG --> SB["E2B SANDBOX\nexecutes code in isolation"]
    SB -->|"stdout: {Acme: 150000...}"| OUT([AGENT formats response])
    SB -->|error traceback| AG
    style AG fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style SB fill:#f0fdf9,stroke:#0D9488,color:#0F172A

The agent can also handle errors by reading the traceback and rewriting the code.


E2B: Managed Sandbox Service

pip install e2b-code-interpreter

SandboxExecutor wraps the E2B API so every code execution happens in an isolated cloud VM, no LLM-generated code ever touches your host machine.

# src/tools/code_executor.py
from e2b_code_interpreter import Sandbox
import asyncio

class SandboxExecutor:
    def __init__(self):
        self.api_key = settings.e2b_api_key
    
    async def run_python(
        self,
        code: str,
        timeout_seconds: int = 30,
        files: dict[str, bytes] = None,
    ) -> dict:
        """Execute Python code in an isolated sandbox."""
        
        # Sandbox() creates an isolated cloud VM: code runs there, not on your host machine
        sandbox = Sandbox(api_key=self.api_key)
        
        try:
            # Upload any data files the generated code needs to read (e.g., the user's CSV)
            if files:
                for filename, content in files.items():
                    sandbox.filesystem.write(filename, content)
            
            # Run the code; timeout_seconds caps execution to prevent runaway loops or infinite computation
            execution = sandbox.run_code(code, timeout=timeout_seconds)
            
            return {
                "success": not execution.error,
                "stdout": execution.text or "",
                "stderr": execution.error or "",  # error message if the code raised an exception
                "results": [
                    {
                        "type": r.type,
                        # Exclude raw image bytes from the dict: just flag that an image was produced
                        "data": r.data if r.type != "image/png" else "<image>",
                    }
                    for r in (execution.results or [])
                ],
                "execution_count": 1,
            }
        
        except Exception as e:
            return {
                "success": False,
                "stdout": "",
                "stderr": str(e),
                "results": [],
            }
        finally:
            # sandbox.kill() is critical: forgetting this leaves running VMs that cost money
            sandbox.kill()

executor = SandboxExecutor()

Data Analysis Agent

The graph below implements an iterative generate-execute-correct loop: the LLM writes code, the sandbox runs it, and the output (or error traceback) feeds back to the LLM for the next iteration.

# src/agents/data_analyst_agent.py
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

class AnalystState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    code_iterations: int       # tracks how many code attempts have been made
    data_file: bytes | None
    last_code: str | None      # the most recent code block extracted from the LLM response
    last_output: str | None    # stdout from the last successful execution
    analysis_complete: bool

# MAX_CODE_ITERATIONS=4 prevents the agent from looping indefinitely on a broken task
MAX_CODE_ITERATIONS = 4

ANALYST_SYSTEM = """You are a Python data analyst. When given data analysis tasks:
1. Write clean, complete Python code that solves the problem
2. Use pandas, matplotlib, and numpy as needed
3. Always print() your results clearly
4. Handle edge cases (missing data, wrong types)
5. If you get an error, read it carefully and fix the specific issue

When writing code, put it in a ```python code block.
After seeing execution results, interpret them for the user."""

def extract_code_from_response(text: str) -> str | None:
    """Extract Python code from markdown code blocks."""
    import re
    # extract_code_from_response uses regex rather than JSON parsing because LLMs sometimes wrap code in prose
    match = re.search(r'```python\n(.*?)```', text, re.DOTALL)
    return match.group(1).strip() if match else None

async def analyst_llm_node(state: AnalystState) -> dict:
    """LLM generates or refines code."""
    # Bail out gracefully once the iteration cap is hit: more attempts rarely help
    if state["code_iterations"] >= MAX_CODE_ITERATIONS:
        return {
            "messages": [AIMessage(content="I've reached the maximum number of code iterations. Here are my findings so far: " + (state["last_output"] or "No output generated."))],
            "analysis_complete": True,
        }
    
    response = llm.invoke(
        [SystemMessage(content=ANALYST_SYSTEM)] + state["messages"]
    )
    return {
        "messages": [response],
        # Parse code from the response so the execute node receives clean Python, not markdown
        "last_code": extract_code_from_response(response.content),
    }

async def execute_code_node(state: AnalystState) -> dict:
    """Execute the generated Python code in a sandbox."""
    if not state["last_code"]:
        return {}
    
    files = {}
    if state["data_file"]:
        # Pass the uploaded file into the sandbox under a predictable name the code can open
        files["data.csv"] = state["data_file"]
    
    result = await executor.run_python(
        code=state["last_code"],
        timeout_seconds=30,
        files=files,
    )
    
    # Format result as an observation for the LLM: success path shows output, failure path shows the traceback
    if result["success"]:
        output = f"Code executed successfully.\nOutput:\n{result['stdout']}"
        if result["results"]:
            output += f"\nGenerated {len(result['results'])} output objects"
    else:
        output = f"Code failed with error:\n{result['stderr']}"
    
    # Wrap the result in a HumanMessage so it enters the conversation as the next "user turn"
    observation_msg = HumanMessage(content=f"Code execution result:\n{output}")
    
    return {
        "messages": [observation_msg],
        "code_iterations": state["code_iterations"] + 1,
        "last_output": result["stdout"],
    }

def should_execute_or_finish(state: AnalystState) -> str:
    """Route: does the last message contain code to execute?"""
    if state.get("analysis_complete"):
        return "finish"
    # If the LLM produced a code block, run it; otherwise the analysis is a prose-only answer
    if state["last_code"]:
        return "execute"
    return "finish"

# Build the graph
analyst_builder = StateGraph(AnalystState)
analyst_builder.add_node("llm", analyst_llm_node)
analyst_builder.add_node("execute", execute_code_node)

analyst_builder.set_entry_point("llm")
analyst_builder.add_conditional_edges("llm", should_execute_or_finish, {
    "execute": "execute",
    "finish": END,
})
# After each execution the result re-enters the LLM node so the model can react to the output
analyst_builder.add_edge("execute", "llm")

data_analyst = analyst_builder.compile()

Running the Data Analyst

import asyncio

async def analyze_data(question: str, csv_content: bytes = None):
    # All state fields must be initialized: LangGraph does not supply defaults
    initial_state: AnalystState = {
        "messages": [HumanMessage(content=question)],
        "code_iterations": 0,    # counter starts at zero; incremented by execute_code_node
        "data_file": csv_content,
        "last_code": None,
        "last_output": None,
        "analysis_complete": False,
    }
    
    result = await data_analyst.ainvoke(initial_state)
    
    # Filter to AI messages only: the list also contains the human turns and execution observations
    ai_messages = [m for m in result["messages"] if isinstance(m, AIMessage)]
    return ai_messages[-1].content  # return the final interpretation, not intermediate code drafts

# Usage
with open("sales_data.csv", "rb") as f:
    csv_data = f.read()

analysis = await analyze_data(
    "What are the top 3 products by revenue? Show monthly trends.",
    csv_content=csv_data,
)
print(analysis)

For agents that need GPU acceleration (image generation, ML inference):

# src/tools/modal_executor.py
import modal

# modal.App groups all GPU functions under one deployment: referenced together in `modal deploy`
app = modal.App("agent-gpu-tools")

# @app.function provisions a fresh GPU container on demand: no GPU sits idle between calls
@app.function(
    gpu="A10G",                  # A10G is cost-effective for Stable Diffusion; use A100 for larger models
    image=modal.Image.debian_slim().pip_install(["torch", "diffusers", "transformers"]),
    timeout=120,                 # Modal kills the container after 120 s, prevents runaway GPU spend
)
def generate_image(prompt: str, negative_prompt: str = "") -> bytes:
    """Generate an image using Stable Diffusion on a GPU."""
    from diffusers import StableDiffusionPipeline
    import torch
    
    pipe = StableDiffusionPipeline.from_pretrained(
        "runwayml/stable-diffusion-v1-5",
        torch_dtype=torch.float16,  # float16 halves VRAM usage with negligible quality loss on A10G
    ).to("cuda")
    
    image = pipe(prompt, negative_prompt=negative_prompt).images[0]
    
    # Serialize the PIL image to PNG bytes so it can cross the Modal RPC boundary
    import io
    buffer = io.BytesIO()
    image.save(buffer, format="PNG")
    return buffer.getvalue()

# Use as a tool in your agent
async def generate_image_tool(prompt: str) -> dict:
    """Generate an image with Stable Diffusion on a GPU."""
    with modal.enable_output():
        # .remote() sends execution to the Modal cloud GPU; the calling process waits for the result
        image_bytes = generate_image.remote(prompt)
    
    # Save locally: in production you'd upload to S3/GCS and return a signed URL instead
    filename = f"generated_{uuid.uuid4().hex[:8]}.png"
    with open(f"/tmp/{filename}", "wb") as f:
        f.write(image_bytes)
    
    return {"filename": filename, "size_bytes": len(image_bytes)}

Code execution agents, especially data analysts and debugging assistants: are among the most valuable AI agent use cases because they can do things LLMs fundamentally cannot: run code, see real outputs, and iterate to correct answers.

Knowledge Check

3 questions to test your understanding

1 Why is running LLM-generated code directly on your host machine dangerous?

2 What is E2B and how does it differ from running code in a Docker container you manage yourself?

3 A data analysis agent generates code, runs it, gets an error, then generates corrected code. What is this pattern called and how many iterations should you allow?

Discussion

Questions and notes from learners on this topic

Loading discussion…

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan