Processes and previews
Start long-running processes like dev servers, wait until they are ready, read their logs with cursors, reattach by name, and get preview URLs.
run waits for a command to exit. A dev server never exits, so start it with box.start instead. You get a handle that knows when the server is ready, keeps its logs, and can be found again by name from another request or another server.
Start a process
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { vercel } from "@opencoredev/sandbox-sdk/vercel";
await using box = await createSandbox(vercel(), { ports: [3000] });
const dev = await box.start("npm run dev -- --port 3000", {
name: "dev",
cwd: "app",
ready: { http: { port: 3000, path: "/" } },
readyTimeoutMs: 90_000,
});
console.log(dev.status, dev.url);
start resolves when the readiness check passes. Because ready names a port, dev.url is a preview URL that the SDK has already checked from outside the sandbox. Some providers need every port declared in ports at creation, and the others ignore it. The ports row of the capability matrix says which, and how many ports each one takes.
name is optional and unique within a sandbox. It is how you find the process later. Without one, the SDK derives a stable name from the command, so starting the same command again reaches the same process, and dev.name tells you what it picked.
Readiness checks
ready |
Passes when |
|---|---|
{ port: 3000 } |
Something accepts TCP connections on port 3000 |
{ http: { port: 3000, path: "/health" } } |
An HTTP request to that path gets a response |
{ log: /Local:/ } |
A line of output matches the regular expression |
Without ready, start returns as soon as the process is running.
Two things can go wrong:
- The process exits first.
startthrowsSandboxErrorwith codeprocess_exited. The error includes the exit code and the last 50 log lines, so you can show why it crashed. - The check times out. The process keeps running. The handle reports
ready: false, and the error carries the end of the log. RaisereadyTimeoutMsfor slow first builds.
Read logs
A process writes its output to a log file inside the sandbox. Read it in pages with a cursor:
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";
await using box = await createSandbox(local(), { key: "project:42" });
const dev = box.process("dev");
const page = await dev.logs({ maxBytes: 16_384 });
console.log(page.text);
const next = await dev.logs({ after: page.nextCursor });
console.log(next.text);
Each page returns text, nextCursor, and truncated. Cursors are plain strings. Store one in your database and a different server process can continue reading where the last one stopped.
To follow output live, iterate lines:
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";
await using box = await createSandbox(local(), { key: "project:42" });
for await (const line of box.process("dev").lines()) {
console.log(line.stream, line.text);
if (line.text.includes("compiled")) break;
}
Every line carries its own cursor, so you can resume from the exact line you stopped at with lines({ after: line.cursor }).
Reattach by name
Process records live inside the sandbox, not in your server’s memory. After a deploy or a restart, connect to the sandbox and call box.process(name):
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";
await using box = await createSandbox(local(), { key: "project:42" });
const all = await box.processes();
for (const p of all) console.log(p.name, p.status, p.ports);
const dev = box.process("dev");
await dev.restart();
processes() returns name, command, status, pid, ports, startedAt, and exitCode for each process. status is one of "starting", "running", "exited", or "failed".
Stop and wait
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { local } from "@opencoredev/sandbox-sdk/local";
await using box = await createSandbox(local());
const job = await box.start("node build.mjs", { name: "build" });
const exit = await job.wait();
console.log(exit.exitCode, exit.signal);
await box.process("dev").stop();
wait() resolves when the process exits. stop() sends SIGTERM, then SIGKILL after a grace period. Calling it on a process that already stopped does nothing.
Previews
box.preview(port) returns a URL for a port, whether or not you started the server with start. On Vercel and Local the port must be in ports; elsewhere any port works.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { daytona } from "@opencoredev/sandbox-sdk/daytona";
await using box = await createSandbox(daytona(), { key: "project:42" });
const preview = await box.preview(3000, { path: "/", waitMs: 10_000 });
const response = await fetch(preview.url, { headers: preview.headers });
console.log(response.status, preview.expiresAt);
With waitMs, the SDK requests the URL from outside the sandbox and returns only once it answers. Some providers need more than a URL: Daytona requires a token header, which arrives in headers. A URL that stops working after a while comes with expiresAt, in epoch milliseconds. Pass headers along whenever they are present.