Skip to main content
Execution

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: map and filter instead of jq and awk.
  • Parallelism: Promise.all instead of shell job control.

Evaluate an expression

evaluate() returns a JSON-serializable value.

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.

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.

Install npm packages

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.

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.