Skip to main content

WorkspaceSandbox

Added in: @mastra/core@1.1.0

The WorkspaceSandbox interface defines how workspaces execute commands and manage background processes.

Properties
Direct link to Properties

processes?:

SandboxProcessManager
Background process manager. If not implemented, process management tools won't be available. See SandboxProcessManager reference.

computer?:

SandboxComputer
Computer-use (desktop) capability. If not implemented, computer tools won't be available. See Computer capability.

Methods
Direct link to Methods

start()
Direct link to start

Starts the sandbox and is called automatically by workspace.init() or the first executeCommand() call.

const result = await sandbox.start()
// { outcome: 'created' } — a fresh VM was created
// { outcome: 'connected' } — reconnected to an existing VM
// undefined — the provider does not report

Returns: void | Promise<SandboxStartResult | void>

A sandbox constructed with a known id resolves that id on start(): reconnect or resume the sandbox if it exists, create it if not (get-or-create). Providers that support this report a SandboxStartResult:

outcome:

'created' | 'connected'
'created' when a brand-new sandbox VM (or working directory) was provisioned; 'connected' when the call reconnected to or resumed an existing one.

Providers that predate the contract return void, which the base class treats as unknown.

Providers implement the start lifecycle using one of the following approaches, listed in order of preference. The base class handles concurrent start calls and status management, as well as the onStart hook and mount processing:

  1. Acquisition primitives: implement protected find() (side-effect-free lookup by logical id, returning a provider-native handle or undefined), connect(handle) (wake/resume/adopt), and create() (provision fresh) without overriding start(). The base orchestrates find → connect → { outcome: 'connected' }, else create → { outcome: 'created' }. The outcome is derived structurally from which branch ran. Used by E2BSandbox, DaytonaSandbox, and LocalSandbox.
  2. start() override returning SandboxStartResult: for providers whose API is a fused get-or-create where decomposition would add round-trips (PlatformSandbox, RailwaySandbox).
  3. start() override returning void: legacy providers, where the outcome is unknown.

Concurrent start() calls on one instance coalesce onto a single in-flight attempt, and joined callers share that attempt's result (all observe outcome: 'created' when the shared attempt created the VM). The in-flight slot is cleared when the attempt settles, so a failed start can be retried. For a sandbox already in the running state, start() resolves { outcome: 'connected' } without re-invoking the provider.

The result is also forwarded to the onStart lifecycle hook as { sandbox, outcome }.

onStart (constructor option)
Direct link to onstart-constructor-option

onStart runs inside the start lifecycle, after the sandbox reaches running status and before pending mounts are processed. It fires on every start regardless of trigger, whether an explicit call, a lazy ensureRunning() from a command, or a revival after the provider replaced the VM. Use this hook for once-per-VM setup: check outcome to decide whether to run setup or check that it already completed.

new E2BSandbox({
id: sessionId,
onStart: async ({ sandbox, outcome }) => {
if (outcome === 'created') {
// Fresh VM: run the full setup.
await runSetup(sandbox)
return
}
// Reconnected: probe, and self-heal if setup never completed.
const probe = await sandbox.executeCommand('test -d ~/repo/.git')
if (probe.exitCode !== 0) await runSetup(sandbox)
},
})

Semantics:

  • A thrown error is fatal: start() rejects with the hook's error and the sandbox is marked error, so a caller never observes a running sandbox whose setup hook failed. Nothing is latched, and the next start() (including the one triggered by the next lazy command) retries the hook. onStop and onDestroy remain non-fatal observers, because teardown proceeds best-effort.
  • outcome distinguishes the branches: 'created' means this start provisioned a fresh VM (run setup), 'connected' means it reconnected or resumed (setup normally already ran, so probe when the hook must self-heal a crash between create and setup-complete). undefined means the provider doesn't report.
  • Keep setup work idempotent. A hook re-runs whenever a probe decides it should, and a checkpoint-recovered fresh VM reports outcome: 'created'.
  • The sandbox status flips to running before the hook executes (the hook runs commands through the sandbox's own command path), so commands issued concurrently through ensureRunning() can interleave with it. Callers awaiting the original start() always observe a sandbox whose hook finished.
  • The hook runs before pending filesystem mounts are processed, so it can't rely on mounted paths.

setOnStart(update)
Direct link to setonstartupdate

Attaches a start hook after construction, for runtimes that receive a sandbox they didn't build. Without it, every host that constructs a sandbox has to accept a hook and pass it to the provider constructor, and a host that forgets leaves setup unrun with no error.

The updater receives the hook currently installed, either the onStart constructor option or one a previous call left behind, and returns the hook to install. Composing this way means a caller never discards a hook it didn't know about:

sandbox.setOnStart?.(previous => async args => {
await previous?.(args) // whatever prepared the sandbox runs first
await mySetup(args)
})

Semantics:

  • Errors stay fatal, exactly as with the constructor option. Because each hook awaits the next, a throw stops the ones sequenced after it.
  • The caller chooses the order. Await previous first when your hook needs the workspace it prepares, or last when yours is the one preparing it.
  • Ignoring previous replaces the installed hook. That's supported, and it's how a caller takes over setup a runtime installed.
  • Each call wraps the current hook, so attach once per sandbox instance. Attaching on a path that runs per request stacks duplicate work on every start.
  • Only starts that begin after the call see the new hook.

workingDirectory (constructor option)
Direct link to workingdirectory-constructor-option

Sets the default directory for command execution and process spawns. When a command provides no per-command cwd, the sandbox runs it from this directory. A per-command cwd always wins. When neither is provided, each provider keeps its own default (E2B home, Docker /workspace, Vercel serverless /tmp, and so on).

const sandbox = new E2BSandbox({ workingDirectory: '/home/user/my-repo' })
await sandbox.executeCommand('pwd') // /home/user/my-repo
await sandbox.executeCommand('pwd', [], { cwd: '/tmp' }) // /tmp

The effective value is readable through the sandbox.workingDirectory getter. Providers that compute their value, such as Daytona's automatic probe or Docker's /workspace default, report the effective directory through the same getter.

Semantics:

  • The value passes to the provider as-is, and the sandbox doesn't create the directory. Absolute paths are recommended: ~-prefixed paths only work where the provider documents expansion.
  • Some providers keep earlier option names as deprecated aliases feeding the same field: workingDir on Docker and Apple container, workdir on Modal. When both the alias and workingDirectory are set, workingDirectory wins.

stop()
Direct link to stop

Stop the sandbox.

await sandbox.stop?.()

destroy()
Direct link to destroy

Clean up sandbox resources. Called by workspace.destroy().

await sandbox.destroy()

snapshot()
Direct link to snapshot

Persists the sandbox's current state when the provider supports snapshots. Other providers resolve without doing work.

await sandbox.snapshot()

Returns: Promise<void>

executeCommand(command, args?, options?)
Direct link to executecommandcommand-args-options

Execute a shell command.

const result = await sandbox.executeCommand('ls', ['-la', '/docs'])
const result = await sandbox.executeCommand('npm', ['install', 'lodash'])

Parameters:

command:

string
Command to execute

args?:

string[]
Command arguments

options?:

Options
Configuration options.
Options

timeout?:

number
Execution timeout in milliseconds

cwd?:

string
Working directory for this command. Defaults to the sandbox's configured workingDirectory, then the provider default.

env?:

Record<string, string>
Additional environment variables for this command. These take precedence over the sandbox environment for this command execution only.

onStdout?:

(data: string) => void
Callback for stdout streaming

onStderr?:

(data: string) => void
Callback for stderr streaming

setEnv(update)
Direct link to setenvupdate

Update the sandbox's runtime environment. These values are merged into every command the sandbox runs, including executeCommand() and processes.spawn(), so credentials installed or rotated after the sandbox was created reach every subsequent command. Optional, so check for support or use optional chaining.

sandbox.setEnv?.(env => ({ ...env, GH_TOKEN: token }))

The updater receives a copy of the current environment and returns the replacement, so a single call can set, unset, or batch-update variables. Removing a key removes it from the sandbox's runtime environment only, so values the provider supplies on its own still apply. Seed the initial values with the env constructor option.

The runtime environment applies to commands executed through the sandbox rather than to the VM's own environment, so it's never written into the VM and survives provider pause and resume. When a command runs, provider defaults are applied first, the sandbox's runtime environment overrides them, and the per-call env option on executeCommand() wins over both.

Parameters:

update:

(env: Record<string, string | undefined>) => Record<string, string | undefined>
Receives a copy of the sandbox's current runtime environment and returns the replacement.

Returns: void

getEnv()
Direct link to getenv

Returns a copy of the sandbox's current runtime environment; because mutations to the returned object don't change the sandbox, use setEnv() for updates.

const token = sandbox.getEnv().GH_TOKEN

Returns: Record<string, string | undefined>

getInfo()
Direct link to getinfo

Get sandbox status and resource information.

const info = await sandbox.getInfo()
// { status: 'running', resources: { memoryMB: 512, cpuPercent: 5 } }

getInstructions(opts?)
Direct link to getinstructionsopts

Returns a description of how this sandbox works. Injected into the agent's system message when the workspace is assigned to an agent.

const instructions = sandbox.getInstructions?.()
// 'Local command execution. Working directory: "/workspace".'

Parameters:

opts.requestContext?:

RequestContext
Forwarded to the instructions function if one was provided in the constructor.

Returns: string

Computer capability
Direct link to Computer capability

Sandboxes with a controllable desktop environment implement the optional SandboxComputer interface on the computer property. When present on a statically configured sandbox, the workspace tools factory registers the mastra_workspace_computer_* tools automatically. See Computer-use tools.

Coordinates are pixels from the top-left corner of the display. Providers normalize their SDK semantics (key names, scroll units) onto this surface and expose richer native APIs through their own accessors.

screenshot():

() => Promise<{ data: Uint8Array; mediaType: "image/png" }>
Capture the current display as a PNG image.

leftClick(x, y):

(x: number, y: number) => Promise<void>
Left-click at the given coordinates.

rightClick(x, y):

(x: number, y: number) => Promise<void>
Right-click at the given coordinates.

doubleClick(x, y):

(x: number, y: number) => Promise<void>
Double-click (left button) at the given coordinates.

moveMouse(x, y):

(x: number, y: number) => Promise<void>
Move the cursor without clicking.

drag(from, to):

(from: ComputerPosition, to: ComputerPosition) => Promise<void>
Press the left button at from, drag to to, and release.

scroll(direction, amount):

(direction: 'up' | 'down', amount: number) => Promise<void>
Scroll the display by the given amount of ticks.

type(text):

(text: string) => Promise<void>
Type text into the focused element.

press(key):

(key: string | string[]) => Promise<void>
Press a key or key combination. A string presses one key (for example 'Enter'); an array presses a chord (for example ['ctrl', 's']).

getScreenSize():

() => Promise<{ width: number; height: number }>
Get the display dimensions.

getCursorPosition():

() => Promise<{ x: number; y: number }>
Get the current cursor position.

streamUrl()?:

() => Promise<string | null>
Get a URL for a live view of the desktop, such as noVNC, or null when unavailable. Optional, not all providers expose a viewer.

Use the supportsComputer() type guard to check for the capability:

import { supportsComputer } from '@mastra/core/workspace'

if (supportsComputer(sandbox)) {
const { data } = await sandbox.computer.screenshot()
}

DaytonaSandbox and E2BDesktopSandbox implement this capability. See the Daytona and E2B Desktop integration pages.