Node.js
Execute JavaScript and TypeScript, install npm dependencies, and manage execution lifecycles in agentOS.
agentOS runs JavaScript and TypeScript on native V8 inside the VM, backed by a
real Node.js surface: node:fs, node:child_process, sockets, and npm.
Running a multi-step operation as code instead of chaining one host call per step is called Code Mode. It has a few advantages over driving Bash:
- Fewer tokens: Ten chained operations cost one round trip, not ten.
- Type checking: Validate generated TypeScript before you run it.
- Real data processing:
mapandfilterinstead ofjqandawk. - Parallelism:
Promise.allinstead of shell job control.
Evaluate an expression
evaluate() returns a JSON-serializable value.
import { createAgentOsClient } from "@rivet-dev/agentos";
const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" });
const vm = client.agentOS.getOrCreate(["my-vm"]);
const sum = await vm.v1.javascript.evaluate({ expression: "1 + 2" });
console.log(sum.value); // 3
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create();
try {
const sum = await runtime.javascript.evaluate<number>("1 + 2");
console.log(sum.outcome === "succeeded" ? sum.value : sum.error); // 3
// More than one statement goes in a function, so the code has somewhere to
// return from.
const report = await runtime.javascript.evaluate<{ total: number }>(`
(() => {
const values = [1, 2, 3];
return { total: values.reduce((a, b) => a + b, 0) };
})()
`);
console.log(report.outcome === "succeeded" ? report.value : report.error); // { total: 6 }
} finally {
await runtime.dispose();
}
Returning undefined, a function, a symbol, or a circular value fails with
evaluation_serialization_failed rather than silently losing the value.
Execute code
execute() runs source and captures its output instead of returning a value.
import { createAgentOsClient } from "@rivet-dev/agentos";
const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" });
const vm = client.agentOS.getOrCreate(["my-vm"]);
const result = await vm.v1.javascript.execute({
source: `console.log("hello from agentOS")`,
});
console.log(result);
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create();
try {
const result = await runtime.javascript.execute(
`console.log("hello from agentOS")`,
{ output: { capture: "all" } },
);
console.log(result.outcome === "succeeded" ? result.stdout : result.error); // "hello from agentOS\n"
} finally {
await runtime.dispose();
}
Executions are ephemeral, so capture stdio only when you want it: "stderr" for
diagnostics, "all" for both streams. onStdout/onStderr stream live and work
independently of capture.
Pass data into code
In the library, inputs hands host values to the guest as real objects, so data never gets
interpolated into source.
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create();
try {
const result = await runtime.javascript.evaluate<number>(
"inputs.items.reduce((total, item) => total + item.price, 0)",
{ inputs: { items: [{ price: 10 }, { price: 32 }] } },
);
console.log(result.outcome === "succeeded" ? result.value : result.error); // 42
} finally {
await runtime.dispose();
}
Keep state between calls
Pass a contextId to keep one V8 isolate and its global state alive across
calls.
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create();
try {
await runtime.createContext("analysis");
await runtime.javascript.execute("globalThis.answer = 40", {
contextId: "analysis",
});
const result = await runtime.javascript.evaluate<number>(
"globalThis.answer + 2",
{
contextId: "analysis",
},
);
console.log(result.outcome === "succeeded" ? result.value : result.error); // 42
// Delete an idle context when you are done with it. `contexts.reset()`
// is the other option: it keeps the id and drops only the retained state.
await runtime.contexts.delete("analysis");
} finally {
await runtime.dispose();
}
A context runs one operation at a time, so reusing a busy contextId fails
immediately. Files, npm, Bash, and type checks may pass the same id, but they run
in fresh processes and never touch retained memory.
JavaScript defaults to format: "module". Each call is a separate root ES
module, so its imports, top-level declarations, and exports are scoped to that
call. Put values on globalThis when a later call in the same context needs
them. For REPL-style script semantics where top-level lexical declarations stay
visible, pass format: "commonjs" consistently for that context.
Create the context explicitly before use; an unknown id fails instead of silently starting fresh state. A context pins to the first inline language that used it, though JavaScript and TypeScript intentionally share one isolate.
Contexts live until the VM is disposed. They are in-memory state, so they do
not survive a library VM recreated from the same database, or a hosted VM’s
sleep and wake. Create them lazily on first use.
Type check before running
Executing TypeScript transpiles it without a semantic check, so validate generated code explicitly.
import { createAgentOsClient } from "@rivet-dev/agentos";
const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" });
const vm = client.agentOS.getOrCreate(["my-vm"]);
// Type checking validates generated code before it executes.
const checked = await vm.v1.typescript.check({
source: `const total: number = "not a number";`,
});
console.log(checked);
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create();
try {
// Type checking validates an agent's generated code before it executes.
// `filePath` labels the source for diagnostics; it is never read from disk.
const checked = await runtime.typescript.check(
`const total: number = "not a number";`,
{ filePath: "example.ts" },
);
// Feed these diagnostics back to the agent so it can fix the code it
// generated and try again, instead of running code you know is broken.
for (const diagnostic of checked.diagnostics) {
// error TS2322: Type 'string' is not assignable to type 'number'.
console.log(
`${diagnostic.category} TS${diagnostic.code}: ${diagnostic.message}`,
);
}
// Executing TypeScript only transpiles it, so check first if it matters.
if (checked.diagnostics.length === 0) {
await runtime.typescript.execute(`const total: number = 42;`);
}
} finally {
await runtime.dispose();
}
Install npm packages
import { createAgentOsClient } from "@rivet-dev/agentos";
const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" });
const vm = client.agentOS.getOrCreate(["my-vm"]);
await vm.v1.javascript.npm.install({});
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create();
try {
await runtime.javascript.npm.install({ frozen: true }); // lockfile-exact
const build = await runtime.javascript.npm.runScript("build");
console.log(build.outcome); // "succeeded"
await runtime.javascript.npm.runPackage("prettier", {
args: ["--check", "."],
});
} finally {
await runtime.dispose();
}
Installs modify the VM-wide filesystem, so a package installed once is importable
by every later execution in that VM, in any language. Only one npm/Python
mutation runs at a time per VM; a concurrent install fails with execution_busy.
Background processes and web servers
spawn starts a long-lived process and returns a handle. From there you get
stdin, output, signals, and waiting.
import { createAgentOsClient } from "@rivet-dev/agentos";
const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" });
const vm = client.agentOS.getOrCreate(["my-vm"]);
const server = await vm.v1.javascript.spawn({
source: `
import http from "node:http";
http.createServer((req, res) => res.end("ok")).listen(3000, "127.0.0.1");
await new Promise(() => {});
`,
});
// The listener stays inside the VM. No host port is exposed.
const response = await vm.v1.network.fetch({
request: { port: 3000, path: "/health" },
});
console.log(response.status); // 200
await vm.v1.process.signal({ process: server, signal: "SIGTERM" });
import { AgentOs } from "@rivet-dev/agentos-core";
const serverSource = `
import http from "node:http";
const app = http.createServer((req, res) => {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, path: req.url }));
});
app.listen(3000, "127.0.0.1", () => console.log("ready"));
await new Promise(() => {});
`;
const runtime = await AgentOs.create({ permissions: { network: "allow" } });
try {
// `spawn` returns as soon as the process starts, before the server is
// listening. Wait for the line it prints once it is ready.
const ready = Promise.withResolvers<void>();
const decoder = new TextDecoder();
const server = await runtime.javascript.spawn(serverSource, {
onStdout: (chunk) => {
const text = decoder.decode(chunk);
process.stdout.write(text);
if (text.includes("ready")) ready.resolve();
},
});
await ready.promise;
// The listener stays inside the VM. No host port is exposed.
const response = await runtime.network.httpRequest({
port: 3000,
path: "/health",
});
console.log(response.status); // 200
await runtime.process.signal(server.pid, "SIGTERM");
await runtime.process.wait(server.pid);
} finally {
await runtime.dispose();
}
Spawned processes always start with fresh state, so they take no contextId.
A full Linux environment underneath
There is a real Linux environment behind all of this, shared by every language. Files and installed packages are immediately visible to Bash, Python, and other executions.
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create({ permissions: { network: "allow" } });
try {
// Filesystem: node:fs is the VM's persistent filesystem.
const files = await runtime.javascript.execute(
`
import fs from "node:fs/promises";
await fs.writeFile("/workspace/data.txt", "hello");
console.log(await fs.readFile("/workspace/data.txt", "utf8"));
`,
{ output: { capture: "all" } },
);
console.log(files.stdout); // "hello\n"
// Process trees: node:child_process spawns real guest processes.
const processes = await runtime.javascript.execute(
`
import { execFileSync } from "node:child_process";
console.log(execFileSync("ls", ["-la", "/workspace"], { encoding: "utf8" }));
`,
{ output: { capture: "all" } },
);
console.log(processes.stdout); // Directory listing for /workspace
// Networking: sockets and fetch go through the VM network policy.
const status = await runtime.javascript.evaluate<number>(
`(async () => (await fetch("https://example.com")).status)()`,
);
console.log(status.outcome === "succeeded" ? status.value : status.error); // 200
} finally {
await runtime.dispose();
}
See Filesystem, Processes & Shells, and Networking.
Host functions
Guest JavaScript calls host functions as async globals, so host credentials stay outside the VM. Host functions are library-only.
import { AgentOs } from "@rivet-dev/agentos-core";
import { z } from "zod";
// The handler runs on the host, so the API key never enters the VM.
const vm = await AgentOs.create({
hostFunctions: {
weather: {
forecast: {
inputSchema: z
.object({ city: z.string() })
.describe("Get the weather forecast for a city"),
execute: async ({ city }) => {
const res = await fetch(
`https://api.weather.example/forecast?city=${city}&key=${process.env.WEATHER_API_KEY}`,
);
return res.json();
},
},
},
},
});
try {
// Guest JavaScript gets each collection as a global, and each host function
// as an async function.
const result = await vm.javascript.execute(
`
const forecast = await weather.forecast({ city: "Paris" });
console.log(forecast);
`,
{ output: { capture: "all" } },
);
console.log(result.outcome === "succeeded" ? result.stdout : result.error);
} finally {
await vm.dispose();
}
Permissions, limits, and timeouts
Every operation inherits the VM permission policy and resource limits.
import { AgentOs } from "@rivet-dev/agentos-core";
const runtime = await AgentOs.create();
try {
// timeoutMs is a wall-clock deadline for the whole operation. It expires
// into a result rather than throwing, and never replaces the VM watchdogs.
const result = await runtime.javascript.execute("while (true) {}", {
timeoutMs: 1_000,
});
console.log(result.outcome); // "timed_out"
} finally {
await runtime.dispose();
}
Embedded API
The library examples use AgentOs.create(). Contexts remain available until
the host calls dispose().
Read more in the embedded API quickstart.