Persistent Sandboxes
Persistent sandboxes let your agent maintain state across multiple user messages. Instead of creating and destroying a fresh sandbox on every invocation, the sandbox is paused after each execution and resumed on the next — preserving the filesystem, installed packages, cloned repos, and any running processes.
When to Use Persistent Sandboxes
Use persistent sandboxes when your agent needs to:
- Maintain filesystem state — a coding agent that clones a repo and makes incremental changes
- Preserve authentication — CLI tools that store credentials on disk (e.g.,
gh auth,claude auth) - Accumulate context — an agent that builds up a working environment over multiple interactions
- Run long-lived processes — background services that the agent interacts with across messages
Don't use persistent sandboxes for simple request/response tools (API calls, data lookups, one-shot transformations). The default ephemeral sandbox is faster and cheaper for these cases.
How It Works
Lifecycle
First message: CREATE sandbox → EXECUTE handler → PAUSE sandbox
↓
Second message: RESUME sandbox → EXECUTE handler → PAUSE sandbox
↓
Third message: RESUME sandbox → EXECUTE handler → PAUSE sandbox
↓
End session: RESUME sandbox → EXECUTE handler → KILL sandbox
(handler sets requestKill: true)What's Preserved
| Preserved across pause/resume | Not preserved after kill |
|---|---|
| Filesystem (files, cloned repos) | Everything |
| Installed packages | |
| Auth credentials on disk | |
| Environment variables | |
| Session metadata (in DB) | Session metadata is preserved |
Billing
- While running: Standard E2B compute rates apply
- While paused: Free — no compute charges
- Auto-cleanup: Sessions paused longer than
maxSessionAgeare automatically killed
Setup
1. Add runtime.persistent to Your Manifest
manifest:
trigger: auto
runtime:
persistent: true
timeoutMs: 300000 # 5 min per invocation
maxSessionAge: 86400000 # auto-kill after 24 hours
tool:
name: my_agent
description: An agent with persistent state
parameters:
type: object
properties:
prompt:
type: string
description: What to do
required: [prompt]2. Use Session Metadata in Your Handler
Session metadata lets your handler track state across invocations. It's stored in the database and available even after a sandbox is killed.
async function handler(input, context) {
const meta = await context.getSessionMetadata();
if (!meta.initialized) {
// First invocation — set up the environment
// Clone repo, install dependencies, etc.
await context.updateSessionMetadata({
initialized: true,
setupAt: Date.now(),
});
}
// Do work...
await context.updateSessionMetadata({
lastRun: Date.now(),
totalRuns: (meta.totalRuns || 0) + 1,
});
return { result: 'Done' };
}
module.exports = { handler };3. Implement Session End
Let users end their session by setting the requestKill metadata flag:
if (input.action === 'end_session') {
await context.updateSessionMetadata({ requestKill: true });
return { result: 'Session ended.' };
}After execution completes, the platform will kill the sandbox instead of pausing it.
4. (Optional) Create a Custom Sandbox Template
If your agent needs specific CLI tools or packages pre-installed, create a sandbox template:
- Go to Developer Portal → Templates tab
- Enter a setup script:
npm install -g @anthropic-ai/claude-code
apt-get install -y git curl jq
mkdir -p /home/user/workspace- Click Create & Build Template (builds in 1-3 minutes)
- Assign the template to your agent in Settings → Sandbox Template
Complete Example
Here's a minimal persistent agent that maintains a counter across messages:
---
name: counter-agent
displayName: Persistent Counter
tier: tool
version: 1.0.0
manifest:
trigger: auto
runtime:
persistent: true
timeoutMs: 60000
tool:
name: counter
description: A counter that persists across messages
parameters:
type: object
properties:
action:
type: string
enum: [increment, get, reset, end]
description: What to do with the counter
required: [action]
---
async function handler(input, context) {
const { action } = input;
const meta = await context.getSessionMetadata();
if (action === 'end') {
await context.updateSessionMetadata({ requestKill: true });
return { result: 'Counter session ended.', finalCount: meta.count || 0 };
}
if (action === 'reset') {
await context.updateSessionMetadata({ count: 0 });
return { result: 'Counter reset to 0.' };
}
if (action === 'increment') {
const newCount = (meta.count || 0) + 1;
await context.updateSessionMetadata({ count: newCount });
return { result: `Counter incremented to ${newCount}.`, count: newCount };
}
// action === 'get'
return { count: meta.count || 0 };
}
module.exports = { handler };Context Preservation
When a session is killed (either by the user or by TTL), the sandbox filesystem is lost. However, session metadata persists in the database. Use this to restore context in new sessions:
const meta = await context.getSessionMetadata();
if (meta.previousSessionContext) {
// Use the saved context to inform the new session
console.log('Restoring context:', meta.previousSessionContext);
}Before ending a session, save a summary:
if (action === 'end_session') {
await context.updateSessionMetadata({
requestKill: true,
previousSessionContext: `Worked on repo ${meta.repoUrl}. Last task: ${meta.lastTask}.`,
});
return { result: 'Session ended.' };
}API Reference
| API | Description |
|---|---|
context.getSessionMetadata() | Read session metadata |
context.updateSessionMetadata(data) | Merge data into session metadata |
manifest.runtime.persistent | Enable persistent sandboxes |
manifest.runtime.timeoutMs | Per-invocation timeout |
manifest.runtime.maxSessionAge | Auto-cleanup TTL |