DocsRun commands

Run commands

Run a shell command inside a sandbox and read its result. You can also stream its output as it arrives, keep it running in the background, feed it stdin, and handle a non-zero exit.

Commands run over the sandbox's own connection, not the public REST API, so there is no REST endpoint and no curl example here. Use the TypeScript or Python SDK. Every example reads IMPELLO_API_KEY from the environment; get a key at dashboard.impello.ai/keys.

Run a command

sandbox.commands.run() starts the command, waits for it to exit and returns its stdout, stderr and exit code. Every command runs through /bin/bash -l -c, so pipes, globs, && and shell variables work as they do in a terminal.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const result = await sandbox.commands.run('echo hello && uname -a')
console.log(result.stdout)
console.log(result.exitCode) // 0

await sandbox.kill()

The result has four fields: stdout, stderr, exitCode (exit_code in Python) and an optional error message. Both streams are decoded as UTF-8 and returned as strings.

Options

TypeScriptPythonDefaultWhat it does
cwdcwdthe user's homeWorking directory for the command.
useruserthe template's userLinux user to run as — any user that exists in the template. base has user, with home /home/user, and root.
envsenvs{}Environment variables for this command. They override the ones given to the sandbox.
onStdout, onStderron_stdout, on_stderrnoneCallbacks that receive output chunks as they arrive. See Stream output.
stdinstdinfalseKeep stdin open so you can write to it. See Send input.
backgroundbackgroundfalseReturn a handle at once instead of waiting. See Background commands.
timeoutMstimeout60000 ms / 60 sHow long the SDK waits for the command. See Long-running commands.

Stream output

Pass onStdout and onStderr to receive output while the command is still running. The callbacks get each chunk as a string; the full output is still collected in the returned result.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

await sandbox.commands.run('for i in 1 2 3; do echo $i; sleep 1; done', {
  onStdout: (data) => {
    process.stdout.write(data)
  },
  onStderr: (data) => {
    process.stderr.write(data)
  },
})

await sandbox.kill()

In the Python sync SDK a background handle is also iterable. Each item is a (stdout, stderr, pty) tuple with one field set and the other two None:

handle = sandbox.commands.run("ls -la /", background=True)

for stdout, stderr, _ in handle:
    if stdout is not None:
        print(stdout, end="")

In the sync SDK the on_stdout and on_stderr arguments to run() apply to foreground commands only. For a background command, iterate the handle or pass them to handle.wait(on_stdout=..., on_stderr=...).

With AsyncSandbox, pass on_stdout and on_stderr to run() for both foreground and background commands; AsyncCommandHandle.wait() takes no callbacks.

Background commands

Set background: true and run() returns a CommandHandle immediately. Use the handle to wait for the exit, read the output so far, or kill the process.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const handle = await sandbox.commands.run('sleep 2 && echo done', {
  background: true,
})
console.log(handle.pid)
console.log(handle.exitCode) // undefined while running

const result = await handle.wait()
console.log(result.stdout) // "done\n"

await sandbox.kill()

In TypeScript the handle exposes pid, stdout, stderr, exitCode and error; exitCode is undefined until the command exits. The Python sync handle exposes pid and the methods below; the async handle adds stdout, stderr, exit_code and error, with exit_code None while running.

handle.disconnect() stops the SDK listening without killing the command. To pick it up again, call sandbox.commands.connect(pid) from any client connected to the same sandbox; the new handle's wait() returns the result when the command exits.

const handle = await sandbox.commands.run('sleep 30 && echo done', {
  background: true,
})
await handle.disconnect()

const again = await sandbox.commands.connect(handle.pid, {
  onStdout: (data) => console.log(data),
})
await again.wait()

Long-running commands

The SDK stays attached to a command for 60 seconds by default. When the limit passes it throws TimeoutError (TimeoutException in Python) from run() or wait(). That stops the SDK waiting; find the process with sandbox.commands.list(), then re-attach with sandbox.commands.connect(pid) to see whether it is still running, or stop it with sandbox.commands.kill(pid). Raise the limit with timeoutMs (timeout in Python, in seconds) to cover the longest run you expect. background: true does not change this: the handle is bounded by the same value, so pass the option there too.

const result = await sandbox.commands.run('npm install', {
  timeoutMs: 10 * 60 * 1000,
})

connect() has the same 60 second default and takes the same option.

The command timeout is separate from the sandbox timeout. A sandbox that reaches its own limit is killed with everything running inside it, unless its lifecycle pauses it instead. A wait() still pending on one of its commands then throws TimeoutError. See Timeouts.

Send input

Start the command with stdin: true, write to it with sendStdin(), and call closeStdin() to send EOF when you are done. Both methods also exist on sandbox.commands taking a pid, for a command you did not start from this handle.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

const handle = await sandbox.commands.run('cat', {
  background: true,
  stdin: true,
})
await handle.sendStdin('hello from stdin\n')
await handle.closeStdin()

const result = await handle.wait()
console.log(result.stdout) // "hello from stdin\n"

await sandbox.kill()

The data may be a string or raw bytes — a Uint8Array in TypeScript, bytes in Python. A handle for a pseudo-terminal supports neither method; use the Interactive terminal module for that instead.

List and kill processes

sandbox.commands.list() returns every running command and PTY session as a ProcessInfo with pid, cmd, args, envs, and optional cwd and tag. Because every command runs through bash, cmd is /bin/bash and your command line is the last element of args.

sandbox.commands.kill(pid) sends SIGKILL. It returns true when the signal was sent and false when no process had that pid. handle.kill() does the same for the handle's own process.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

await sandbox.commands.run('sleep 600', { background: true })

for (const proc of await sandbox.commands.list()) {
  console.log(proc.pid, proc.args.at(-1))
  if (proc.args.at(-1) === 'sleep 600') {
    console.log(await sandbox.commands.kill(proc.pid)) // true
  }
}

await sandbox.kill()

Non-zero exit codes

A command that exits with anything other than 0 makes run() and wait() throw CommandExitError (CommandExitException in Python). The error carries the same exitCode, stdout and stderr fields as a result, so you can read what went wrong.

import { CommandExitError, Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')

try {
  await sandbox.commands.run('ls /does-not-exist')
} catch (err) {
  if (err instanceof CommandExitError) {
    console.log(err.exitCode) // 2
    console.log(err.stderr)
  } else {
    throw err
  }
}

await sandbox.kill()

In Python, str(e) reads Command exited with code 2 and error: followed by stderr. The other classes a command can raise are listed on the Errors page.

Next: Interactive terminal.