Already have code running on Daytona? Moving to Tenki is a small, mechanical change. Both give you a disposable cloud sandbox with the same core loop — create → run a command → capture output → dispose — so most scripts port in a few minutes with the official @tenkicloud/sandbox SDK.
This example shows the two scripts side by side, an API map, and a run.mjs you can run against live Tenki.
Daytona (@daytonaio/sdk):
import { Daytona } from "@daytonaio/sdk";
const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY }); // or implicit DAYTONA_API_KEY
const sandbox = await daytona.create();
const r = await sandbox.process.executeCommand("echo hello from daytona"); // runs via a shell
console.log(r.result.trim(), "· exit", r.exitCode); // hello from daytona · exit 0
await daytona.delete(sandbox);Tenki (@tenkicloud/sandbox):
import { TenkiSandbox, stdoutText } from "@tenkicloud/sandbox";
const tenki = new TenkiSandbox({ authToken: process.env.TENKI_AUTH_TOKEN });
const sandbox = await tenki.createAndWait({
cpuCores: 1,
memoryMb: 1024,
workspaceId: process.env.TENKI_WORKSPACE_ID,
});
try {
// exec() runs a bare binary + args (no shell). Wrap shell syntax in `sh -c`.
const r = await sandbox.exec("sh", { args: ["-c", "echo hello from tenki"] });
console.log(stdoutText(r).trim(), "· exit", r.exitCode); // hello from tenki · exit 0
} finally {
await sandbox.close();
}| Concept | Daytona (@daytonaio/sdk) |
Tenki (@tenkicloud/sandbox) |
|---|---|---|
| Install | npm i @daytonaio/sdk |
npm i @tenkicloud/sandbox |
| Import | import { Daytona } from "@daytonaio/sdk" |
import { TenkiSandbox, stdoutText } from "@tenkicloud/sandbox" |
| Auth | new Daytona({ apiKey }) (or implicit DAYTONA_API_KEY) |
new TenkiSandbox({ authToken }) (+ workspaceId) |
| Create | await daytona.create() |
await tenki.createAndWait({ cpuCores, memoryMb, workspaceId }) |
| Run a command | await sandbox.process.executeCommand("ls -la") (shell) |
await sandbox.exec("ls", { args: ["-la"] }) (no shell — see note) |
| Run a code snippet | await sandbox.process.codeRun("print(6 * 7)") |
await sandbox.exec("python3", { args: ["-c", "print(6 * 7)"] }) |
| stdout / stderr | r.result (string) |
stdoutText(r) / stderrText(r) (decode the byte fields) |
| Exit code | r.exitCode |
r.exitCode |
| Env vars | executeCommand(cmd, cwd, env) or create({ envVars }) |
exec(cmd, { env }) or createAndWait({ env }) |
| Working dir | executeCommand(cmd, cwd) |
exec(cmd, { cwd }) |
| Write a file | await sandbox.fs.uploadFile(Buffer.from(data), path) |
await sandbox.writeFile(path, data) |
| Read a file | await sandbox.fs.downloadFile(path) (Buffer) |
await sandbox.readFile(path) |
| Sandbox ID | sandbox.id |
sandbox.id |
| Reconnect | await daytona.get(id) |
await tenki.get(id) |
| List sandboxes | await daytona.list() |
await tenki.list() |
| Dispose | await daytona.delete(sandbox) |
await sandbox.close() (or await using auto-disposes) |
This is the change that bites most ports. Daytona's process.executeCommand("echo $HOME && ls") runs your string through a shell, so pipes, globs, &&, and $VARS just work. Tenki's exec(command, { args }) runs a bare binary plus its argument array — nothing is interpreted.
// Daytona
await sandbox.process.executeCommand("echo $HOME && ls /tmp");
// Tenki — ask for a shell explicitly
await sandbox.exec("sh", { args: ["-c", "echo $HOME && ls /tmp"] });Daytona's other runner, process.codeRun("print(6 * 7)"), ships a code snippet to the sandbox's language runtime. On Tenki you invoke that interpreter directly as a bare binary — no shell needed:
// Daytona
await sandbox.process.codeRun("print(6 * 7)");
// Tenki — the interpreter is just a binary + args
await sandbox.exec("python3", { args: ["-c", "print(6 * 7)"] });npm install
export TENKI_AUTH_TOKEN=... # from `tenki login` (~/.config/tenki/config.yaml)
export TENKI_WORKSPACE_ID=...
node run.mjs # hello from tenki · exit 0verify.mjs is the CI proof: it reads your token from TENKI_AUTH_TOKEN / TENKI_API_KEY or ~/.config/tenki/config.yaml, then does the full loop against live Tenki — create → exec a shell command and a bare binary → assert the captured output → terminate — and exits non-zero on any failure.
node verify.mjs # ✓ create → exec sh + python3 → "hello-from-tenki" / 42 → dispose- Output is bytes.
ExecResult.stdout/.stderrareUint8Array; usestdoutText(r)/stderrText(r)to get strings (Daytona hands you aresultstring directly). - Auth is explicit. Daytona reads
DAYTONA_API_KEY(implicitly, or vianew Daytona({ apiKey })); Tenki takes anauthTokenon the client, plus aworkspaceIdfor placement (fromtenki login). - Disposal.
await sandbox.close()maps to Daytona'sdaytona.delete(sandbox). BecauseSessionis anAsyncDisposable, you can also writeawait using sandbox = await tenki.createAndWait(...)and let the scope terminate it for you. Either way the microVM is billed per second and self-reaps on its idle / lifetime caps. - Networking is off by default. If your code needs outbound network, pass
createAndWait({ allowOutbound: true }). - Requires Node 20+ for
await using(therun.mjshere uses explicitclose(), so Node 18+ is fine).
Built with the official @tenkicloud/sandbox SDK. Learn more at tenki.cloud.