> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orgo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Template schema

> Every field in the orgo.ai/v1 template format.

A template is a single `orgo.ai/v1` document, written in YAML or JSON. This page is the field-by-field reference. The canonical machine-readable contract is the [JSON Schema](/api-reference/templates/schema) at `GET /api/template-schema`. Point your editor at it for autocomplete and inline validation.

```yaml theme={null}
# yaml-language-server: $schema=https://www.orgo.ai/api/template-schema
```

<Warning>
  The published JSON Schema describes the **canonical** form only. It requires `api_version` and `template`, and it rejects unknown top-level keys. An editor validating against it therefore flags every [sugar](#sugar-form) document as invalid on two counts. Sugar is desugared server-side, so check a sugar template with [Validate template](/api-reference/templates/validate) instead of relying on editor validation.
</Warning>

## Two forms

Templates accept a **canonical** form and a shorter **sugar** form. Both normalize to the same document and the same `digest`.

<CodeGroup>
  ```yaml Canonical theme={null}
  api_version: orgo.ai/v1
  template:
    name: my-workstation
    version: 1.0.0
  hardware:
    cpu: 2
    ram_gb: 4
    resolution: 1280x800x24
  env:
    NODE_ENV: production
  files:
    - to: /opt/welcome.txt
      inline: "hello"
  hooks:
    on_every_boot: |
      echo "ready"
  ```

  ```yaml Sugar theme={null}
  api_version: orgo.ai/v1
  name: my-workstation@1.0.0
  cpu: 2
  ram: 4gb
  display: 1280x800

  env:
    NODE_ENV: production

  files:
    /opt/welcome.txt: "hello"

  on_every_boot: |
    echo "ready"
  ```
</CodeGroup>

<a id="sugar-form" />

### Sugar → canonical

| Sugar | Canonical |
| - | - |
| `name: foo@1.0.0` | `template: { name: foo, version: 1.0.0 }` |
| `version:`, `description:`, `publisher:` | the matching `template` field |
| `cpu: 2` | `hardware: { cpu: 2 }` |
| `ram: 4gb` | `hardware: { ram_gb: 4 }` |
| `disk: 30gb` | `hardware: { disk_gb: 30 }` |
| `display: 1280x800` | `hardware: { resolution: 1280x800x24 }`, where depth defaults to `24` |
| `gpu: t4` / `audio: true` | the matching `hardware` field |
| `bandwidth: 500` | `hardware: { bandwidth_mbps: 500 }` |
| `files: { /path: "text" }` | `files: [{ to: /path, inline: "text" }]` |
| `files: { /path: { from: …, mode: … } }` | `files: [{ to: /path, from: …, mode: … }]` |
| `apps: { server: "cmd" }` | one app named `server` with one `restart: always` service running `cmd` |
| `on_every_boot: \|` | `hooks: { on_every_boot: … }`, with the same shortcut for the other four hooks |
| `wallpaper: "https://…"` | a `files` entry downloading the image to `/usr/share/backgrounds/wallpaper.jpg`, plus an `on_every_boot` hook that sets it as the XFCE background. That hook runs during the build's cold boot, so the wallpaper is baked into the golden snapshot |

`api_version` has no sugar: every document needs it, in either form. Don't mix the forms: a document with a canonical `template:` block that otherwise parses as canonical is read as canonical, and its top-level sugar keys, such as `cpu` or `on_every_boot`, are ignored. In a sugar document, a canonical `hardware` value wins over its flat key: if a document sets both `hardware.cpu` and top-level `cpu`, the `hardware` value is used and the sugar key is ignored. `ram` and `disk` accept `4gb`, `4g`, or a bare `4`; a sub-gigabyte size such as `512mb` resolves to `0`, which is the same as omitting it.

## Top-level fields

| Field | Type | Required | Description |
| - | - | :-: | - |
| `api_version` | string | yes | Must be `orgo.ai/v1`. Any other value is rejected with code `unknown_api_version`. |
| `template` | object | yes | [Identity](#metadata): name, version, description. |
| `hardware` | object | no | [Computer resource shape](#hardware). Omitted, every hardware field takes its default. |
| `secrets` | array | no | [Vault secrets](/guides/templates/secrets) the template needs. Max 64. |
| `vars` | map | no | [Variables](#vars-and-interpolation) for `${var.X}` interpolation. |
| `env` | map | no | [Environment variables](#env), literal or secret-backed. |
| `build` | object | no | [Build-time install steps](#build). |
| `files` | array | no | [Files](#files) written into the computer. Max 256. |
| `apps` | array | no | [Apps, services, and health checks](#apps). Max 64. |
| `triggers` | array | no | [Reactive automation](/guides/templates/triggers). Max 256. |
| `terminal` | array | no | [Pre-staged tmux sessions](#terminal). Max 32. |
| `hooks` | object | no | [Lifecycle shell hooks](#hooks). |
| `telemetry` | object | no | [Metrics and logs](#telemetry). |
| `egress_policy` | object | no | [Per-computer network rules](#egress-policy). Omitted, no filtering is applied. |
| `streaming` | array | no | [Outbound RTMP streams](#streaming). Max 16. |

Every field except `api_version` and `template` is optional, so the smallest valid template is a name, a version, and the API version.

<a id="metadata" />

## template

Identity and provenance. `name` and `version` are required.

```yaml theme={null}
template:
  name: claude-code          # lowercase kebab-case, 1-64 chars
  version: 1.0.0             # semver, immutable once published
  description: Claude Code CLI, ready in the terminal.
  publisher: orgo            # optional handle (kebab-case)
  license: MIT               # optional SPDX id (informational)
  homepage: https://…        # optional
  source: https://github.com/…  # optional
```

<a id="hardware" />

## hardware

The computer's resource shape. Every field is optional; omit one and the platform default applies. A [Create computer](/api-reference/computers/create) request's `cpu`, `ram`, and disk override these at launch. The template's `resolution` always wins over the request's, because the golden snapshot was captured at it.

| Field | Type | Values | Notes |
| - | - | - | - |
| `os` | string | `linux` | Linux only. |
| `cpu` | integer | `1`, `2`, `4`, `8`, `16` | vCPU count. Any other value is rejected. Capped by your plan. |
| `ram_gb` | integer | `4`, `8`, `16`, `32`, `64` | RAM in GB. Any other value is rejected. Capped by your plan. |
| `disk_gb` | integer | ≥ 1 | Disk in GB. |
| `gpu` | string | `none`, `t4`, `l4`, `a10`, `l40s`, `a100-40`, `a100-80`, `h100` | Accepted, but not used at launch. |
| `gpu_count` | integer | ≥ 0 | Accepted, but not used at launch. |
| `resolution` | string | `WIDTHxHEIGHTxDEPTH` | e.g. `1280x720x24`. All three parts are required in the canonical form. |
| `auto_stop_minutes` | integer | `0`-`1440` | Accepted, but not applied: computers launched from a template are created always-on. |
| `region` | string | `auto-us`, `iad`, `sjc`, `lax` | Accepted, but not used for placement. |
| `audio` | boolean | `true` / `false` | Enable the audio device. Defaults to `false`. |
| `bandwidth_mbps` | integer | ≥ 0 | Network bandwidth cap in Mbps. |

<a id="vars-and-interpolation" />

## vars and interpolation

`vars` are compile-time strings, interpolated across the document before build.

```yaml theme={null}
vars:
  region: sjc2
  tag: v7

env:
  REGION: "${var.region}"

files:
  - to: /opt/config
    inline: "region=${var.region} tag=${var.tag}"
```

* `${var.X}` resolves from `vars`.
* `${env.X}` resolves from a literal `env` value (not secret-backed ones).
* `$${var.X}` escapes to a literal `${var.X}`.
* Only the `var.` and `env.` forms are substituted, so a shell variable such as `${HOME}` passes through unchanged.
* An unknown reference is not caught by [Validate template](/api-reference/templates/validate) or at publish. It fails the build, and the build's `error` names the exact field path.

<a id="env" />

## env

Environment variables written to the computer. Keys must be `UPPER_SNAKE_CASE`. Each value is a literal string, or a `{secret: <name>}` reference resolved from the launching user's vault at create time. A value cannot be both.

```yaml theme={null}
secrets:
  - name: anthropic_api_key

env:
  NODE_ENV: production
  ANTHROPIC_API_KEY:
    secret: anthropic_api_key
```

A `{secret: …}` reference to a name you never declared under `secrets` is a validation error (`unknown_reference`).

Literal values are written to `/root/.env`, which is sourced at boot before services, terminal sessions, and boot hooks start, and by new interactive shells. Two caveats apply at launch, both covered in [Secrets](/guides/templates/secrets#reference):

* A `{secret: …}` reference is not applied. The value arrives only under the secret's own `UPPER_SNAKE_CASE` name, so the example above works because `ANTHROPIC_API_KEY` is that name.
* A launch that injects secrets rewrites `/root/.env` with only the secrets, so literal values are not set on that computer. Put a value a service needs in that service's own `env`.

<a id="build" />

## build

Package and command steps run once when baking the golden snapshot, before app installs. This is where dependencies get pre-installed so launches are instant.

```yaml theme={null}
build:
  apt: [ffmpeg, ripgrep]
  pip: [requests, httpx]
  npm: [pnpm]
  run:                      # up to 64 shell commands, in order
    - curl -fsSL https://example.com/install.sh | bash
```

<a id="files" />

## files

Files materialized into the computer. Each entry sets exactly one of `from` or `inline`. Setting both, or neither, is a validation error.

```yaml theme={null}
files:
  - to: /opt/hello.txt
    inline: "hello world"

  - to: /usr/share/backgrounds/bg.png
    from: https://example.com/bg.png   # fetched once, at build

  - to: /opt/run.sh
    mode: "0755"                        # octal permissions
    inline: |
      #!/bin/bash
      echo hi
    when: runtime                       # build (default) or runtime
```

| Field | Required | Description |
| - | :-: | - |
| `to` | yes | Absolute destination path. No `..`, no reserved system paths. A `~/…` path passes validation but is not expanded, so write `/root/…` instead. |
| `from` | one of | Source URI with scheme `http`, `https`, `git`, `s3`, `file`, or `secret`. See [File sources](#file-sources). |
| `inline` | one of | Inline file content. |
| `mode` | no | Octal permissions, 3 or 4 digits, e.g. `"0644"`. Omitted, the file is `0644`. |
| `owner` / `group` | no | Ownership, resolved against the computer's `/etc/passwd`. A name that does not resolve leaves the file owned by root. |
| `when` | no | `build` (the default, baked into the snapshot) or `runtime` (re-applied on each cold boot). |

<a id="file-sources" />

### File sources

Sources are fetched once, on the build machine, and the result is baked in.

* `http` and `https` download the URL.
* `git` clones over HTTPS: `git://github.com/owner/repo#path/to/file` writes that file, and a directory is written as a tar archive.
* `s3` works only for a public AWS bucket.
* `file` is accepted by validation, but the build cannot read it and fails.
* `secret://<name>` must name a declared secret, but the file is never written. See [Secrets](/guides/templates/secrets#reference).

<Warning>
  `to` must be an absolute path with no `..`. Reserved system paths are rejected: `/proc`, `/sys`, `/boot`, `/dev`, `/tmp`, `/run`, plus the Orgo runtime (`/etc/orgo`, `/var/orgo`, `/orgo`, `/etc/supervisor`, and Orgo's own binaries under `/opt` and `/usr/local/sbin`). Your apps can still write under `/opt` and elsewhere. Only Orgo's own runtime files are off-limits.
</Warning>

<a id="apps" />

## apps

An app bundles an install step with the long-running services, health checks, and ports it needs. Services are managed by supervisord, so they start at boot and respawn on crash.

```yaml theme={null}
apps:
  - name: my-app
    title: "My Application"
    install: |
      npm install -g my-app
    services:
      - name: my-app-server
        run: "my-app serve --port 8000"
        cwd: /opt/my-app
        user: root            # default: orgo (unprivileged)
        restart: always       # always | on-failure | no
        env:
          PORT: "8000"
    health:
      type: http              # http | tcp | command | process
      url: http://127.0.0.1:8000/health
      every: 15s
      timeout: 3s
      retries: 3
      on_fail: restart_service:my-app-server
    ports:
      - { internal: 8000, public: true }
    autostart:
      - { run: "firefox http://localhost:8000", delay: 3 }
```

App `name` is required and must be unique kebab-case. `requires` lists other app names this one depends on, and `data` lists persistent data paths, validated the same way as `files[].to`.

### services

| Field | Required | Description |
| - | :-: | - |
| `name` | yes | Unique service name (kebab-case), unique across all apps. |
| `run` | yes | The long-lived command. |
| `cwd` / `user` / `env` | no | Working directory, run-as user, and extra environment variables for this service. |
| `restart` | no | `always` (default), `on-failure`, or `no`. |
| `stop_signal` / `log` | no | Stop signal (e.g. `TERM`) and log file path. |

### health

A polled liveness check. After `retries` consecutive failures, `on_fail` runs. This is the per-app **watchdog**. `type` is required, and so is the field that type checks (`url`, `port`, `command`, or `process`). The other fields are optional.

| `type` | Checks |
| - | - |
| `http` | `url` returns 2xx/3xx |
| `tcp` | `port` accepts a connection |
| `command` | `command` exits `0` |
| `process` | `process` name is running |

`every` defaults to `30s`, `timeout` to `5s`, and `retries` to `3`. All durations are Go duration strings (`30s`, `2m`, `1h`). `on_fail` is one of `restart_service:<name>`, `restart_vm`, `alert`, or `none`. Only `restart_service` acts: `restart_vm` and `alert` write a log line and do nothing else.

### ports

| Field | Required | Description |
| - | :-: | - |
| `internal` | yes | In-computer port, 1-65535. `5900`, `5999`, `6080`, and `8080` are reserved by the Orgo runtime and rejected. |
| `external` | no | Host port to publish a `public` port on. `0` (the default) picks a free one. |
| `protocol` | no | `tcp`, `udp`, `http`, or `ws`. Accepted, but a published port forwards TCP only. |
| `public` | no | Expose publicly. Defaults to `false`. When `true`, the port is mapped to a host port. [Test-run template](/api-reference/templates/test-run) returns that mapping in `template_ports`; [Get computer](/api-reference/computers/get) does not. |

<a id="terminal" />

## terminal

Pre-staged tmux sessions. Each one is created detached on every cold boot unless it already exists, and its `run`, if set, is typed into it as the first command. A launch that restores the golden snapshot's memory keeps the sessions the build left running. Orgo's browser terminal attaches to them by name. `name` is required and must be unique.

```yaml theme={null}
terminal:
  - name: logs
    title: "Service Logs"
    cwd: /opt/my-app
    run: "tail -F /var/log/orgo/my-app.log"
```

<Note>
  Sessions are **not** auto-restarted. For a process that must respawn, use an [app service](#apps) instead.
</Note>

<a id="hooks" />

## hooks

Shell that runs at lifecycle points. Each runs with `set -e` and times out after 10 minutes.

<Warning>
  **Only `on_first_boot` and `on_every_boot` run today.** Validation accepts `on_pre_snapshot`, `on_resume`, and `on_shutdown`, but Orgo's builds do not run `on_pre_snapshot`, and launched computers do not run `on_resume` or `on_shutdown`.
</Warning>

| Hook | When |
| - | - |
| `on_first_boot` | Once, during the build. The marker it leaves is baked into the golden snapshot, so it never runs on a launched computer. |
| `on_every_boot` | On every **cold** boot: during the build, and on a launch that cold-boots instead of restoring the snapshot's memory. A launch cold-boots when it injects your [secrets](/guides/templates/secrets), or when its host cannot restore the snapshot. `/root/.env` is sourced first. |
| `on_pre_snapshot` | Accepted, but not run. |
| `on_resume` | Accepted, but not run. |
| `on_shutdown` | Accepted, but not run. |

```yaml theme={null}
hooks:
  on_every_boot: |
    echo "cold boot at $(date)" >> /var/log/orgo/boots.log
```

<Tip>
  Work that should be baked into the image belongs in [`build`](#build) or an app's `install`, not in a boot hook. To start a program with a launch-time secret, use a [terminal](#terminal) session's `run` or an [app service](#apps). See [Secrets](/guides/templates/secrets#secrets-and-golden-snapshots).
</Tip>

<a id="telemetry" />

## telemetry

```yaml theme={null}
telemetry:
  metrics: true               # push system metrics to otel_endpoint
  logs: [my-app-server]       # service names whose logs are pushed to otel_endpoint
  otel_endpoint: https://otel.example.com   # OTLP/HTTP collector; nothing is pushed without it
```

Metrics go to `<otel_endpoint>/v1/metrics` and logs to `<otel_endpoint>/v1/logs`, as OTLP JSON. Each `logs` entry names a service, whose log is `/var/log/orgo/<service>.log`.

<a id="egress-policy" />

## egress\_policy

Per-computer network filtering, applied on the host when the computer is created. `mode` is `allow` or `block` (`allowlist` and `denylist` also work), and `rules` lists domains, IPs, or CIDRs. The [JSON Schema](/api-reference/templates/schema) requires both fields, but publish does not check them: a policy with no rules, or with any other `mode`, leaves the computer unfiltered.

```yaml theme={null}
egress_policy:
  mode: allow                 # allow or block; see the rules below
  rules:
    - "*.github.com"
    - "api.anthropic.com"
    - "10.0.0.0/8"
```

* **Domain rules filter DNS lookups only.** A rule matches any lookup whose name contains its second-level label, so `*.github.com`, `github.com`, and `api.github.com` all match every name containing `github`.
* **IP and CIDR rules** filter traffic to those addresses.
* **`block`** drops lookups that match a domain rule and traffic to a listed address. Everything else is allowed.
* **`allow`** accepts traffic to listed addresses and lookups that match a domain rule, and drops all other lookups. With at least one domain rule, only lookups are filtered, so a connection made by IP address is not blocked. With only IP or CIDR rules, all other traffic is dropped.

The VNC and Desktop API paths are always reachable. Omit the block entirely for no filtering.

<a id="streaming" />

## streaming

Outbound RTMP(S) streams of the desktop. `name`, `url`, and `key` are required; `url` must start with `rtmp://` or `rtmps://`.

```yaml theme={null}
secrets:
  - name: stream_key

streaming:
  - name: live
    url: rtmps://ingest.example.com/app
    key:
      secret: stream_key       # literal string or a vault secret
    autostart: true            # defaults to false
```

<Note>
  `streaming` is parsed and validated, but no runtime streamer is wired up yet. A declared stream does not broadcast.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Secrets" icon="key" href="/guides/templates/secrets">
    The full secret-injection model.
  </Card>

  <Card title="Triggers" icon="bolt" href="/guides/templates/triggers">
    Sources, actions, and dedup.
  </Card>

  <Card title="Examples" icon="book-open" href="/guides/templates/examples">
    Annotated real templates.
  </Card>

  <Card title="Publish API" icon="code" href="/api-reference/templates/publish">
    Ship a template over HTTP.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.