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

# Guest customisation templates

> Reusable cloud-init and sysprep templates — a saved boot script with declared variables, rendered into a VM at create time.

A **guest customisation template** is a boot script you save once and reuse: the user-data a Linux VM runs on first boot, or the `autounattend.xml` a Windows VM is sysprepped with. Both are stored in Celum's own database (not on a supervisor), both are Go `text/template` documents, and both declare their own variables so the same template can produce a different result per VM.

They are edited under **Settings → Cloud-init templates**, and selected on the Create VM form.

> [!NOTE]
>
> A template is not a VM image and not a cluster template. It customises a guest **after** it boots from an image you already have in the [Image library](https://docs.celum.io/vms/image-library.md); the cluster equivalent, which renders whole manifests and applies them to the supervisor, is on [Cluster templates](https://docs.celum.io/clusters/templates.md).

## The two collections with almost the same name

This is the one thing worth reading before anything else, because the failure it causes is a 404 with no clue in it.

| Collection | Keyed by | Where it lives | Usable as `cloudInitTemplateId`? |
| - | - | - | - |
| **Cloud-init templates** | database **id** | `/api/cloud-init-templates` | **Yes** — this is the one |
| **VM cloud-init templates** | **name** | `/api/vm-cloud-init-templates` | No — different collection, read-only |

Both are reachable from one MCP tool (`list_vm_templates`) under different `kind` values, which is exactly how the two get mixed up: `kind=cloud-init` returns the ids the rest of the flow expects, `kind=vm-cloud-init` returns names that 404 everywhere else. If a lookup fails with the id apparently right in front of you, check which kind produced it.

## What a template is made of

| Field | Meaning |
| - | - |
| Name | How it appears in the Create VM picker |
| Description | One line, for the picker |
| Content | The body — a Go `text/template` document referencing its variables as `{{.name}}` |
| Variables | The list the form renders: each has a `name`, a `label`, a `default` and a `description` |

A variable's `default` is what the form pre-fills and what a render uses when the caller says nothing. Overrides are merged over the defaults, so a caller sends only what it wants to change.

> [!WARNING]
>
> Rendering uses `missingkey=zero`: a reference that matches no declared variable renders as **empty**, not as an error. A misspelled variable name therefore produces a template that renders happily and boots a VM with a blank where a hostname or a package name should have been. **Render before you create** — that is what the preview exists for.

## How it reaches the VM

At create time the template is rendered, split on newlines, and **prepended** to the VM's startup commands. Both image families consume it identically: cloud-init images and Ignition (Talos-family) images take the same list, so one template serves both.

Two consequences of "prepended" that decide what you can write in one:

* **Template lines run before anything typed on the form, in the same boot script.** A startup command the operator adds on the Create VM screen runs *after* the template, so a template can set up a repository or a user that a later ad-hoc line depends on — but not the other way round. On a cloud-init image the lines become one shell script, so shell state carries on too: a template that starts with `set -e` makes a failing startup command typed on the form stop the rest of the script.
* **The guest-agent install runs first of all.** When it applies, the `qemu-guest-agent` install is prepended ahead of the template, so the agent is present before your script runs. It is skipped for ISO installs and Windows goldens, which never run cloud-init at all.

## Windows: sysprep templates

The Windows sibling is a **sysprep template** — an `autounattend.xml` with the same Go-template variables, chosen with `sysprepTemplateId` and `sysprepVariables`. Leaving it empty uses the platform's built-in answer file with the universal virtio driver-path list, which is what most golden clones want.

Provisioning scripts do **not** belong in the answer file. A Windows VM takes a separate `windowsSetupScript`, delivered as `setup.ps1` on the sysprep CD and run once at first logon — that is the Windows equivalent of a cloud-init startup command. Putting the same content in `FirstLogonCommands` instead looks equivalent and is not: the command-line length cap there will invalidate the whole answer file, and an invalid answer file means an unattended install that stops and waits for a human.

## Preview before you commit

`POST /api/cloud-init-templates/{id}/render` takes a `variables` object and returns the exact user-data a VM would boot with, plus the resolved variable set. It creates nothing. This is the only way to see the `missingkey=zero` problem before a VM has already booted around it.

> [tab] UI
>
> **Settings → Cloud-init templates** lists, creates, edits and deletes templates. The Create VM form shows the picker and renders a field per declared variable, pre-filled with its default.

> [tab] API
>
> ```text theme={null}
> GET    /api/cloud-init-templates
> POST   /api/cloud-init-templates
> GET    /api/cloud-init-templates/{id}
> PUT    /api/cloud-init-templates/{id}
> DELETE /api/cloud-init-templates/{id}
> POST   /api/cloud-init-templates/{id}/render
>
> GET    /api/sysprep-templates
> POST   /api/sysprep-templates
> GET    /api/sysprep-templates/default
> POST   /api/sysprep-templates/preview
> GET    /api/sysprep-templates/{id}
> PUT    /api/sysprep-templates/{id}
> DELETE /api/sysprep-templates/{id}
>
> GET    /api/vm-cloud-init-templates
> GET    /api/vm-cloud-init-templates/{name}
> ```

> [tab] MCP
>
> `list_vm_templates` (with `kind=cloud-init`, `vm-cloud-init`, `sysprep` or `sysprep-default`), `get_cloud_init_template`, `create_cloud_init_template`, `update_cloud_init_template`, `render_cloud_init_template`, `get_sysprep_template`, `create_sysprep_template`, `update_sysprep_template`.
>
> **There is no delete tool for either kind.** An assistant can create and revise templates but cannot remove one — deletion is a UI or API action.

## Permissions

| Task | Action | KRN |
| - | - | - |
| List / read cloud-init templates | `cloud-init-template:List`, `cloud-init-template:Get` | `krn:celum:cloud-init-template:*` |
| Create, edit, delete one | `cloud-init-template:Create`, `cloud-init-template:Update`, `cloud-init-template:Delete` | `krn:celum:cloud-init-template:*` |
| The same for Windows answer files | `sysprep-template:List` / `:Get` / `:Create` / `:Update` / `:Delete` | `krn:celum:sysprep-template:*` |
| Read the name-keyed VM collection | `vm-cloudinit-template:List`, `vm-cloudinit-template:Get` | `krn:celum:vm-cloudinit-template:*` |

Rendering a template is authorised as a **read** — it changes nothing — so an operator who may list templates may also preview them.

## What commonly goes wrong

| Symptom | Cause and fix |
| - | - |
| A rendered value is blank and nothing errored | `missingkey=zero`. The reference in the content does not match a declared variable name. Render and read the output. |
| `cloud-init template not found` on VM create | The id came from `kind=vm-cloud-init` (name-keyed) rather than `kind=cloud-init`. |
| The template ran but an operator-typed startup command did not take effect | Order. Template lines are prepended, so a later ad-hoc line overwrites what the template did, not the reverse. |
| A Windows unattended install stops at a prompt | Provisioning was put in `FirstLogonCommands` and hit the command-line length cap, invalidating the answer file. Use `windowsSetupScript`. |
| The template picker is empty | Templates live in Celum's database. Without `DATABASE_URL` the endpoints return `503` and there is nothing to pick. |

## Related

> [card] [Create a VM](https://docs.celum.io/vms/create.md)
>
> Where a template is selected and its variables filled in.

> [card] [Image library](https://docs.celum.io/vms/image-library.md)
>
> The golden images a template customises — and the RWO/RWX decision taken at import.

> [card] [Console & access](https://docs.celum.io/vms/console-and-access.md)
>
> SSH keys, passwords and the guest agent a template can assume.

> [card] [Cluster templates](https://docs.celum.io/clusters/templates.md)
>
> The cluster-level equivalent, which renders manifests instead of boot scripts.


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