# Node.js

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](https://blog.cloudflare.com/code-mode/). It has a few advantages over
driving [Bash](/agentos/docs/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.

examples/js-quickstart/src/index.ts:

```ts
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.

examples/js-sdk-overview/src/index.ts:

```ts
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.

examples/js-sdk-overview/src/inputs.ts:

```ts
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.

examples/js-sdk-overview/src/contexts.ts:

```ts
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.

examples/js-typescript/src/index.ts:

```ts
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

examples/js-sdk-overview/src/npm.ts:

```ts
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.

examples/js-dev-servers/src/index.ts:

```ts
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.

examples/js-sdk-overview/src/platform.ts:

```ts
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](/agentos/docs/filesystem), [Processes & Shells](/agentos/docs/processes), and
[Networking & Previews](/agentos/docs/networking).

## Bindings

Guest code invokes [bindings](/agentos/docs/bindings) as ordinary typed commands, so host
credentials stay outside the VM.

examples/bindings/exec-javascript.ts:

```ts
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](/agentos/docs/permissions) and
[resource limits](/agentos/docs/resource-limits).

examples/js-sdk-overview/src/limits.ts:

```ts
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();
}
```
