- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Cloudflare (Edge, Advanced)
The Cloudflare sandbox runs the agent loop, the run history, and the container in one deploy. A run continues after the user closes the tab.
@tanstack/ai-sandbox-cloudflare gives you both. createCloudflareSandboxAgent builds the Worker routes, a coordinator Durable Object that drives each run and keeps its log, and the container plumbing.
For the provider-neutral basics (workspace, tools, policy, lifecycle), start at the Overview.
Deploy an agent at the edge#
- Install the packages.
npm i @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code zodpnpm add @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code zodyarn add @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code zodbun add @tanstack/ai @tanstack/ai-sandbox @tanstack/ai-sandbox-cloudflare @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code zodnpm i -D wranglerpnpm add -D wrangleryarn add -D wranglerbun add -d wrangler- Add a Dockerfile for the sandbox container. Start from the cloudflare/sandbox base image, and install your harness CLI. Match the image tag to the @cloudflare/sandbox version in your lockfile.
FROM docker.io/cloudflare/sandbox:0.12.1
RUN npm install -g @anthropic-ai/claude-code --include=optional && claude --version- Declare the container and the two Durable Objects in wrangler.jsonc.
{
"name": "edge-agent",
"main": "src/worker.ts",
"compatibility_date": "2026-05-01",
"compatibility_flags": ["nodejs_compat"],
"containers": [{ "class_name": "Sandbox", "image": "./Dockerfile", "max_instances": 5 }],
"durable_objects": {
"bindings": [
// The coordinator that owns each run and its log.
{ "name": "RUN_COORDINATOR", "class_name": "RunCoordinator" },
// The container host.
{ "name": "Sandbox", "class_name": "Sandbox" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["RunCoordinator", "Sandbox"] }
],
"vars": { "SANDBOX_TRANSPORT": "rpc" }
}- Create the agent. The package binds no API key, so declare the key your harness needs and pass it as a workspace secret. This example uses ANTHROPIC_API_KEY for Claude Code. Grok Build uses XAI_API_KEY, and Codex uses CODEX_API_KEY.
// src/agent.ts
import { claudeCodeText } from '@tanstack/ai-claude-code'
import {
createSecrets,
defineSandbox,
defineWorkspace,
} from '@tanstack/ai-sandbox'
import { cloudflareSandbox } from '@tanstack/ai-sandbox-cloudflare'
import { createCloudflareSandboxAgent } from '@tanstack/ai-sandbox-cloudflare/agent'
import type { SandboxAgentEnv } from '@tanstack/ai-sandbox-cloudflare/agent'
export interface AppEnv extends SandboxAgentEnv {
ANTHROPIC_API_KEY: string
}
export const agent = createCloudflareSandboxAgent<AppEnv>({
adapter: () => claudeCodeText('claude-opus-4-8'),
sandbox: (_input, env) =>
defineSandbox({
id: 'cf-edge-agent',
provider: cloudflareSandbox({ binding: env.Sandbox }),
workspace: defineWorkspace({
// The image already has the CLI. Nothing to clone.
source: { type: 'none' },
secrets: createSecrets({ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY }),
}),
// One sandbox per thread, so a follow-up run reuses the workspace.
lifecycle: { reuse: 'thread' },
}),
})- Export the Worker and the Durable Object classes, set the key, and deploy.
// src/worker.ts
import { agent } from './agent'
// The class names in wrangler.jsonc.
export const RunCoordinator = agent.Coordinator
export const Sandbox = agent.Sandbox
export default agent.workernpx wrangler secret put ANTHROPIC_API_KEY
npx wrangler deployStart a run from the client#
POST /runs starts a run and returns 202 { runId } at once. Then open a WebSocket on /runs/<runId>/stream to tail it.
export async function startRun(threadId: string, prompt: string) {
const response = await fetch('/runs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
threadId,
messages: [{ role: 'user', content: prompt }],
}),
})
const body: unknown = await response.json()
if (
typeof body !== 'object' ||
body === null ||
!('runId' in body) ||
typeof body.runId !== 'string'
) {
throw new Error('POST /runs returned no runId')
}
return body.runId
}
// Pass the last `seq` you saw to resume after a drop. -1 replays from the start.
export function tailRun(threadId: string, runId: string, lastSeq = -1) {
const url = new URL(`/runs/${runId}/stream`, window.location.href)
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
url.searchParams.set('threadId', threadId)
url.searchParams.set('lastSeq', String(lastSeq))
const socket = new WebSocket(url)
socket.addEventListener('message', (message) => {
// `{ seq, chunk }` per event, then one `{ type: 'status', record }`.
console.log(JSON.parse(String(message.data)))
})
return socket
}Call startRun, then tailRun. Close the tab in the middle, open it again, and tail with your last seq: the run kept going.
How it works#
- The Worker only routes. POST /runs goes to the coordinator Durable Object for that threadId, which registers the run and returns. The Worker does not wait for the agent.
- The coordinator drives the run. It runs chat() with your adapter. The agent CLI runs in the container. Your chat() tools reach the agent through an MCP bridge on the coordinator's own fetch handler.
- Every chunk goes to a run log in Durable Object storage, under a growing seq. Clients tail that log from a cursor. A refresh, a dropped WebSocket, or a coordinator that hibernates between chunks never touches the sandbox.
- The container is reached over the network, through callback URLs that the coordinator derives from the request host. A *.workers.dev deploy needs no config.
This is the default DO-drives model. In the co-located model, the agent loop runs inside the container instead. See Cloudflare Edge Details.
Durable runs at the edge#
Cloudflare is the log-first tier of the durable runs protocol. The coordinator's run log is the durable copy. A closed tab or dropped WebSocket only detaches the tail: the coordinator keeps driving, and the log keeps filling.
Two rules matter in production:
- Stop must destroy the container. cloudflareSandbox declares killableProcesses: false: no signal crosses the Workers RPC boundary. So the only cancel that stops the agent is to destroy the container. A run marked aborted without the destroy keeps working, and billing, behind a UI that says "stopped". See what cancel means on a provider that cannot kill.
- The run log survives, the container does not. If the container stops, the agent dies mid-run, like anywhere else. The log keeps everything emitted up to that point.
Warning
The Sandbox container class has a sleepAfter idle timeout. A detached run that is quiet (nobody connected, the agent thinking) can look idle, and an idle container sleeps. The run then "dies for no reason", while the log keeps its prefix. Set sleepAfter well above your longest quiet stretch before you rely on detached runs.
Stall watchdog#
createCloudflareSandboxAgent sets one stall policy for the whole app. stallTimeoutMs applies to every run, in both do-drives (the default) and colocated mode:
| Value | Behavior |
|---|---|
| Omitted | A run with no persisted activity for 300000 ms (five minutes) is stalled. |
| Positive safe integer | Use that many milliseconds as the limit. |
| false | No stall detection. |
To allow ten quiet minutes, add one line next to adapter in src/agent.ts:
export const agent = createCloudflareSandboxAgent<AppEnv>({
adapter: () => claudeCodeText('claude-opus-4-8'),
+ stallTimeoutMs: 10 * 60_000,
sandbox: (_input, env) =>How it decides:
- The alarm checks every 30 seconds, or every stallTimeoutMs when that is shorter. So detection can lag the limit by up to one check.
- Activity resets the clock. Persisted run events do. Authenticated /_bridge and /tool-exec callbacks do too, when they arrive and when they complete. Unknown-run and unauthorized requests do not.
- Set the limit above your longest quiet period. A native operation that emits no event and makes no callback is invisible to the watchdog.
- An in-flight authenticated callback is protected. A callback that never completes can leave an orphaned running record. So can false.
On a stall, the watchdog marks the run log failed. It does not promise to kill the agent process or the container. Use provider lifecycle controls and the reaper for cleanup.
Go deeper#
For the co-located model, live previews, callback hosts, the run log internals, and what edge durability does not cover, see Cloudflare Edge Details.