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.
Letting an agent write code instead of chaining one tool 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 { AgentOs } from "@rivet-dev/agentos";
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 { AgentOs } from "@rivet-dev/agentos";
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
inputs hands host values to the guest as real objects, so data never gets
interpolated into source.
import { AgentOs } from "@rivet-dev/agentos";
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 globals, imports, and modules alive across calls in
one retained V8 isolate.
import { AgentOs } from "@rivet-dev/agentos";
const runtime = await AgentOs.create();
try {
await runtime.createContext("analysis");
await runtime.javascript.execute("const answer = 40", {
contextId: "analysis",
});
const result = await runtime.javascript.evaluate<number>("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 — 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.
Contexts live for the VM lifetime. They do not survive actor sleep/wake, because the VM is disposed; create them lazily on the first stateful use after wake.
Type check before running
Executing TypeScript transpiles it without a semantic check, so validate the agent’s generated code explicitly.
import { AgentOs } from "@rivet-dev/agentos";
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 { AgentOs } from "@rivet-dev/agentos";
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 pid. From there you get
stdin, output, signals, and waiting.
import { AgentOs } from "@rivet-dev/agentos";
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 {
const server = await runtime.javascript.spawn(serverSource, {
onStdout: (chunk) => process.stdout.write(new TextDecoder().decode(chunk)),
});
// 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, agents, and other executions.
import { AgentOs } from "@rivet-dev/agentos";
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 & Previews.
Bindings
Guest code invokes bindings as ordinary typed commands, so host credentials stay outside the VM.
import { AgentOs, type Bindings } from "@rivet-dev/agentos";
import { z } from "zod";
// The handler runs on the host, so the API key never enters the VM.
const weather: Bindings = {
name: "weather",
description: "Weather data bindings",
bindings: {
forecast: {
description: "Get the weather forecast for a city",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }: { city: string }) => {
const res = await fetch(
`https://api.weather.example/forecast?city=${city}&key=${process.env.WEATHER_API_KEY}`,
);
return res.json();
},
},
},
};
// The collection is projected into the VM as an `agentos-weather` command.
const runtime = await AgentOs.create({ bindings: [weather] });
try {
const result = await runtime.javascript.execute(
`
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);
const { stdout } = await run("agentos-weather", ["forecast", "--city", "Paris"]);
console.log(JSON.parse(stdout).result);
`,
{ output: { capture: "all" } },
);
console.log(result.outcome === "succeeded" ? result.stdout : result.error);
} finally {
await runtime.dispose();
}
Permissions, limits, and timeouts
Every operation inherits the VM permission policy and resource limits.
import { AgentOs } from "@rivet-dev/agentos";
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();
}