惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Martin Fowler
Martin Fowler
博客园 - 【当耐特】
GbyAI
GbyAI
M
MIT News - Artificial intelligence
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
罗磊的独立博客
Apple Machine Learning Research
Apple Machine Learning Research
腾讯CDC
F
Fortinet All Blogs
IT之家
IT之家
WordPress大学
WordPress大学
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
Google DeepMind News
Google DeepMind News
Jina AI
Jina AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Help Net Security
V
Visual Studio Blog
小众软件
小众软件
Y
Y Combinator Blog

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors
Sandbox
2026-05-31 · via Workflow SDK Documentation

Model one Vercel Sandbox per workflow run — durable, idle-efficient, and not bound by the 5-hour sandbox hard cap.

Vercel Sandbox provides isolated code execution environments. The @vercel/sandbox package has first-class support for the Workflow SDK — the Sandbox class is serializable, and its methods (create, runCommand, stop, snapshot) implicitly run as steps. You can use Sandbox directly inside a workflow function without wrapping each call in a separate "use step" function.

A sandbox alone gets you an isolated VM. A workflow around it gets you a durable controller for that VM's entire lifetime:

  • One workflow run = one sandbox session. The runId is the only state you need to persist on the client. Close the tab, come back a week later, POST the same runId and you're back in the same session.
  • Efficient resource use. Active sandboxes cost money; hibernated workflows cost nothing. The workflow races a command hook against a sleep() timer — when idle, it calls sandbox.snapshot() (which also stops the VM) and waits indefinitely. Next command → spin a new sandbox from the snapshot with filesystem, installed packages, and git history intact.
  • Beyond the 5-hour hard cap. Every Vercel Sandbox has a maximum lifetime. The workflow tracks that deadline and proactively snapshots + recreates before the cap, so the logical session outlives any one VM. Effectively unbounded session duration on top of time-bounded infrastructure.
  • Automatic cleanup. try/finally in the workflow guarantees the VM is stopped on failure or destroy.

An effectively unbounded sandbox session is still one workflow run, so it stays on the deployment that started it. If the controller or agent code should upgrade over time, use an explicit version boundary and pass the serialized state or stream handles forward. See Versioning.

This is the pattern Open Agents uses to spawn coding agents that run "infinitely in the cloud." Each agent session gets its own sandbox — full filesystem, network, and runtime access — and the durable workflow keeps the agent loop resumable across restarts, auto-hibernates when the user walks away, and reconnects instantly when they return.

Most coding-agent workloads look like this:

  • User sends a task → agent plans, reads files, runs shell commands, commits.
  • User walks away mid-run → agent keeps going, eventually goes idle waiting for input.
  • User comes back days later → same branch, same filesystem, same conversation history.

Without durable workflows you'd need a separate state store for the agent loop, a separate job queue for retries, a separate scheduler for idle cleanup, and bespoke reconnection logic. With the pattern below, all of it is one file.

Before the full session pattern, the simplest shape. Each sandbox method is an implicit step, so the event log records every command and the workflow replays from the last completed call on restart.

import { Sandbox } from "@vercel/sandbox";

export async function sandboxPipeline(input: { commands: string[] }) {
  "use workflow";

  const sandbox = await Sandbox.create({ runtime: "node22" }); 

  try {
    const results = [];
    for (const command of input.commands) {
      const result = await sandbox.runCommand({ 
        cmd: "bash",
        args: ["-c", command],
      });
      results.push({
        command,
        exitCode: result.exitCode,
        stdout: await result.stdout(),
        stderr: await result.stderr(),
      });
    }
    return { status: "completed", results };
  } finally {
    await sandbox.stop(); 
  }
}

One workflow run owns a sandbox for its whole lifetime. The workflow's loop does two jobs simultaneously:

  1. Command pipeline — await a hook, run the next user command, stream output, loop.
  2. Sandbox lifecycle — race the hook against a sleep() timer armed for whichever comes first: the idle deadline or the sandbox's refresh deadline (a safety margin before its hard cap).

When the timer wins:

  • Idlesandbox.snapshot() and wait indefinitely for the next command. No compute while asleep.
  • Near sandbox hard capsandbox.snapshot() and immediately create a new sandbox from the snapshot. The session appears continuous; the underlying VM just rotated.

The only way out is an explicit /destroy command.

  1. One workflow = one session. The workflow owns a sandbox for its entire lifetime. The runId is the only state the client has to remember.
  2. Hook created once. commandHook.create({ token: workflowRunId }) outside the loop. Creating it twice with the same token throws HookConflictError.
  3. Two timer branches. The active-state race wakes on the earlier of idleDeadline and refreshDeadline. The hibernated state awaits the hook alone — no timer, no compute.
  4. Proactive refresh. refreshDeadline = sandboxExpiresAt - REFRESH_SAFETY_MS. Hitting this triggers a snapshot + immediate new sandbox from that snapshot, rolling over the hard cap without user intervention.
  5. sandbox.snapshot() stops the VM. It's documented as part of the snapshot process — don't call stop() separately.
  6. Resume = new sandbox. Sandbox.create({ source: { type: "snapshot", snapshotId } }) creates a fresh VM from the snapshot. The new sandbox has a different sandboxId; filesystem, installed packages, and git history are preserved.
  7. Reconnect by runId. getRun(runId).getReadable({ startIndex: 0 }) replays the durable event log to a returning client, who rebuilds UI state from the replay.
  8. Exit only on /destroy. The workflow loop has no hard deadline of its own. Individual sandboxes time out; the session doesn't.

sandbox.stop() is terminal

A stopped sandbox cannot be restarted — you have to create a new one. Hibernation is only possible via snapshot() + new-sandbox-from-snapshot. Don't try to "pause" an active sandbox with stop() and resume later.

snapshot() already stops the VM

Calling stop() after snapshot() either errors or is a no-op depending on timing. Snapshot takes care of it.

New sandboxId after resume and refresh

Both resuming (idle → command) and refreshing (near-hard-cap rotation) create a new sandbox with a new sandboxId. Emit it on the subsequent status: "active" event and have the UI read from there, not from the initial created event.

Keep the refresh margin generous

snapshot() + Sandbox.create({ source }) takes real time (typically tens of seconds). If REFRESH_SAFETY_MS is too small, the old sandbox hits its hard cap mid-snapshot. Leave at least 60–90 seconds; 5 minutes is comfortable.

Don't call writable.close() inside a workflow function

Stream closure must happen inside a "use step" function. Calling writable.close() directly in the workflow body throws Not supported in workflow functions. The runtime closes the underlying writable when the workflow returns.

Handle stale runId gracefully

Clients can hold runIds from long-gone workflow runs (localStorage, back button, server restart). Gate the reconnect path on run.exists and fall through to starting fresh. On hook.resume, catch not found / expired and return 410 so the client clears its state.

Keep the hook outside the loop

Each iteration's hook.then(...) attaches a listener to the same hook instance. Creating a new hook per iteration with the same token throws HookConflictError. One hook, one token (workflowRunId), reused every iteration.