n8n-io/n8n · error
Sandbox "${this.name}" has been destroyed
Error message
Sandbox "${this.name}" has been destroyed What it means
BaseSandbox.ensureRunning() gates every command/process operation. When `status === 'destroyed'`, the sandbox has been fully torn down and cannot be reused, so it throws immediately rather than attempt a start that would fail confusingly deep inside _start(). This is the terminal-state branch of the sandbox state machine.
Source
Thrown at packages/@n8n/agents/src/workspace/sandbox/base-sandbox.ts:125
/**
* Drop the cached "running" state so the next `ensureRunning()`/`_start()`
* re-runs the provider's `start()`. Used to recover when the remote sandbox
* was stopped/deleted out from under us (the in-memory status is stale).
* No-op once destroyed/destroying — those are terminal.
*
* Intentionally does NOT touch `startPromise`: clearing it would let a
* concurrent caller bypass the single-flight start dedupe in `_start()` and
* launch a second start. A start that is genuinely in flight will be awaited
* (and deduped) by the next `_start()`.
*/
protected markNeedsStart(): void {
if (this.status === 'destroyed' || this.status === 'destroying') return;
this.status = 'pending';
}
async ensureRunning(options?: AbortableOptions): Promise<void> {
if (this.status === 'destroyed') {
throw new Error(`Sandbox "${this.name}" has been destroyed`);
}
if (this.status === 'destroying') {
if (this.destroyPromise) await this.destroyPromise.catch(() => {});
throw new Error(`Sandbox "${this.name}" has been destroyed`);
}
if (this.status === 'stopping') {
if (this.stopPromise) await this.stopPromise.catch(() => {});
}
if (this.status !== 'running') {
await raceWithAbort(async () => await this._start(), options?.abortSignal);
}
if (this.status !== 'running') {
throw new Error(`Sandbox "${this.name}" failed to start (status: ${this.status})`);
}
}
async executeCommand(
command: string,View on GitHub (pinned to 5ac6606e81)
Solutions
- Create a fresh sandbox/workspace — a destroyed sandbox is not recoverable.
- Drop all references after destroy(); never reuse a sandbox object across workspace lifecycles.
- Order teardown so it only runs after all in-flight commands have settled.
Defensive patterns
Strategy: validation
Validate before calling
function isUsable(sandbox: { status: string }): boolean {
return sandbox.status !== 'destroyed' && sandbox.status !== 'destroying';
} Try / catch
try {
await sandbox.ensureRunning();
} catch (e) {
if (e instanceof Error && /has been destroyed/.test(e.message)) {
// create a new sandbox/workspace
} else throw e;
} Prevention
- Never hold sandbox references across workspace lifecycles.
- Await all in-flight commands before calling destroy().
- Treat any 'destroyed' error as terminal — recreate, don't retry on the same object.
When it happens
Trigger: Calling ensureRunning(), executeCommand(), or processes.spawn() on a sandbox instance after destroy() has completed — e.g. a reference held in a long-lived variable, a retry loop that didn't recreate the workspace, or a late timeout handler firing after cleanup.
Common situations: Agent code cached a sandbox reference across workspace lifecycles; a finally-block ran destroy() before an in-flight op resolved; an external timeout triggered cleanup while queued commands were still pending.
Related errors
- Sandbox "${this.name}" failed to start (status: ${this.statu
- Filesystem "${this.id}" is not ready (status: ${this.status}
- Daytona sandbox "${this.id}" is not running
- Workspace has no sandbox
- Sandbox does not support command execution
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/04549541b5a085f1.
Report an issue: GitHub.