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.
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
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}")
Click on any sandbox in the UI to access an interactive shell directly in your browser.
For more details, see the sandbox SDK reference on GitHub for Python or TypeScript.