# Template gallery

Official templates give your agent a working environment in one call: the runtime, libraries and tools are already installed, and the sandbox starts from a RAM snapshot of the booted system (target p95 < 500 ms), not from a cold boot.

```python
sandbox = client.sandboxes.create(template="python")
print(sandbox.exec(command="python -c 'import pandas; print(pandas.__version__)'").stdout)
```

Every official template is built from a declarative recipe with pinned versions and digests, ships with an SBOM, stays within a size budget, and passes an in-sandbox smoke test (`taskcloud-smoke`) before release.

> **Private beta.** The gallery is rolling out region by region. `GET /templates?owner=builtin` lists the templates available to your account; `base` and `desktop` are available everywhere.

## Gallery

| Template | What's inside | Default size | Ports | Good for |
| --- | --- | --- | --- | --- |
| `base` | Ubuntu 24.04, systemd, Docker, Python 3.12, git, build-essential | `default` | – | anything; the default |
| `python` | Python 3.12 (default venv) and 3.13 via uv; NumPy, pandas, Polars, SciPy, scikit-learn, DuckDB, PyArrow, Matplotlib | `default` | – | data analysis, code interpreters, ML without GPU |
| `node` | Node.js 22 LTS, npm, pnpm, Bun, TypeScript | `default` | – | JS/TS agents, web app builders |
| `browser` | headless Chromium, Playwright for Python and Node, Noto/CJK/emoji fonts, Xvfb | `default` | – | browser automation, scraping, computer use without a desktop |
| `coding-agent` | git, gh, ripgrep, fd, build-essential, Docker in the VM, Node 22, Python 3.12; Codex CLI, Gemini CLI, OpenCode, Aider | `default` | – | coding agents, CI-like tasks, repo work |
| `jupyter` | JupyterLab (already running), Python 3.12 data stack | `default` | 8888 | notebooks, data exploration |
| `desktop` | `base` + XFCE, KasmVNC, Chromium, Firefox, xdotool | `default` | – | computer use with a visible desktop, see [Desktop and computer use](guides/desktop-and-computer-use.md) |

All templates run as user `user` (uid 1000) with passwordless `sudo`, have SSH, and keep the full disk persistent across pause and resume. Templates are not tied to a size: pass `size` to override the default (`desktop` needs `default` or `large`).

## Create a sandbox from a template

Use the template name, or its ID (`tpl_python`). The same call works for every template.

### TypeScript

```ts
import { Client } from "@taskcloud/sdk";

const client = new Client();
const sandbox = await client.sandboxes.create({ template: "python" });
const result = await sandbox.exec({ command: "taskcloud-smoke" });
console.log(result.exit_code, result.stdout); // 0 ... TASKCLOUD_SMOKE_OK python
```

### Python

```python
from taskcloud import Client

client = Client()
sandbox = client.sandboxes.create(template="python")
result = sandbox.exec(command="taskcloud-smoke")
print(result.exit_code, result.stdout)  # 0 ... TASKCLOUD_SMOKE_OK python
```

### CLI

```bash
id=$(taskcloud create --template python --json | jq -r .id)
taskcloud exec "$id" -- taskcloud-smoke
taskcloud template ls          # the gallery and your own templates
```

### curl

```bash
curl -sS -X POST https://api.task.cloud/v1/sandboxes \
  -H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
  -d '{"template":"python","name":"analysis-1"}'

curl -sS "https://api.task.cloud/v1/templates?owner=builtin" -H "Authorization: Bearer $TASKCLOUD_API_KEY"
```

### MCP

The `create_sandbox` tool of the [MCP server](mcp.md) describes the official templates, so an agent can pick one by itself. Tool call:

```json
{ "name": "create_sandbox", "arguments": { "template": "browser" } }
```

## Templates

### base

Ubuntu 24.04 with systemd, Docker, Python 3.12 (system `python3`), git and build-essential. This is what you get without a `template`. Built with debootstrap rather than from an OCI image.

```ts
const sandbox = await client.sandboxes.create({ template: "base" });
await sandbox.exec({ command: "docker run --rm alpine echo hello" });
```

### python

The default virtualenv `/opt/taskcloud/venv` (Python 3.12.16) is on `PATH` and owned by `user`, so `pip install` and `uv pip install` work without `sudo`. Python 3.13 is installed for `uv`: `uv run --python 3.13 ...`. Matplotlib uses the `Agg` backend (no display).

```python
sandbox = client.sandboxes.create(template="python")
sandbox.files.write("/home/user/sales.csv", "region,amount\neu,10\nus,32\n")
r = sandbox.exec(command="python -c \"import pandas as pd; print(pd.read_csv('sales.csv').amount.sum())\"")
print(r.stdout)  # 42
```

### node

Node.js 22 LTS in `/opt/taskcloud/node` (owned by `user`: `npm install -g` works without `sudo`), pnpm, Bun and TypeScript.

```ts
const sandbox = await client.sandboxes.create({ template: "node" });
await sandbox.exec({ command: "pnpm create vite@latest app -- --template react-ts && cd app && pnpm install" });
await sandbox.exec({ command: "cd app && pnpm dev --host 0.0.0.0 --port 5173", background: true });
const port = await sandbox.ports.expose({ port: 5173, visibility: "public" });
console.log(port.url);
```

### browser

Headless Chromium from Playwright, shared by the Python and Node bindings (same Playwright version, one browser build in `/ms-playwright`). Fonts cover Latin, CJK and emoji, so screenshots of real pages render correctly. For headed runs use `xvfb-run`; for a visible desktop use `desktop`.

```python
sandbox = client.sandboxes.create(template="browser")
script = """
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
    page = p.chromium.launch().new_page()
    page.goto("https://example.com")
    page.screenshot(path="/home/user/shot.png")
    print(page.title())
"""
sandbox.files.write("/home/user/shot.py", script)
print(sandbox.exec(command="python shot.py").stdout)
png = sandbox.files.read("/home/user/shot.png")  # bytes
```

From Node, `require("playwright")` works out of the box (`NODE_PATH` points at the global modules). In your own project, install the same version (`npm i playwright@1.64.1`) and it reuses the installed browser.

### coding-agent

Everything a coding agent needs in a repository: git, the GitHub CLI, ripgrep, fd, build-essential, Docker (the daemon runs inside the VM; `user` is in the `docker` group), Node 22 and Python 3.12.

Agent CLIs: open-source ones are preinstalled (Codex CLI, Gemini CLI, OpenCode, Aider). Proprietary CLIs are not redistributed in the image; install them inside the sandbox from the vendor, under the vendor's terms, with `taskcloud-agents install`:

```bash
taskcloud-agents list                    # what is installed, licenses, which env var each CLI reads
taskcloud-agents install claude-code     # npm install -g @anthropic-ai/claude-code (vendor's official method)
```

No credentials are in the image. Pass API keys as sandbox env when you create the sandbox:

```ts
const sandbox = await client.sandboxes.create({
  template: "coding-agent",
  env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY!, GH_TOKEN: process.env.GH_TOKEN! },
});
await sandbox.exec({ command: "gh repo clone acme/app && cd app && codex exec 'fix the failing test'", timeout_seconds: 1800 });
```

### jupyter

JupyterLab is already running on port 8888 when the sandbox starts. Every sandbox has its own token: pass `JUPYTER_TOKEN` in `env` at create, or read the generated one with `taskcloud-jupyter-token`. A token or login cookie from another sandbox (including a fork) does not work.

```python
import secrets

token = secrets.token_urlsafe(32)
sandbox = client.sandboxes.create(
    template="jupyter",
    env={"JUPYTER_TOKEN": token},
    ports=[{"port": 8888, "visibility": "public"}],
)
print(f"{sandbox.ports.url(8888)}/lab?token={token}")
```

```ts
const sandbox = await client.sandboxes.create({ template: "jupyter", ports: [{ port: 8888, visibility: "public" }] });
const token = (await sandbox.exec({ command: "taskcloud-jupyter-token" })).stdout.trim();
console.log(`${sandbox.ports.url(8888)}/lab?token=${token}`);
```

With `visibility: "private"` (the default) the edge also requires a sandbox access token; see [Networking](guides/networking.md). Health check without a token: `curl -fsS http://127.0.0.1:8888/api`.

### desktop

`base` plus an XFCE desktop streamed with KasmVNC, Chromium and Firefox. The desktop is already running. See [Desktop and computer use](guides/desktop-and-computer-use.md) for screenshots, input and the stream URL.

## Smoke tests

Every official template contains `taskcloud-smoke`, the same test we run before each release. It needs no network and no credentials, prints one line per check and ends with `TASKCLOUD_SMOKE_OK <template>` (exit code 0) or `TASKCLOUD_SMOKE_FAIL <template>` (exit code 1). Run it to check a sandbox after you changed it:

```bash
taskcloud exec sbx_2fqxnt4bkq7m -- taskcloud-smoke
```

| Template | What `taskcloud-smoke` checks |
| --- | --- |
| `base` | user, sudo, Python, compiler, Docker |
| `python` | Python 3.12 and 3.13, NumPy/pandas/Polars/scikit-learn/DuckDB, a Matplotlib PNG, a new venv with uv |
| `node` | Node 22, npm, pnpm, Bun running TypeScript, `tsc` |
| `browser` | a Chromium screenshot from Python and from Node, fonts, a headed run under Xvfb |
| `coding-agent` | git + ripgrep in a repo, gh, a C build, Docker, each agent CLI, no credentials in the image |
| `jupyter` | `/api` health, token required (403 without), a kernel start and stop |
| `desktop` | KasmVNC, X display, a desktop screenshot, Chromium and Firefox |

## How official templates are built

- **Recipes:** a Dockerfile per template on Ubuntu 24.04 pinned by digest (`base` and `desktop`: debootstrap), converted to a VM root filesystem by our template builder, booted, and snapshotted with RAM.
- **Pinned versions:** language runtimes and tools by exact version, downloads checked against SHA-256, Python packages from hash-locked requirement files, Ubuntu packages from a dated archive snapshot.
- **SBOM:** an SPDX and a CycloneDX SBOM per release, plus a package list at `/usr/share/doc/taskcloud-template/packages.txt` inside the sandbox.
- **Size budget:** the release fails if an image grows past its budget, so start times and storage stay predictable.
- **Metadata:** `/etc/taskcloud/template.json` in the sandbox names the template and version.

Current versions: Python 3.12.16 / 3.13.18, uv 0.13.7, Node.js 22.23.4, pnpm 12.18.0, Bun 1.4.4, TypeScript 7.0.2, Playwright 1.64.1, JupyterLab 4.6.6, gh 2.103.3, Codex CLI 0.167.0, Gemini CLI 0.67.0, OpenCode 1.19.5, Aider 0.86.2.

## Listing templates

`GET /templates` returns official templates with `official: true`, `tags`, `ports`, `docs_url` and `example_command`; your own templates have `official: false` and empty `tags` and `ports`.

```json
{
  "id": "tpl_jupyter",
  "name": "jupyter",
  "official": true,
  "builtin": true,
  "state": "ready",
  "size": "default",
  "tags": ["jupyter", "notebooks", "python", "data"],
  "ports": [{ "port": 8888, "name": "jupyterlab", "protocol": "http", "description": "JupyterLab; ..." }],
  "docs_url": "https://task.cloud/docs/templates.html#jupyter"
}
```

If your account already has a template with the same name as an official one (for example your own `python`), the name keeps resolving to yours; use the ID (`tpl_python`) for the official one.

## Customize

Start from an official template, install what you need, then save it as your own template so every new sandbox starts with it:

```ts
const sandbox = await client.sandboxes.create({ template: "python" });
await sandbox.exec({ command: "pip install transformers==4.57.0 && python -c 'import transformers'" });
const snap = await sandbox.snapshots.create({ name: "py-transformers" });
await client.templates.create({ name: "py-transformers", snapshot_id: snap.id });
```

Or build a template from your own OCI image; see [Templates](concepts/templates.md).

## Related

- [Templates](concepts/templates.md): versions, building your own from an image or a snapshot
- [Exec and files](guides/exec-and-files.md)
- [Networking](guides/networking.md): ports and access tokens
- [API reference](api-reference.md)
