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

  1. Create a fresh sandbox/workspace — a destroyed sandbox is not recoverable.
  2. Drop all references after destroy(); never reuse a sandbox object across workspace lifecycles.
  3. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/04549541b5a085f1. Report an issue: GitHub.