The LangSmith SDK provides a programmatic interface to create and interact with sandboxes.
Install
# uv
uv add "langsmith[sandbox]"
# pip
pip install "langsmith[sandbox]"
npm install langsmith
# or
yarn add langsmith
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
Pass a snapshot ID or name when you want to boot from a reusable custom filesystem image; see Snapshots for that flow.
from langsmith.sandbox import SandboxClient
# Client uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
client = SandboxClient()
# Create a sandbox with the default runtime and run code
with client.sandbox() as sb:
result = sb.run("python -c 'print(2 + 2)'")
print(result.stdout) # "4\n"
print(result.success) # True
import { SandboxClient } from "langsmith/sandbox";
// Client uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
const client = new SandboxClient();
// Create a sandbox with the default runtime and run code
const sandbox = await client.createSandbox();
const result = await sandbox.run("node -e 'console.log(2 + 2)'");
console.log(result.stdout); // "4\n"
// Don't forget to clean up
await sandbox.delete();
Run commands
Every run() call returns an ExecutionResult with stdout, stderr, exit_code, and success.
with client.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
const sandbox = await client.createSandbox();
try {
const result = await sandbox.run("echo 'Hello, World!'");
console.log(result.stdout); // "Hello, World!\n"
console.log(result.stderr); // ""
console.log(result.exit_code); // 0
// Pass environment variables and working directory
const envResult = await sandbox.run("echo $MY_VAR", {
env: { MY_VAR: "test-value" },
cwd: "/tmp",
});
} finally {
await sandbox.delete();
}
Stream output
For long-running commands, stream output in real time using callbacks or a CommandHandle.
Stream with callbacks
import sys
with client.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'}")
const result = await sandbox.run("make build", {
timeout: 600,
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stderr.write(data),
});
console.log(`Exit code: ${result.exit_code}`);
Stream with CommandHandle
Set wait=False to get a CommandHandle for full control over the output stream.
with client.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}")
const handle = await sandbox.run("python train.py", {
wait: false,
timeout: 600,
});
console.log(`Command ID: ${handle.commandId}`);
console.log(`PID: ${handle.pid}`);
for await (const chunk of handle) {
if (chunk.stream === "stdout") {
process.stdout.write(chunk.data);
} else {
process.stderr.write(chunk.data);
}
}
const result = await handle.result;
console.log(`Exit code: ${result.exit_code}`);
Send stdin and kill commands
with client.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
const handle = await sandbox.run("python -i", { wait: false });
// Send input to stdin
handle.sendInput("print(2 + 2)\n");
handle.sendInput("exit()\n");
for await (const chunk of handle) {
process.stdout.write(chunk.data);
}
Kill a running command:
with client.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()
const handle = await sandbox.run("sleep 300", { wait: false });
handle.kill();
const result = await handle.result;
console.log(result.exit_code); // non-zero
Reconnect to a running command
If a client disconnects, reconnect using the command ID:
with client.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
const handle = await sandbox.run("long-task", { wait: false });
const commandId = handle.commandId;
// Later, or from a different client
const newHandle = await sandbox.reconnect(commandId);
for await (const chunk of newHandle) {
process.stdout.write(chunk.data);
}
File operations
Read and write files in the sandbox:
with client.sandbox() 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")
const sandbox = await client.createSandbox();
try {
// Write a file (string content)
await sandbox.write("/app/script.py", "print('Hello from file!')");
// Run the script
const result = await sandbox.run("python /app/script.py");
console.log(result.stdout); // "Hello from file!\n"
// Read a file (returns Uint8Array)
const content = await sandbox.read("/app/script.py");
console.log(new TextDecoder().decode(content));
// Write binary files
await sandbox.write("/app/data.bin", new Uint8Array([0x00, 0x01, 0x02, 0x03]));
} finally {
await sandbox.delete();
}
Sandbox lifetime and retention
Sandboxes are governed by a two-stage retention model anchored to idle
activity and the stopped state.
| Field | What it controls | When it fires |
|---|
idle_ttl_seconds | The launcher stops the sandbox after this many seconds of inactivity. Any command execution or file I/O resets the timer. 0 disables the idle stop. | Default 600 (10 minutes) when omitted. |
delete_after_stop_seconds | Once the sandbox enters the stopped state, this timer starts. After it elapses, the sandbox row + filesystem clone are permanently deleted by a server-side sweep. 0 disables stop-anchored deletion (manual cleanup required). | Server applies its configured default (typically 14 days) when omitted. |
Both values must be multiples of 60 (minute resolution). The full lifecycle is:
running ──(idle for idle_ttl_seconds)──▶ stopped ──(delete_after_stop_seconds)──▶ deleted
You can also call stop_sandbox / stopSandbox explicitly — that also
populates stopped_at and starts the deletion timer.
# Default retention (server defaults: 10-min idle stop, 14-day delete)
with client.sandbox() as sb:
sb.run("echo hello")
# Aggressive: stop after 5 min idle, delete 1 hour after stop
sb = client.create_sandbox(
idle_ttl_seconds=300,
delete_after_stop_seconds=3600,
)
# Long-running: never auto-stop, delete 7 days after manual stop
sb = client.create_sandbox(
idle_ttl_seconds=0,
delete_after_stop_seconds=604800,
)
# Update retention on an existing sandbox
sb = client.update_sandbox(
sb.name,
idle_ttl_seconds=1800,
delete_after_stop_seconds=2592000, # 30 days
)
// Default retention (server defaults applied)
const sandbox = await client.createSandbox();
// Aggressive: stop after 5 min idle, delete 1 hour after stop
const sb = await client.createSandbox({
idleTtlSeconds: 300,
deleteAfterStopSeconds: 3600,
});
// Long-running: never auto-stop, delete 7 days after manual stop
const longRunning = await client.createSandbox({
idleTtlSeconds: 0,
deleteAfterStopSeconds: 604800,
});
// Update retention on an existing sandbox
await client.updateSandbox(sb.name, {
idleTtlSeconds: 1800,
deleteAfterStopSeconds: 2592000, // 30 days
});
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() 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,
)
const sandbox = await client.createSandbox();
try {
// Long-running task: 30-min idle timeout, 1-hour session TTL
const handle = await sandbox.run("python train.py", {
timeout: 0, // No command timeout
idleTimeout: 1800, // Kill after 30min with no clients
ttlSeconds: 3600, // Keep session for 1 hour after exit
wait: false,
});
// Fire-and-forget: no idle timeout, infinite TTL
const bg = await sandbox.run("python background_job.py", {
timeout: 0,
idleTimeout: -1, // Never kill due to idle
ttlSeconds: -1, // Keep session forever
wait: false,
});
} finally {
await sandbox.delete();
}
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() 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
# Snapshot built from the official postgres:16 image
sb = client.create_sandbox(snapshot_id=postgres_snapshot_id)
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:
async with await client.sandbox() 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() 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)
import { SandboxClient } from "langsmith/sandbox";
const client = new SandboxClient();
const tracingEnv = {
LANGSMITH_API_KEY: "lsv2_pt_...",
LANGSMITH_ENDPOINT: "https://api.smith.langchain.com",
LANGSMITH_TRACING: "true",
LANGSMITH_PROJECT: "my-sandbox-traces",
};
const sandbox = await client.createSandbox();
try {
await sandbox.run("pip install langsmith", { timeout: 120, env: tracingEnv });
const result = await sandbox.run("python3 my_agent.py", { env: tracingEnv });
console.log(result.stdout);
} finally {
await sandbox.delete();
}
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() 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}")
import {
LangSmithSandboxError,
LangSmithResourceNotFoundError,
LangSmithResourceTimeoutError,
LangSmithSandboxConnectionError,
LangSmithCommandTimeoutError,
LangSmithQuotaExceededError,
} from "langsmith/sandbox";
try {
const sandbox = await client.createSandbox("not-a-real-snapshot");
await sandbox.delete();
} catch (e) {
if (e instanceof LangSmithResourceNotFoundError) {
console.log(`${e.resourceType} not found: ${e.message}`);
} else if (e instanceof LangSmithResourceTimeoutError) {
console.log(`Timeout waiting for ${e.resourceType}: ${e.message}`);
} else if (e instanceof LangSmithSandboxError) {
console.log(`Error: ${e.message}`);
}
}
For more details, see the sandbox SDK reference on GitHub for Python or TypeScript.