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-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 { 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
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 — 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 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-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 { 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 pid. From there you get
stdin, output, signals, and waiting.
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, agents, 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 & Previews.
Host functions
Guest code invokes host functions as ordinary typed commands, so host credentials stay outside the VM.
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 weather = {
forecast: {
inputSchema: z
.object({ city: z.string() })
.describe("Get the weather forecast for a city"),
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({ hostFunctions: { weather: 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-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 examples above already use AgentOs.create(). Contexts remain available
until the host calls dispose(); there is no actor sleep cycle to recreate them
after.
Read more in the embedded API quickstart.