WorkspaceSandbox
Added in: @mastra/core@1.1.0
The WorkspaceSandbox interface defines how workspaces execute commands and manage background processes.
PropertiesDirect link to Properties
processes?:
computer?:
MethodsDirect 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' 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:
- Acquisition primitives: implement protected
find()(side-effect-free lookup by logical id, returning a provider-native handle orundefined),connect(handle)(wake/resume/adopt), andcreate()(provision fresh) without overridingstart(). The base orchestrates find → connect →{ outcome: 'connected' }, else create →{ outcome: 'created' }. The outcome is derived structurally from which branch ran. Used byE2BSandbox,DaytonaSandbox, andLocalSandbox. start()override returningSandboxStartResult: for providers whose API is a fused get-or-create where decomposition would add round-trips (PlatformSandbox,RailwaySandbox).start()override returningvoid: 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 markederror, so a caller never observes a running sandbox whose setup hook failed. Nothing is latched, and the nextstart()(including the one triggered by the next lazy command) retries the hook.onStopandonDestroyremain non-fatal observers, because teardown proceeds best-effort. outcomedistinguishes 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).undefinedmeans 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
runningbefore the hook executes (the hook runs commands through the sandbox's own command path), so commands issued concurrently throughensureRunning()can interleave with it. Callers awaiting the originalstart()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
previousfirst when your hook needs the workspace it prepares, or last when yours is the one preparing it. - Ignoring
previousreplaces 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:
workingDiron Docker and Apple container,workdiron Modal. When both the alias andworkingDirectoryare set,workingDirectorywins.
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:
args?:
options?:
timeout?:
cwd?:
workingDirectory, then the provider default.env?:
onStdout?:
onStderr?:
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:
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?:
instructions function if one was provided in the constructor.Returns: string
Computer capabilityDirect 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():
leftClick(x, y):
rightClick(x, y):
doubleClick(x, y):
moveMouse(x, y):
drag(from, to):
from, drag to to, and release.scroll(direction, amount):
type(text):
press(key):
getScreenSize():
getCursorPosition():
streamUrl()?:
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.