Scenario 1 of 3
Create a sandbox.
A sandbox is an isolated Linux microVM. From an Inngest function it is four steps: create it, wait for it to boot, run a command, destroy it. Each one lands in the trace with its inputs, outputs and timing.
| Status | Run ID | Trigger | App | Queued at | Started at | Ended at | Duration |
|---|---|---|---|---|---|---|---|
| queued | - | playground/create.run | sandbox-playground | - | - | - | - |
- Run ID
- -
- App
- sandbox-playground
- Function
- create-sandbox
- Duration
- -
- Queued at
- -
- Started at
- -
- Ended at
- -
Trace
Press Run to watch the sample trace unfold.
- ○create-sandboxCreate the sandbox (2 vCPU, 2 GB)
- ○wait-runningWait until RUNNING
- ○run-commandRun: node --version
- ○destroy-sandboxDestroy the sandbox
Command outputstdout / stderr
$ node --version
Run a command to see its output.The real function this demo illustrates · 28 lines
import { Inngest } from "inngest";
import { sandboxMiddleware } from "inngest/experimental";
const inngest = new Inngest({ id: "my-app", middleware: [sandboxMiddleware()] });
export const helloSandbox = inngest.createFunction(
{ id: "create-sandbox", triggers: [{ event: "playground/create.run" }] },
async ({ event, step }) => {
// A sandbox is a step. Inngest records it in the trace and memoizes the result.
const created = await step.sandbox.create("create-sandbox", {
name: `hello-${event.data.sessionId}`,
vcpu: 2,
memoryMb: 2048,
runningTimeout: false, // return as soon as the VM is allocated
});
// Wait for the guest to boot. Also a step.
const sandbox = await created.waitUntilRunning("wait-running", { timeout: "60s" });
// Run one command and capture stdout, stderr and the exit code.
const result = await sandbox.commands.run("run-command", "node --version", { timeout: "30s" });
// Destroy it. Files, processes and retained output go with it.
await sandbox.destroy("destroy-sandbox");
return { sandboxId: sandbox.id, stdout: result.stdout, exitCode: result.exitCode };
},
);The same job without durable steps: every call, retry, poll and cleanup written by hand · 39 lines
const API = "https://api.inngest.com";
const headers = { authorization: `Bearer ${process.env.INNGEST_SIGNING_KEY}`, "content-type": "application/json" };
export async function helloSandbox(sessionId: string) {
// 1. Create. A timeout here leaves you unsure whether a VM exists.
const created = await fetch(`${API}/v2/sandboxes`, {
method: "POST",
headers,
body: JSON.stringify({ name: `hello-${sessionId}`, vcpu: 2, memoryMb: 2048 }),
});
if (!created.ok) throw new Error(`create failed: ${created.status}`);
const sandboxId = (await created.json()).data.id;
try {
// 2. Poll until RUNNING.
for (let i = 0; ; i++) {
const res = await fetch(`${API}/v2/sandboxes/${sandboxId}`, { headers });
const status = (await res.json()).data.status;
if (status === "RUNNING") break;
if (status === "FAILED" || i > 60) throw new Error(`sandbox did not start: ${status}`);
await new Promise((r) => setTimeout(r, 1000));
}
// 3. Run the command. Output comes back base64 encoded.
const exec = await fetch(`${API}/v2/sandboxes/${sandboxId}/exec`, {
method: "POST",
headers,
body: JSON.stringify({ command: ["/bin/sh", "-c", "node --version"], timeout: "30s" }),
});
if (!exec.ok) throw new Error(`exec failed: ${exec.status}`); // may have run anyway
const out = (await exec.json()).data;
return { sandboxId, stdout: atob(out.stdout), exitCode: out.exitCode };
} finally {
// 4. Destroy. Skipped if the process died before reaching here.
await fetch(`${API}/v2/sandboxes/${sandboxId}`, { method: "DELETE", headers }).catch(() => {});
}
}
// No trace of any of this unless you add tracing yourself.