Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.opencomputer.dev/llms.txt

Use this file to discover all available pages before exploring further.

Three primitives, each tuned for a different shape of work:
UsePrimitiveWhy
One-shot command, wait for the resultexec.run()Simplest call. Returns once the command exits. Each call is isolated — no state carries between calls.
Long-running / streaming processexec.start() / exec.background()Returns a session handle you can stream from, send stdin to, or reattach to later. Use for servers, builds, and any job you need to observe live.
Multi-step workflow, state must persistexec.shell()Opens a long-lived bash whose cwd, exported env, and shell functions persist across .run() calls — the ergonomics of a terminal tab. Reconnectable via exec.reattachShell(sessionId).
Rules of thumb:
  • Need a return value and don’t care about streaming? → run().
  • Starting something that outlives the call (a server, a log tailer, a slow build)? → start() / background().
  • Doing a sequence of commands where one step’s state (directory, env var, activated venv) affects the next? → shell().
Under the hood shell() is an exec session — its sessionId shows up in exec.list() next to anything started via start(). The difference is purely how the SDK frames commands on top: a sentinel protocol per .run() call so you can treat one long-lived bash as many discrete calls.
import { Sandbox } from "@opencomputer/sdk";

const sandbox = await Sandbox.create();
const result = await sandbox.exec.run("echo Hello, World!");
console.log(result.stdout); // "Hello, World!\n"
await sandbox.kill();

Quick Commands: exec.run()

Run a shell command and wait for the result. The command runs via sh -c, so pipes, redirects, and shell expansion work.
// Working directory and environment variables
const result = await sandbox.exec.run("npm run build", {
  cwd: "/app",
  env: { NODE_ENV: "production" },
  timeout: 120,
});

if (result.exitCode !== 0) {
  console.error("Build failed:", result.stderr);
}

Parameters

ParameterTypeDefaultDescription
commandstringShell command to run (required)
timeoutnumber60Timeout in seconds
envobjectEnvironment variables
cwdstringWorking directory

ProcessResult

FieldTypeScriptPythonType
Exit codeexitCodeexit_codenumber
Standard outputstdoutstdoutstring
Standard errorstderrstderrstring

Async Commands: exec.start() / exec.background()

Start a command as an exec session for long-running processes or streaming output. Returns an ExecSession with callbacks for stdout, stderr, and exit. exec.background() is an alias for exec.start() — same options, same return type.
const session = await sandbox.exec.start("node server.js", {
  cwd: "/app",
  env: { PORT: "3000" },
  onStdout: (data) => process.stdout.write(data),
  onStderr: (data) => process.stderr.write(data),
  onExit: (code) => console.log("Server exited:", code),
  maxRunAfterDisconnect: 300, // keep running 5min after disconnect
});

// Send input
session.sendStdin("some input\n");

// Wait for completion (or kill)
await session.kill();

ExecStartOpts

ParameterTypeScriptPythonDescription
Argumentsargs: string[]args: list[str]Command arguments
Envenv: objectenv: dictEnvironment variables
Working dircwd: stringcwd: strWorking directory
Timeouttimeout: numbertimeout: intTimeout in seconds
Keep-alivemaxRunAfterDisconnectmax_run_after_disconnectSeconds to keep running after disconnect
StdoutonStdouton_stdoutCallback receiving raw bytes
StderronStderron_stderrCallback receiving raw bytes
ExitonExiton_exitCallback receiving the exit code

ExecSession

MemberTypeScriptPythonDescription
Session IDsessionIdsession_idSession identifier
Donedone: Promise<number>done: asyncio.Future[int]Resolves with exit code
Send stdinsendStdin(data)send_stdin(data)Write to the process
Killkill(signal?)kill(signal=9)Kill the process
Detachclose()close()Close the WebSocket (process keeps running)

Stateful Shell: exec.shell()

Open a long-lived bash session whose state (cwd, exported env vars, shell functions) persists across .run() calls. Foreground-only: concurrent .run() rejects with ShellBusyError.
const sh = await sandbox.exec.shell({ cwd: "/app" });

await sh.run("npm install");
await sh.run("export NODE_ENV=test");
const r = await sh.run("npm test", {
  onStdout: (b) => process.stdout.write(b),
});
console.log(r.exitCode);

await sh.close();

Reattaching to an open shell

The shell is just an exec session, so its sessionId is stable across SDK invocations. Keep the id and revisit the same shell later — cwd, env, and shell functions are still there because the bash process never went anywhere.
const sh = await sandbox.exec.shell();
await sh.run("cd /srv && export DEPLOY=canary");
const id = sh.sessionId; // persist this somewhere

// ...later, possibly a different process...

const sh2 = await sandbox.exec.reattachShell(id);
const r = await sh2.run("pwd; echo $DEPLOY");
// → "/srv\ncanary\n"
Reattach assumes the shell is idle (no in-flight .run() from another client). If two clients try to drive the same shell concurrently, their output will interleave — coordinate at the application level.

Terminal-tab semantics

Running exit (or exit N) inside sh.run() closes the shell — same as closing a terminal tab. The pending .run() rejects with ShellClosedError and any subsequent .run() on the same Shell also rejects. Start a fresh shell() if you need another one.

Streaming output

Pass onStdout/onStderr to sh.run() and they fire as bytes arrive, before the promise resolves. Two caveats worth knowing:
  • bash builtin output is block-buffered. echo, printf, and other builtins go through glibc stdio, which only flushes when the buffer fills (~4 KB) or bash exits. If you want live output from a simple loop, use an external binary (/bin/echo) or a tool that flushes explicitly (python -u, stdbuf -oL <cmd>).
  • Upstream hops may coalesce small frames. Chunks travel bash → agent → worker (gRPC) → WS → CDN → client. Any of those hops is allowed to combine small frames under light load, so a short command may arrive as one chunk even if its output was produced incrementally. Real workloads (builds, installs, servers) produce enough output that streaming is visible in practice.
See sdks/typescript/examples/stream-demo.ts / sdks/python/examples/stream_demo.py for a ~6-second apt-install simulation that prints per-chunk arrival timestamps — a good way to eyeball streaming behavior against your deployment.
Per-call cwd, env, and timeout are intentionally not supported in v1. Use inline shell syntax (cd /x && cmd, FOO=bar cmd) — the shell state carries across calls. Use exec.background() for fire-and-forget processes.

Managing Sessions

List Sessions

const sessions = await sandbox.exec.list();
for (const s of sessions) {
  console.log(s.sessionID, s.running ? "running" : `exited (${s.exitCode})`);
  console.log(`  command: ${s.command}, clients: ${s.attachedClients}`);
}

Attach to Running Session

Reconnect to a running exec session to resume streaming output:
const session = await sandbox.exec.attach(sessionId, {
  onStdout: (data) => process.stdout.write(data),
  onStderr: (data) => process.stderr.write(data),
  onExit: (code) => console.log("Exited:", code),
  onScrollbackEnd: () => console.log("--- live output ---"),
});
On attach, the server replays the scrollback buffer (historical output), sends a scrollback-end marker, then streams live output.

Kill a Session

await sandbox.exec.kill(sessionId);
await sandbox.exec.kill(sessionId, 15); // SIGTERM
sandbox.commands is a deprecated alias for sandbox.exec. Use sandbox.exec in all new code.
CLI equivalent: oc exec. Full reference: TypeScript SDK · Python SDK · HTTP API.