Guide #4 Multi-Agent Frameworks 25 min read March 2026

Give Every CrewAI Agent Its Own Machine

CrewAI is the fastest-growing multi-agent framework — 44,000 GitHub stars and climbing. You define agents with roles, goals, and tools. Then you assemble them into crews that collaborate on complex tasks.

But every agent in a CrewAI crew shares the same Python process. The researcher can't browse the web. The developer can't run pytest in isolation. The analyst can't open LibreOffice. This guide gives each crew member its own dedicated sandbox — a browser, a terminal, or a full desktop — matched to its role.

Why CrewAI crews need dedicated compute

CrewAI gives you the orchestration layer — role definitions, task delegation, memory, and agent-to-agent handoffs. What it doesn't give you is compute. When your Researcher agent needs to open a browser, it has no browser. When your Developer agent needs to run a test suite, it shares your local Python process. When your Analyst agent needs to produce a PDF report in a desktop application, it has no desktop.

This matters for three reasons:

CrewAI Orchestrator Your local machine — assigns tasks, collects results



Researcher Sandbox Developer Sandbox Analyst Sandbox
Chrome browser   |   Terminal + build tools   |   XFCE desktop + LibreOffice

The pattern is simple: CrewAI runs locally and orchestrates. Each agent's tools call the Sandbox Platform API to create sandboxes, execute commands, browse the web, and retrieve files. The agents never share an environment. The orchestrator never runs untrusted code.

Prerequisites and installation

You need three things: a Sandbox Platform API key, Python 3.10+, and two packages.

bash
# Install CrewAI and httpx (our HTTP client for sandbox API calls)
pip install crewai httpx

# Verify versions
python -c "import crewai; print(f'CrewAI {crewai.__version__}')"
python -c "import httpx; print(f'httpx {httpx.__version__}')"

# Set your API keys
export SANDBOX_API_KEY="ab0t_sk_live_YOUR_KEY_HERE"
export SANDBOX_URL="https://sandbox.dev.ab0t.com"
export OPENAI_API_KEY="sk-YOUR_KEY_HERE"  # CrewAI uses OpenAI by default
Why httpx?

CrewAI tools run in sync context. httpx provides both sync and async HTTP clients with a clean API, connection pooling, and proper timeout handling. It's the right choice for wrapping API calls inside CrewAI tool functions.

Building CrewAI tools that wrap sandbox operations

CrewAI agents use tools — Python functions decorated with @tool. We need four core tools that map to sandbox API operations: creating sandboxes, executing commands, browsing URLs, and downloading files.

The sandbox client

First, a thin wrapper around the Sandbox Platform API. Every tool will use this client.

python — sandbox_client.py
import os
import time
import httpx

class SandboxClient:
    """Thin wrapper around the Sandbox Platform API."""

    def __init__(self):
        self.base_url = os.environ["SANDBOX_URL"]
        self.api_key = os.environ["SANDBOX_API_KEY"]
        self.client = httpx.Client(
            base_url=self.base_url,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            },
            timeout=120.0,
        )

    def create_sandbox(self, name: str, sandbox_type: str = "terminal",
                        instance_type: str = "ab0t.small") -> dict:
        """Create a sandbox and wait for it to be ready."""
        resp = self.client.post("/api/sandboxes", json={
            "name": name,
            "type": sandbox_type,  # "terminal", "browser", or "desktop"
            "instance_type": instance_type,
            "auto_stop_minutes": 60,
        })
        resp.raise_for_status()
        sandbox = resp.json()
        sandbox_id = sandbox["sandbox_id"]

        # Poll until ready (typically 20-40 seconds)
        for _ in range(60):
            status = self.get_sandbox(sandbox_id)
            if status["status"] == "running":
                return status
            time.sleep(2)
        raise TimeoutError(f"Sandbox {sandbox_id} did not start in time")

    def get_sandbox(self, sandbox_id: str) -> dict:
        resp = self.client.get(f"/api/sandboxes/{sandbox_id}")
        resp.raise_for_status()
        return resp.json()

    def execute(self, sandbox_id: str, command: str,
                timeout_seconds: int = 300) -> dict:
        """Execute a shell command in a sandbox."""
        resp = self.client.post(
            f"/api/sandboxes/{sandbox_id}/execute",
            json={"command": command, "timeout": timeout_seconds},
            timeout=timeout_seconds + 10,
        )
        resp.raise_for_status()
        return resp.json()

    def browse(self, sandbox_id: str, url: str) -> dict:
        """Open a URL in the sandbox's browser and get page content."""
        resp = self.client.post(
            f"/api/sandboxes/{sandbox_id}/browse",
            json={"url": url, "wait_for": "networkidle"},
        )
        resp.raise_for_status()
        return resp.json()

    def download_file(self, sandbox_id: str, path: str) -> bytes:
        """Download a file from a sandbox."""
        resp = self.client.get(
            f"/api/sandboxes/{sandbox_id}/files",
            params={"path": path},
        )
        resp.raise_for_status()
        return resp.content

    def upload_file(self, sandbox_id: str, path: str,
                    content: str) -> dict:
        """Upload a file to a sandbox."""
        resp = self.client.post(
            f"/api/sandboxes/{sandbox_id}/files",
            json={"path": path, "content": content},
        )
        resp.raise_for_status()
        return resp.json()

    def take_screenshot(self, sandbox_id: str) -> bytes:
        """Take a screenshot of a desktop sandbox."""
        resp = self.client.get(
            f"/api/sandboxes/{sandbox_id}/screenshot",
        )
        resp.raise_for_status()
        return resp.content

    def stop_sandbox(self, sandbox_id: str) -> dict:
        resp = self.client.post(f"/api/sandboxes/{sandbox_id}/stop")
        resp.raise_for_status()
        return resp.json()


# Singleton instance used by all tools
sandbox_client = SandboxClient()

CrewAI tools for each sandbox type

Now we create the tools that agents will actually call. Each tool wraps one or more sandbox client methods and returns results as strings (CrewAI tools must return strings).

python — sandbox_tools.py
from crewai.tools import tool
from sandbox_client import sandbox_client
import json

# ── Browser tools (for the Researcher) ──────────────────────────

@tool
def browse_url(sandbox_id: str, url: str) -> str:
    """Browse a URL in the sandbox's Chrome browser and return the page
    content as text. Use this to read documentation, READMEs, blog posts,
    and any other web page. Returns the visible text content of the page."""
    result = sandbox_client.browse(sandbox_id, url)
    return result.get("text_content", result.get("html", "No content returned"))

@tool
def search_web(sandbox_id: str, query: str) -> str:
    """Search the web using the sandbox's browser. Returns search result
    snippets and URLs. Use this to find documentation, tutorials, and
    relevant resources before browsing specific pages."""
    url = f"https://www.google.com/search?q={query}"
    result = sandbox_client.browse(sandbox_id, url)
    return result.get("text_content", "No results returned")

# ── Terminal tools (for the Developer) ──────────────────────────

@tool
def run_command(sandbox_id: str, command: str) -> str:
    """Execute a shell command in the sandbox terminal. Use this to run
    git commands, install packages, run tests, compile code, or any other
    CLI operation. Returns stdout and stderr."""
    result = sandbox_client.execute(sandbox_id, command)
    output = result.get("stdout", "")
    errors = result.get("stderr", "")
    exit_code = result.get("exit_code", -1)
    return f"Exit code: {exit_code}\n\nSTDOUT:\n{output}\n\nSTDERR:\n{errors}"

@tool
def write_file(sandbox_id: str, file_path: str, content: str) -> str:
    """Write content to a file in the sandbox. Creates the file if it
    doesn't exist, overwrites if it does. Use this to create scripts,
    config files, or any other text file."""
    sandbox_client.upload_file(sandbox_id, file_path, content)
    return f"File written: {file_path}"

@tool
def read_file(sandbox_id: str, file_path: str) -> str:
    """Read the contents of a file from the sandbox. Returns the file
    content as a string. Use this to read test results, log files,
    source code, or any text file."""
    content = sandbox_client.download_file(sandbox_id, file_path)
    return content.decode("utf-8", errors="replace")

# ── Desktop tools (for the Analyst) ─────────────────────────────

@tool
def desktop_command(sandbox_id: str, command: str) -> str:
    """Execute a command on the desktop sandbox. The sandbox has a full
    XFCE desktop with LibreOffice, a web browser, and standard Linux tools.
    Use this to run GUI applications, generate documents, and produce
    reports."""
    result = sandbox_client.execute(sandbox_id, command)
    output = result.get("stdout", "")
    errors = result.get("stderr", "")
    exit_code = result.get("exit_code", -1)
    return f"Exit code: {exit_code}\n\nSTDOUT:\n{output}\n\nSTDERR:\n{errors}"

@tool
def take_desktop_screenshot(sandbox_id: str) -> str:
    """Take a screenshot of the desktop sandbox. Saves the screenshot
    locally and returns the file path. Use this to verify that GUI
    applications are rendering correctly."""
    image_bytes = sandbox_client.take_screenshot(sandbox_id)
    path = f"/tmp/screenshot_{sandbox_id}.png"
    with open(path, "wb") as f:
        f.write(image_bytes)
    return f"Screenshot saved to {path}"
Tool docstrings matter.

CrewAI passes the docstring to the LLM as the tool description. Be specific about what the tool does, what it returns, and when to use it. Vague docstrings lead to agents misusing tools.

Creating the three-agent crew

Now we define the agents and their tasks. Each agent has a role, a goal, a backstory (which shapes how the LLM behaves), and a set of tools. The key insight: each agent gets tools for a specific sandbox type.

Agent definitions

python — agents.py
from crewai import Agent
from sandbox_tools import (
    browse_url, search_web,
    run_command, write_file, read_file,
    desktop_command, take_desktop_screenshot,
)


researcher = Agent(
    role="Senior Research Analyst",
    goal="Thoroughly research a GitHub repository by browsing its documentation, "
         "README, wiki, and related web resources. Produce a structured summary "
         "of the project's purpose, architecture, dependencies, and notable patterns.",
    backstory="You are an expert technical researcher who specializes in analyzing "
              "open-source projects. You have access to a dedicated browser sandbox. "
              "You browse real web pages to gather information. You never fabricate "
              "content — you only report what you actually read on the page.",
    tools=[browse_url, search_web],
    verbose=True,
    memory=True,
)

developer = Agent(
    role="Senior Software Developer",
    goal="Clone a GitHub repository, analyze its code structure, install "
         "dependencies, run the test suite, and report on code quality, "
         "test coverage, and any issues found.",
    backstory="You are a seasoned developer with deep expertise in Python, "
              "JavaScript, and system administration. You have a dedicated terminal "
              "sandbox with full shell access. You clone repos, run tests, read code, "
              "and produce detailed technical assessments.",
    tools=[run_command, write_file, read_file],
    verbose=True,
    memory=True,
)

analyst = Agent(
    role="Technical Report Analyst",
    goal="Compile research findings and technical assessments into a polished "
         "report with structured sections, tables, and a professional format. "
         "Use the desktop environment to produce a finished document.",
    backstory="You are a technical writer and analyst who produces executive-grade "
              "reports. You have a desktop sandbox with LibreOffice and full Linux "
              "GUI tools. You write reports in Markdown, convert them to PDF using "
              "pandoc, and verify the output looks correct.",
    tools=[desktop_command, take_desktop_screenshot, read_file],
    verbose=True,
    memory=True,
)

Task definitions

Each task is assigned to one agent and describes what the agent should do. Tasks can depend on each other — the analyst's task depends on the researcher's and developer's output.

python — tasks.py
from crewai import Task
from agents import researcher, developer, analyst


def create_tasks(repo_url: str, sandbox_ids: dict) -> list:
    """Create tasks for analyzing a GitHub repo.

    sandbox_ids should be:
        {"researcher": "sandbox_xxx", "developer": "sandbox_yyy", "analyst": "sandbox_zzz"}
    """

    research_task = Task(
        description=f"""Research the GitHub repository at {repo_url}.

Your browser sandbox ID is: {sandbox_ids['researcher']}
Pass this sandbox_id to every tool call.

Steps:
1. Browse the repository's main page at {repo_url}
2. Read the README.md by browsing {repo_url}/blob/main/README.md
3. Check for documentation in /docs or a wiki
4. Search the web for "{repo_url.split('/')[-1]} tutorial" and
   "{repo_url.split('/')[-1]} architecture" to find community resources
5. Browse at least 2 of the most relevant search results

Produce a structured research summary with:
- Project purpose and description
- Key features and capabilities
- Architecture overview (if documented)
- Dependencies and technology stack
- Community size and activity
- Notable patterns or design decisions""",
        expected_output="A detailed research summary in Markdown format with sections for "
                        "purpose, features, architecture, dependencies, community, and patterns.",
        agent=researcher,
    )

    development_task = Task(
        description=f"""Analyze the codebase of {repo_url} by cloning it and running its tests.

Your terminal sandbox ID is: {sandbox_ids['developer']}
Pass this sandbox_id to every tool call.

Steps:
1. Clone the repository:
   run_command(sandbox_id, "git clone {repo_url} /workspace/repo")
2. Explore the project structure:
   run_command(sandbox_id, "find /workspace/repo -type f -name '*.py' | head -50")
   run_command(sandbox_id, "cat /workspace/repo/README.md")
3. Check for a requirements.txt or pyproject.toml:
   run_command(sandbox_id, "ls /workspace/repo/requirements*.txt /workspace/repo/pyproject.toml 2>/dev/null")
4. Install dependencies:
   run_command(sandbox_id, "cd /workspace/repo && pip install -e '.[dev]' 2>/dev/null || pip install -r requirements.txt 2>/dev/null || pip install -e .")
5. Run the test suite:
   run_command(sandbox_id, "cd /workspace/repo && python -m pytest --tb=short -q 2>&1 | tail -40")
6. Check code quality:
   run_command(sandbox_id, "cd /workspace/repo && pip install ruff && ruff check . 2>&1 | tail -30")
7. Count lines of code:
   run_command(sandbox_id, "find /workspace/repo -name '*.py' -not -path '*/venv/*' | xargs wc -l 2>/dev/null | tail -5")

Produce a technical assessment covering:
- Project structure and organization
- Dependency analysis (what it requires, any concerns)
- Test results (pass/fail counts, coverage if available)
- Code quality issues found
- Lines of code and project size
- Build/install experience (smooth or problematic)""",
        expected_output="A technical assessment in Markdown format with sections for structure, "
                        "dependencies, test results, code quality, project size, and build experience.",
        agent=developer,
    )

    analysis_task = Task(
        description=f"""Compile the research findings and technical assessment into a
polished final report.

Your desktop sandbox ID is: {sandbox_ids['analyst']}
Pass this sandbox_id to every tool call.

You will receive the outputs from the researcher and developer agents.
Use them to produce a comprehensive report.

Steps:
1. Write the report as a Markdown file:
   desktop_command(sandbox_id, "cat > /workspace/report.md << 'REPORT'\n...\nREPORT")

   The report should include these sections:
   - Executive Summary (3-4 sentences)
   - Project Overview (from research)
   - Technical Architecture (from research + code analysis)
   - Code Quality Assessment (from developer)
   - Test Suite Analysis (from developer)
   - Strengths and Weaknesses
   - Recommendations
   - Appendix: Raw Metrics

2. Convert to PDF using pandoc:
   desktop_command(sandbox_id, "apt-get install -y pandoc texlive-latex-base texlive-fonts-recommended 2>/dev/null")
   desktop_command(sandbox_id, "cd /workspace && pandoc report.md -o report.pdf --pdf-engine=pdflatex -V geometry:margin=1in -V fontsize=11pt")

3. Verify the PDF was created:
   desktop_command(sandbox_id, "ls -la /workspace/report.pdf")

4. Take a screenshot of the desktop to confirm:
   take_desktop_screenshot(sandbox_id)""",
        expected_output="A comprehensive analysis report in Markdown format. Also confirm that "
                        "a PDF version was generated at /workspace/report.pdf.",
        agent=analyst,
        context=[research_task, development_task],  # depends on both
    )

    return [research_task, development_task, analysis_task]
Notice the context parameter.

The analysis_task receives context=[research_task, development_task]. This tells CrewAI that the analyst needs the output from both other tasks before it can start. The researcher and developer run in parallel (no dependencies between them), then the analyst runs once both finish.

Full working example: analyze a GitHub repo

Here's the complete script that provisions three sandboxes, creates the crew, and kicks off the analysis. Copy this, set your API keys, and run it.

python — analyze_repo.py
#!/usr/bin/env python3
"""
CrewAI + Sandbox Platform: Analyze a GitHub repo with 3 dedicated agents.

Each agent gets its own sandbox:
  - Researcher: browser sandbox (Chrome) for web research
  - Developer:  terminal sandbox for cloning, building, testing
  - Analyst:    desktop sandbox (XFCE + LibreOffice) for report generation

Usage:
    export SANDBOX_API_KEY="ab0t_sk_live_YOUR_KEY"
    export SANDBOX_URL="https://sandbox.dev.ab0t.com"
    export OPENAI_API_KEY="sk-YOUR_KEY"
    python analyze_repo.py https://github.com/pydantic/pydantic
"""

import sys
import time
from crewai import Crew, Process
from sandbox_client import sandbox_client
from agents import researcher, developer, analyst
from tasks import create_tasks


def provision_sandboxes() -> dict:
    """Create three sandboxes, one per agent role."""
    print("Provisioning 3 sandboxes...")
    start = time.time()

    # Create all three in parallel-ish (the API handles queueing)
    sandbox_configs = {
        "researcher": {
            "name": "crew-researcher",
            "type": "browser",        # Chrome browser sandbox
            "instance": "ab0t.small",   # 2 vCPU, 2 GB RAM
        },
        "developer": {
            "name": "crew-developer",
            "type": "terminal",      # Terminal-only sandbox
            "instance": "ab0t.medium",  # 2 vCPU, 4 GB (needs RAM for builds)
        },
        "analyst": {
            "name": "crew-analyst",
            "type": "desktop",       # Full XFCE desktop sandbox
            "instance": "ab0t.medium",  # 2 vCPU, 4 GB (desktop needs memory)
        },
    }

    sandbox_ids = {}
    for role, config in sandbox_configs.items():
        sandbox = sandbox_client.create_sandbox(
            name=config["name"],
            sandbox_type=config["type"],
            instance_type=config["instance"],
        )
        sandbox_ids[role] = sandbox["sandbox_id"]
        print(f"  {role}: {sandbox['sandbox_id']} ({config['type']}, {config['instance']})")

    elapsed = time.time() - start
    print(f"All sandboxes ready in {elapsed:.0f}s\n")
    return sandbox_ids


def cleanup_sandboxes(sandbox_ids: dict):
    """Stop all sandboxes to stop billing."""
    print("\nCleaning up sandboxes...")
    for role, sid in sandbox_ids.items():
        try:
            sandbox_client.stop_sandbox(sid)
            print(f"  Stopped {role}: {sid}")
        except Exception as e:
            print(f"  Warning: could not stop {role} ({sid}): {e}")


def main():
    if len(sys.argv) < 2:
        print("Usage: python analyze_repo.py <github_repo_url>")
        print("Example: python analyze_repo.py https://github.com/pydantic/pydantic")
        sys.exit(1)

    repo_url = sys.argv[1]
    print(f"Analyzing: {repo_url}\n")

    # Step 1: Provision sandboxes
    sandbox_ids = provision_sandboxes()

    try:
        # Step 2: Create tasks with sandbox IDs baked in
        tasks = create_tasks(repo_url, sandbox_ids)

        # Step 3: Assemble the crew
        crew = Crew(
            agents=[researcher, developer, analyst],
            tasks=tasks,
            process=Process.sequential,  # researcher & developer first, then analyst
            verbose=True,
            memory=True,
            max_rpm=30,  # rate-limit LLM calls
        )

        # Step 4: Kick off the crew
        print("Starting crew execution...\n")
        result = crew.kickoff()

        # Step 5: Print the final report
        print("\n" + "=" * 72)
        print("FINAL REPORT")
        print("=" * 72)
        print(result)

        # Step 6: Download the PDF from the analyst's sandbox
        try:
            pdf_bytes = sandbox_client.download_file(
                sandbox_ids["analyst"], "/workspace/report.pdf"
            )
            with open("report.pdf", "wb") as f:
                f.write(pdf_bytes)
            print(f"\nPDF report saved to: report.pdf ({len(pdf_bytes):,} bytes)")
        except Exception as e:
            print(f"\nNote: Could not download PDF: {e}")
            print("The Markdown report above is the primary output.")

    finally:
        # Always clean up sandboxes
        cleanup_sandboxes(sandbox_ids)


if __name__ == "__main__":
    main()

Run it:

bash
python analyze_repo.py https://github.com/pydantic/pydantic

# Output:
# Analyzing: https://github.com/pydantic/pydantic
#
# Provisioning 3 sandboxes...
#   researcher: sandbox_r1a2b3 (browser, ab0t.small)
#   developer: sandbox_d4e5f6 (terminal, ab0t.medium)
#   analyst: sandbox_a7b8c9 (desktop, ab0t.medium)
# All sandboxes ready in 38s
#
# Starting crew execution...
#
# [Researcher] Browsing https://github.com/pydantic/pydantic ...
# [Developer]  Cloning repository ...
# [Developer]  Running pytest ...
# [Analyst]    Compiling final report ...
#
# ========================================================================
# FINAL REPORT
# ========================================================================
# # Pydantic Analysis Report
# ## Executive Summary ...
#
# PDF report saved to: report.pdf (48,291 bytes)
#
# Cleaning up sandboxes...
#   Stopped researcher: sandbox_r1a2b3
#   Stopped developer: sandbox_d4e5f6
#   Stopped analyst: sandbox_a7b8c9

Assigning different sandbox types per role

The example above uses three sandbox types. Here's when to use each one and how to choose the right instance size.

Sandbox TypeWhat It HasBest ForMin Instance
browser Headless Chrome, CDP endpoint, Playwright pre-installed Web research, scraping, form filling, screenshot analysis ab0t.small (2 GB)
terminal Ubuntu shell, build tools, package managers, git Code execution, testing, builds, file processing ab0t.micro (1 GB)
desktop XFCE, LibreOffice, Firefox, noVNC, full GUI environment Document generation, spreadsheet processing, GUI automation ab0t.medium (4 GB)

Mix and match for your use case

The GitHub repo analysis is one pattern. Here are others:

Competitive Intelligence Crew

  • Scraper: browser sandbox — visits competitor websites
  • Analyzer: terminal sandbox — runs NLP/sentiment analysis
  • Reporter: desktop sandbox — generates executive brief

QA Testing Crew

  • Tester: browser sandbox — navigates the web app
  • Backend: terminal sandbox — runs API tests, checks logs
  • Documenter: terminal sandbox — compiles test results

You can also give multiple agents the same sandbox type with different configurations. Two researchers might each get a browser sandbox, but one has a larger instance for JavaScript-heavy sites:

python
# Two browser sandboxes, different sizes
fast_researcher = sandbox_client.create_sandbox(
    name="crew-researcher-fast",
    sandbox_type="browser",
    instance_type="ab0t.small",    # 2 GB — for static docs
)

heavy_researcher = sandbox_client.create_sandbox(
    name="crew-researcher-heavy",
    sandbox_type="browser",
    instance_type="ab0t.medium",   # 4 GB — for JS-heavy SPAs
)

# Two terminal sandboxes, different purposes
builder = sandbox_client.create_sandbox(
    name="crew-builder",
    sandbox_type="terminal",
    instance_type="ab0t.large",    # 8 GB — for compiling large projects
)

linter = sandbox_client.create_sandbox(
    name="crew-linter",
    sandbox_type="terminal",
    instance_type="ab0t.micro",    # 1 GB — just running ruff/eslint
)

Running agents in parallel

In the example above, we used Process.sequential for simplicity. But the researcher and developer have no dependencies — they can run at the same time. CrewAI supports this natively with Process.hierarchical or by structuring your task dependencies.

python — parallel crew setup
from crewai import Crew, Process

# The key: research_task and development_task have NO context dependencies.
# Only analysis_task depends on both via context=[research_task, development_task].
# CrewAI will run the independent tasks in parallel automatically
# when you use Process.hierarchical with a manager agent.

manager = Agent(
    role="Project Manager",
    goal="Coordinate the research, development, and analysis tasks efficiently.",
    backstory="You manage a technical team. Delegate tasks and ensure quality.",
    verbose=True,
)

crew = Crew(
    agents=[researcher, developer, analyst],
    tasks=[research_task, development_task, analysis_task],
    process=Process.hierarchical,
    manager_agent=manager,
    verbose=True,
)

# The manager will run researcher and developer in parallel,
# then hand off both outputs to the analyst.
result = crew.kickoff()
Parallel sandboxes = parallel billing.

When three sandboxes run for 10 minutes each in parallel, you pay for 30 sandbox-minutes. The wall-clock time is 10 minutes (plus the analyst's solo work after), but the cost is the same as sequential. The benefit is speed: your crew finishes in 15 minutes instead of 30.

Cost math for multi-agent runs

The question every developer needs to answer for their manager: "How much does a 3-agent crew cost per run?"

AgentSandbox TypeInstanceDurationCost/hrCost
Researcher browser ab0t.small ~8 min $0.1/hr $0.013
Developer terminal ab0t.medium ~12 min $0.08/hr $0.016
Analyst desktop ab0t.medium ~5 min $0.2/hr $0.017
Sandbox compute (3 agents, ~25 min total) $0.046
LLM tokens (GPT-4o, ~15K input + 5K output per agent) ~$0.30
Total cost per crew run ~$0.35

The sandbox compute is the cheap part. At ~$0.046 per run, you could execute about 21 crew runs for a dollar of compute. The LLM tokens still dominate the cost. This is true for almost every agent workload: compute is commoditized, intelligence is not.

Compare to doing this manually.

A senior developer spending 2 hours analyzing a GitHub repo costs $100–$200 in salary. A 3-agent crew costs $0.31 and takes 15 minutes. Even if you run it 10 times to get a great result, you're at $3.10 — still 30x cheaper than the human-hours alternative.

Scaling cost projections

ScenarioAgentsRuns/DayDaily CostMonthly Cost
One-off repo analysis31$0.31N/A
Daily competitor monitoring31$0.31$9.30
PR review automation210$2.10$63
Research team (5 topics/day)35$1.55$46.50
QA test suite (full matrix)53$2.50$75

Advanced patterns

Reusing sandboxes across runs

Creating sandboxes takes 20–40 seconds. If you're running the same crew repeatedly (e.g., daily competitor monitoring), keep the sandboxes alive between runs. Set auto_stop_minutes to a high value and reuse the sandbox IDs.

python
import json
from pathlib import Path

SANDBOX_CACHE = Path("sandbox_ids.json")

def get_or_create_sandboxes() -> dict:
    """Reuse existing sandboxes if they're still running."""

    if SANDBOX_CACHE.exists():
        cached = json.loads(SANDBOX_CACHE.read_text())
        # Verify they're still running
        all_running = True
        for role, sid in cached.items():
            try:
                status = sandbox_client.get_sandbox(sid)
                if status["status"] != "running":
                    all_running = False
                    break
            except Exception:
                all_running = False
                break
        if all_running:
            print("Reusing cached sandboxes")
            return cached

    # Create fresh sandboxes
    sandbox_ids = provision_sandboxes()
    SANDBOX_CACHE.write_text(json.dumps(sandbox_ids))
    return sandbox_ids

Passing files between agents

The researcher might find a CSV on the web. The developer might generate a test report. The analyst needs both. Use the sandbox file API to shuttle data between sandboxes:

python
def transfer_file(from_sandbox: str, from_path: str,
                    to_sandbox: str, to_path: str):
    """Copy a file from one sandbox to another."""
    content = sandbox_client.download_file(from_sandbox, from_path)
    sandbox_client.upload_file(
        to_sandbox, to_path, content.decode("utf-8", errors="replace")
    )
    print(f"Transferred {from_path} -> {to_path}")

# Example: copy test results from developer to analyst
transfer_file(
    from_sandbox=sandbox_ids["developer"],
    from_path="/workspace/repo/test_results.xml",
    to_sandbox=sandbox_ids["analyst"],
    to_path="/workspace/test_results.xml",
)

Error handling and retries

Sandboxes can timeout. Commands can fail. Browsers can crash. Wrap your tools with proper error handling so agents can recover:

python — resilient tool example
import httpx
from crewai.tools import tool

@tool
def run_command_safe(sandbox_id: str, command: str) -> str:
    """Execute a shell command with automatic retry on transient failures.
    Retries up to 2 times on timeout or connection errors."""
    max_retries = 2
    for attempt in range(max_retries + 1):
        try:
            result = sandbox_client.execute(sandbox_id, command)
            exit_code = result.get("exit_code", -1)
            stdout = result.get("stdout", "")
            stderr = result.get("stderr", "")

            if exit_code != 0:
                return (f"Command failed (exit code {exit_code}).\n"
                        f"STDOUT:\n{stdout}\nSTDERR:\n{stderr}\n"
                        f"Try a different approach or fix the command.")

            return f"Success (exit code 0).\nOutput:\n{stdout}"

        except httpx.TimeoutException:
            if attempt < max_retries:
                continue
            return "Command timed out after multiple retries. The command may be " \
                   "taking too long. Try breaking it into smaller steps."

        except httpx.HTTPStatusError as e:
            return f"API error: {e.response.status_code} - {e.response.text}"

Tips for production CrewAI + Sandbox setups

Pin your sandbox environments

Use a custom Docker image for each sandbox type so the environment is deterministic. A browser sandbox should always have the same Chrome version. A terminal sandbox should always have the same Python version. Pass docker_image when creating sandboxes.

Use memory wisely

CrewAI's memory=True gives agents short-term memory within a run and long-term memory across runs. Combined with sandbox persistence, your crew can resume where it left off. The analyst can reference findings from yesterday's run.

Set token limits per agent

The developer agent can generate enormous output by running pytest -v on a large project. Use max_iter on agents and truncate tool outputs to keep token costs reasonable:

python
developer = Agent(
    role="Senior Software Developer",
    goal="Analyze code and run tests.",
    backstory="...",
    tools=[run_command, write_file, read_file],
    max_iter=15,        # Maximum 15 tool calls per task
    max_rpm=10,         # Maximum 10 LLM requests per minute
    verbose=True,
)

# In your run_command tool, truncate long output:
@tool
def run_command(sandbox_id: str, command: str) -> str:
    """Execute a shell command. Output is truncated to 4000 characters."""
    result = sandbox_client.execute(sandbox_id, command)
    output = result.get("stdout", "")
    if len(output) > 4000:
        output = output[:2000] + "\n\n... (truncated) ...\n\n" + output[-2000:]
    return f"Exit: {result.get('exit_code')}\n{output}"

Log everything

Add logging to your sandbox client so you have a record of every API call, every command executed, and every sandbox created. When a crew run fails at 3 AM, logs are how you debug it:

python
import logging

logging.basicConfig(
    filename="crew_run.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

# In SandboxClient.execute():
def execute(self, sandbox_id, command, **kwargs):
    logging.info(f"EXEC [{sandbox_id}]: {command[:200]}")
    result = self.client.post(...)
    logging.info(f"RESULT [{sandbox_id}]: exit={result.json().get('exit_code')}")
    return result.json()

Always clean up sandboxes

Use try/finally (as shown in the full example) to stop sandboxes even if the crew throws an exception. A forgotten ab0t.medium running for a week costs $6.72. Not a disaster, but annoying when multiplied by 10 crews.

Troubleshooting

CrewAI agent calls the wrong tool

This usually means the tool docstrings are ambiguous. Make each tool's description clearly state what it does, what sandbox type it requires, and when to use it vs. other tools. Also ensure each agent only has tools for its sandbox type — don't give the researcher terminal tools.

Sandbox times out during creation

Desktop sandboxes take longer to boot (30–60 seconds) because they need to start the X11 display server. Increase the polling timeout in create_sandbox() or provision desktop sandboxes first while terminal and browser sandboxes boot faster.

"Connection refused" from sandbox API calls

The sandbox might not be fully initialized when the first command runs. Add a health-check step after creation:

python
# After create_sandbox returns, verify the sandbox is responsive:
result = sandbox_client.execute(sandbox_id, "echo ready")
assert result["stdout"].strip() == "ready", "Sandbox not responsive"

Analyst can't install pandoc

Desktop sandboxes run as non-root by default. Prefix commands with sudo. Or better: build a custom Docker image with pandoc and LaTeX pre-installed so the analyst doesn't waste time on package installation during every run.

Agent gets stuck in a loop

Set max_iter on the agent to cap the number of tool calls per task. Without it, an agent that keeps getting errors can loop indefinitely, burning tokens. A value of 15–25 is usually enough for complex tasks.

LLM rate limit errors

When three agents run in parallel, they triple your LLM API request rate. Set max_rpm on the Crew or individual agents. For GPT-4o, 30 RPM per crew is a safe starting point. For Claude, the limit depends on your tier.

What's next

Give every agent its own machine

Browser, terminal, or desktop. Each crew member gets dedicated compute. $0.01 per run.

Get Started Free