Skip to content

Terminals

Last updated View as MarkdownAgent setup

A terminal is an interactive PTY in the current container for a sandbox. Use it for full-duplex terminal I/O: a browser shell, resize, interrupt, and reconnect.

Command execution uses exec and process handles. Terminals are a separate resource type. API reference: Terminals API.

Processes and terminals

Process (exec) Terminal
Role Supervised argv process Interactive PTY
Input Launch-time argv (and whatever the program reads on its own) PTY input via write() or browser connect()
Output logs(), output(), waits output(), snapshot, waitForExit()
Stop kill(signal?) interrupt() / terminate()
Lookup getProcess / listProcesses getTerminal / listTerminals

Both kinds of resource live only in the current container for a sandbox ID. Lookup methods do not start a container. Refer to Sandbox lifecycle and How long a process lives.

Create a terminal

const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	cols: 120,
	rows: 40,
});

console.log(terminal.id);
const terminal = await sandbox.createTerminal({
	command: ["bash"],
	cwd: "/workspace",
	cols: 120,
	rows: 40,
});

console.log(terminal.id);

You can write to the PTY from the Worker, resize it, stream output, or end it:

await terminal.write(new TextEncoder().encode("uname -a\n"));
await terminal.resize(100, 30);
await terminal.terminate();
await terminal.write(new TextEncoder().encode("uname -a\n"));
await terminal.resize(100, 30);
await terminal.terminate();

Lifetime

  • A terminal exists only in the current container for that sandbox ID.
  • getTerminal / listTerminals return null / [] when no container is running. They do not start one.
  • After the container stops or is replaced, old terminal IDs are invalid. Create a new terminal if you need one again.
  • An active terminal can keep the container alive across Worker requests, as an active process can.

Store terminal.id to resume the same PTY while that container is still up.

Browser connect

  1. Create a terminal and keep terminal.id with the sandbox id.
  2. On each WebSocket upgrade, resolve the terminal with getTerminal, then return terminal.connect(request).
  3. In the browser, use @cloudflare/sandbox/xterm with terminalId.

Worker

import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (
			url.pathname === "/ws/terminal" &&
			request.headers.get("Upgrade")?.toLowerCase() === "websocket"
		) {
			const sandboxId = url.searchParams.get("sandboxId");
			const terminalId = url.searchParams.get("terminalId");
			if (!sandboxId || !terminalId) {
				return new Response("sandboxId and terminalId are required", {
					status: 400,
				});
			}

			const sandbox = getSandbox(env.Sandbox, sandboxId);
			const terminal = await sandbox.getTerminal(terminalId);
			if (!terminal) {
				return new Response("Terminal not found", { status: 404 });
			}

			return terminal.connect(request, {
				cursor: url.searchParams.get("cursor") ?? undefined,
			});
		}

		return new Response("Not found", { status: 404 });
	},
};
import { getSandbox } from "@cloudflare/sandbox";

export { Sandbox } from "@cloudflare/sandbox";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (
			url.pathname === "/ws/terminal" &&
			request.headers.get("Upgrade")?.toLowerCase() === "websocket"
		) {
			const sandboxId = url.searchParams.get("sandboxId");
			const terminalId = url.searchParams.get("terminalId");
			if (!sandboxId || !terminalId) {
				return new Response("sandboxId and terminalId are required", {
					status: 400,
				});
			}

			const sandbox = getSandbox(env.Sandbox, sandboxId);
			const terminal = await sandbox.getTerminal(terminalId);
			if (!terminal) {
				return new Response("Terminal not found", { status: 404 });
			}

			return terminal.connect(request, {
				cursor: url.searchParams.get("cursor") ?? undefined,
			});
		}

		return new Response("Not found", { status: 404 });
	},
};

Create the terminal from an application route when the UI needs one:

const sandboxId = "user-123";
const sandbox = getSandbox(env.Sandbox, sandboxId);
const terminal = await sandbox.createTerminal({ command: ["bash"] });
return Response.json({ sandboxId, terminalId: terminal.id });
const sandboxId = "user-123";
const sandbox = getSandbox(env.Sandbox, sandboxId);
const terminal = await sandbox.createTerminal({ command: ["bash"] });
return Response.json({ sandboxId, terminalId: terminal.id });

Browser (xterm.js)

npm install @xterm/xterm @xterm/addon-fit @cloudflare/sandbox@next
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";

const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
const sandboxAddon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
});

term.loadAddon(fitAddon);
term.loadAddon(sandboxAddon);
term.open(document.getElementById("terminal"));
fitAddon.fit();

// Values returned by your create-terminal route
const sandboxId = "user-123";
const terminalId = "term_...";
sandboxAddon.connect({ sandboxId, terminalId });
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";

const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
const sandboxAddon = new SandboxAddon({
	// `origin` is already a WebSocket origin (`wss://` or `ws://`).
	getWebSocketUrl: ({ sandboxId, terminalId, cursor, origin }) => {
		const params = new URLSearchParams({ sandboxId });
		if (terminalId) params.set("terminalId", terminalId);
		if (cursor) params.set("cursor", cursor);
		return `${origin}/ws/terminal?${params}`;
	},
	reconnect: true,
});

term.loadAddon(fitAddon);
term.loadAddon(sandboxAddon);
term.open(document.getElementById("terminal")!);
fitAddon.fit();

// Values returned by your create-terminal route
const sandboxId = "user-123";
const terminalId = "term_...";
sandboxAddon.connect({ sandboxId, terminalId });
Stable package Preview
sandbox.terminal(request) createTerminal + getTerminal + connect
xterm / URL sessionId terminalId (and optional cursor)

Was this helpful?