The LangSmith SDK provides a programmatic interface to create and interact with sandboxes.
Install
# uv
uv add "langsmith[sandbox] @ git+https://github.com/langchain-ai/langsmith-sdk#subdirectory=python"
# pip
pip install "langsmith[sandbox] @ git+https://github.com/langchain-ai/langsmith-sdk#subdirectory=python"
The [sandbox] extra for Python installs websockets, which enables real-time streaming and timeout=0. Without it, run() falls back to HTTP automatically. For TypeScript, install the optional ws package for WebSocket streaming:
Create and run a sandbox
from langsmith.sandbox import SandboxClient
# Client uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
client = SandboxClient()
# Create a template (defines the container image)
client.create_template(
name="python-sandbox",
image="python:3.12-slim",
)
# Create a sandbox from the template and run code
with client.sandbox(template_name="python-sandbox") as sb:
result = sb.run("python -c 'print(2 + 2)'")
print(result.stdout) # "4\n"
print(result.success) # True
Run commands
Every run() call returns an ExecutionResult with stdout, stderr, exit_code, and success.
with client.sandbox(template_name="my-sandbox") as sb:
result = sb.run("echo 'Hello, World!'")
print(result.stdout) # "Hello, World!\n"
print(result.stderr) # ""
print(result.exit_code) # 0
print(result.success) # True
# Commands that fail return non-zero exit codes
result = sb.run("exit 1")
print(result.success) # False
print(result.exit_code) # 1
Stream output
For long-running commands, stream output in real time using callbacks or a CommandHandle.
Stream with callbacks
import sys
with client.sandbox(template_name="my-sandbox") as sb:
result = sb.run(
"make build",
timeout=600,
on_stdout=lambda s: print(s, end=""),
on_stderr=lambda s: print(s, end="", file=sys.stderr),
)
print(f"\nBuild {'succeeded' if result.success else 'failed'}")
Stream with CommandHandle
Set wait=False to get a CommandHandle for full control over the output stream.
with client.sandbox(template_name="my-sandbox") as sb:
handle = sb.run("make build", timeout=600, wait=False)
print(f"Command ID: {handle.command_id}")
for chunk in handle:
prefix = "OUT" if chunk.stream == "stdout" else "ERR"
print(f"[{prefix}] {chunk.data}", end="")
result = handle.result
print(f"\nExit code: {result.exit_code}")
Send stdin and kill commands
with client.sandbox(template_name="my-sandbox") as sb:
handle = sb.run(
"python -c 'name = input(\"Name: \"); print(f\"Hello {name}\")'",
timeout=30,
wait=False,
)
for chunk in handle:
if "Name:" in chunk.data:
handle.send_input("World\n")
print(chunk.data, end="")
result = handle.result
Kill a running command:
with client.sandbox(template_name="my-sandbox") as sb:
handle = sb.run("python server.py", timeout=0, wait=False)
for chunk in handle:
print(chunk.data, end="")
if "Ready" in chunk.data:
break
handle.kill()
Reconnect to a running command
If a client disconnects, reconnect using the command ID:
with client.sandbox(template_name="my-sandbox") as sb:
handle = sb.run("make build", timeout=600, wait=False)
command_id = handle.command_id
# Later, possibly in a different process
handle = sb.reconnect(command_id)
for chunk in handle:
print(chunk.data, end="")
result = handle.result
File operations
Read and write files in the sandbox:
with client.sandbox(template_name="my-python") as sb:
# Write a file
sb.write("/app/script.py", "print('Hello from file!')")
# Run the script
result = sb.run("python /app/script.py")
print(result.stdout) # "Hello from file!\n"
# Read a file (returns bytes)
content = sb.read("/app/script.py")
print(content.decode()) # "print('Hello from file!')"
# Write binary files
sb.write("/app/data.bin", b"\x00\x01\x02\x03")
Command lifecycle and TTL
The sandbox daemon manages command session lifecycles with two timeout mechanisms:
- Session TTL (finished commands): After a command finishes, its session remains in memory for a TTL period. During this window you can reconnect to retrieve output. After the TTL expires, the session is cleaned up.
- Idle timeout (running commands): Running commands with no connected clients are killed after an idle timeout (default: 5 minutes). The idle timer resets each time a client connects. Set to
-1 for no idle timeout.
Combine lifecycle options
with client.sandbox(template_name="my-sandbox") as sb:
# Long-running task: 30-min idle timeout, 1-hour session TTL
handle = sb.run(
"python train.py",
timeout=0, # No command timeout
idle_timeout=1800, # Kill after 30min with no clients
ttl_seconds=3600, # Keep session for 1 hour after exit
wait=False,
)
# Fire-and-forget: no idle timeout, infinite TTL
handle = sb.run(
"python background_job.py",
timeout=0,
idle_timeout=-1, # Never kill due to idle
ttl_seconds=-1, # Keep session forever
wait=False,
)
Set kill_on_disconnect=True (Python) or killOnDisconnect: true (TypeScript) to kill the command immediately when the last client disconnects, instead of waiting for the idle timeout.
Service URLs (Python)
Access an HTTP service running inside a sandbox via an authenticated URL. You can open it in a browser, call it from code, or share it with a teammate.
with client.sandbox(template_name="my-sandbox") as sb:
sb.run("python -m http.server 8000", timeout=0, wait=False)
svc = sb.service(port=8000)
# Open in a browser
print(svc.browser_url)
# Or make requests with built-in helpers (auth is injected automatically)
resp = svc.get("/api/data")
resp = svc.post("/api/data", json={"key": "value"})
For more details, including use cases, REST API access, and a full FastAPI example, see Service URLs.
TCP tunnels (Python)
Access any TCP service running inside a sandbox as if it were local. The tunnel opens a local TCP port and forwards connections through a WebSocket to the target port inside the sandbox.
import psycopg2
# Template uses the official postgres:16 image
sb = client.create_sandbox(template_name="my-postgres")
pg_handle = sb.run(
"POSTGRES_HOST_AUTH_METHOD=trust docker-entrypoint.sh postgres",
timeout=0,
wait=False,
)
import time; time.sleep(6) # Wait for Postgres to start
try:
with sb.tunnel(remote_port=5432, local_port=25432) as t:
conn = psycopg2.connect(
host="127.0.0.1",
port=t.local_port,
user="postgres",
)
cursor = conn.cursor()
cursor.execute("SELECT version()")
print(cursor.fetchone())
conn.close()
finally:
pg_handle.kill()
client.delete_sandbox(sb.name)
Tunnels work with any TCP service (Redis, HTTP servers, etc.) and you can open multiple tunnels simultaneously:
with sb.tunnel(remote_port=5432, local_port=25432) as t1, \
sb.tunnel(remote_port=6379, local_port=26379) as t2:
# Use both Postgres and Redis simultaneously
pass
Async support (Python)
The Python SDK provides a full async client:
from langsmith.sandbox import AsyncSandboxClient
async def main():
async with AsyncSandboxClient() as client:
await client.create_template(name="async-python", image="python:3.12-slim")
async with await client.sandbox(template_name="async-python") as sb:
result = await sb.run("python -c 'print(1 + 1)'")
print(result.stdout) # "2\n"
await sb.write("/app/test.txt", "async content")
content = await sb.read("/app/test.txt")
print(content.decode())
# Async streaming
handle = await sb.run("make build", timeout=600, wait=False)
async for chunk in handle:
print(chunk.data, end="")
result = await handle.result
# Async service URLs
svc = await sb.service(port=8000)
resp = await svc.get("/api/data")
url = await svc.get_service_url()
token = await svc.get_token()
Trace sandbox activity
Pass LangSmith tracing environment variables through the env parameter on run() to send traces from code running inside a sandbox. Call flush() before the process exits to ensure all traces are delivered.
from langsmith.sandbox import SandboxClient
client = SandboxClient()
tracing_env = {
"LANGSMITH_API_KEY": "lsv2_pt_...",
"LANGSMITH_ENDPOINT": "https://api.smith.langchain.com",
"LANGSMITH_TRACING": "true",
"LANGSMITH_PROJECT": "my-sandbox-traces",
}
with client.sandbox(template_name="my-template") as sandbox:
sandbox.run("pip install langsmith", timeout=120, env=tracing_env)
result = sandbox.run("python3 my_agent.py", env=tracing_env)
print(result.stdout)
Inside the sandbox, any LangSmith-instrumented code (@traceable, LangChain, LangGraph) automatically picks up the tracing configuration from the injected environment variables.
Always call flush() before the sandbox process exits — langsmith.Client().flush() in Python or await new Client().flush() in TypeScript. Without it, traces may be lost because the container is destroyed when the command finishes.
Error handling
Both SDKs provide typed exceptions for specific error handling:
from langsmith.sandbox import (
SandboxClientError, # Base exception
ResourceCreationError, # Provisioning failed
ResourceNotFoundError, # Resource doesn't exist
ResourceTimeoutError, # Operation timed out
SandboxNotReadyError, # Sandbox not ready yet
SandboxConnectionError, # Network/WebSocket error
CommandTimeoutError, # Command exceeded timeout
QuotaExceededError, # Quota limit reached
)
try:
with client.sandbox(template_name="my-sandbox") as sb:
result = sb.run("sleep 999", timeout=10)
except CommandTimeoutError as e:
print(f"Command timed out: {e}")
except ResourceNotFoundError as e:
print(f"{e.resource_type} not found: {e}")
except SandboxClientError as e:
print(f"Error: {e}")
For more details, see the sandbox SDK reference on GitHub for Python or TypeScript.