Set MANDALA_API_KEY to your bearer key. MANDALA_BASE_URL, when set, is the full API base including /api/v1. Replace the illustrative resource IDs with yours. These pinned 0.16.0 SDK and direct HTTP examples do not read saved device-login credentials.
200 Complete template JSON Schema response.
HTTP 200
Content-Type: application/json
X-Request-ID: 3f2c9a1e-7b4d-4e8a-9c61-0d5f2b8e4a17
{
"$id": "https://app.mandala.computer/api/v1/templates/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"description": "A launch template: what a computer is built from and the shape it gets when the create names no numbers. mandala/v1 describes how one is BUILT as well: `spec.build` is applied to the parent named by `spec.from`, step by step in the order written, and `spec.env` is written into the image's profile. What is still absent is a build from nothing — every document layers over a parent — and lifecycle hooks, because nothing runs them; a field arrives under this apiVersion only once something executes it, and adding one does not make a v2.",
"properties": {
"apiVersion": {
"const": "mandala/v1",
"description": "The document format. A new value here means a field that already existed has changed meaning; fields being added does not."
},
"kind": {
"const": "Template",
"description": "Always Template."
},
"metadata": {
"additionalProperties": false,
"description": "What the template is called. Nothing here reaches the image.",
"properties": {
"label": {
"description": "What a person is shown in the catalogue. Optional; a template with no label is shown by its name.",
"maxLength": 64,
"type": "string"
},
"name": {
"description": "The template's name within its namespace. This is what a create's `template` field carries today.",
"maxLength": 63,
"not": {
"pattern": "\\s"
},
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$",
"type": "string"
},
"namespace": {
"description": "The publisher this template belongs to. `system` is the catalogue that ships with the product.",
"maxLength": 63,
"not": {
"pattern": "\\s"
},
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$",
"type": "string"
},
"version": {
"description": "MAJOR.MINOR.PATCH. A published ref is immutable: republishing namespace/name@version with any change at all is refused, so a change means a new version. Prerelease and build metadata are not accepted — they would be two names for one document.",
"not": {
"pattern": "\\s"
},
"pattern": "^(0|[1-9][0-9]{0,8})\\.(0|[1-9][0-9]{0,8})\\.(0|[1-9][0-9]{0,8})$",
"type": "string"
}
},
"required": [
"namespace",
"name",
"version"
],
"type": "object"
},
"spec": {
"additionalProperties": false,
"description": "What the template is.",
"properties": {
"build": {
"description": "What to do to the parent image, in order. Order is the semantics: the steps are applied in the sequence written. A template with no build steps is one whose image a script produced — see `family`.",
"items": {
"oneOf": [
{
"additionalProperties": false,
"properties": {
"apt": {
"additionalProperties": false,
"description": "Install Debian packages.",
"properties": {
"packages": {
"items": {
"not": {
"pattern": "\\s"
},
"pattern": "^[a-z0-9][a-z0-9+.-]*[a-z0-9+](:[a-z0-9][a-z0-9-]*)?(=[A-Za-z0-9.+:~-]+|/[a-z0-9.-]+)?$",
"type": "string"
},
"minItems": 1,
"type": "array"
},
"recommends": {
"description": "Install recommended packages too. Defaults to false, which is what you almost always want — a toolchain that drags in a documentation browser is a bigger image and a bigger attack surface. A desktop is the case that wants true.",
"type": "boolean"
}
},
"required": [
"packages"
],
"type": "object"
}
},
"required": [
"apt"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"run": {
"additionalProperties": false,
"description": "Run a shell script inside the image.",
"properties": {
"as": {
"description": "Run as this user, through a login shell. Omit for root. Getting this wrong is the most consequential mistake in a build of this kind: an installer run as root puts the user's data in /root, where the desktop session — which logs in as someone else — will never see it, and the image looks installed and behaves uninstalled.",
"not": {
"pattern": "\\s"
},
"pattern": "^$|^[a-z_][a-z0-9_-]*$",
"type": "string"
},
"script": {
"not": {
"pattern": "\u0000"
},
"pattern": "[^\\s
]",
"type": "string"
}
},
"required": [
"script"
],
"type": "object"
}
},
"required": [
"run"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"file": {
"additionalProperties": false,
"description": "Write a file into the image.",
"properties": {
"content": {
"type": "string"
},
"mode": {
"description": "Octal, as a STRING — YAML reads a bare 0644 as the decimal 644, which is a mode nobody meant and chmod would accept.",
"not": {
"pattern": "\\s"
},
"pattern": "^$|^0[0-7]{3}$",
"type": "string"
},
"owner": {
"description": "`user:group`. Omit for root:root.",
"not": {
"pattern": "\\s"
},
"pattern": "^$|^[a-z_][a-z0-9_-]*:[a-z_][a-z0-9_-]*$",
"type": "string"
},
"path": {
"not": {
"pattern": "(^|/)\\.\\.?(/|$)"
},
"pattern": "^/([^/\\x00:]+/)*[^/\\x00:]+$",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
"required": [
"file"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"mkdir": {
"additionalProperties": false,
"description": "Create a directory, with parents.",
"properties": {
"path": {
"not": {
"pattern": "(^|/)\\.\\.?(/|$)"
},
"pattern": "^/([^/\\x00:]+/)*[^/\\x00:]+$",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
"required": [
"mkdir"
],
"type": "object"
}
]
},
"type": [
"array",
"null"
]
},
"desktop": {
"description": "The display protocol this image's desktop speaks. Omit it for X11, which is what every Linux image built before this field runs. It decides which guest scripts the window, window-action and clipboard routes use: an X11 image is read with xprop and xdotool, a Wayland one through its compositor. Setting it wrongly does not break the desktop — it makes those three routes refuse, because the tools they reach for are not the ones the guest has.",
"enum": [
"x11",
"wayland"
]
},
"env": {
"additionalProperties": {
"not": {
"pattern": "[\n\u0000]"
},
"pattern": "^[^\n\u0000]*$",
"type": "string"
},
"description": "Environment variables baked into the image, written as a script in `/etc/profile.d` so every login shell in every session sees them. Rendered in key order, so the same document always produces the same bytes.",
"propertyNames": {
"not": {
"pattern": "\\s"
},
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
},
"type": [
"object",
"null"
]
},
"family": {
"description": "The golden image family this template resolves to, without a version suffix — the version a computer is pinned to is chosen at launch from the host's `<family>.current` pointer, so `golden-xfce` and never `golden-xfce-v6`. This field is the seam between a document and an image a build script produced, and the compiler removes it: a content-addressed template is named by its build digest. A document that declares build steps has to name a family of your OWN — `golden-<your account id>`, or that and a `-` and a name of your choosing — because a build WRITES into the family it names; a document with no steps names a family to launch FROM and is under no such rule. That is not checked here: this daemon has no accounts, so the answer comes from the control plane on publish and on build.",
"not": {
"pattern": "-v[0-9]+$|\\s"
},
"pattern": "^golden-[a-z0-9]([a-z0-9-]*[a-z0-9])?$",
"type": "string"
},
"from": {
"description": "The template this one is layered onto, as `namespace/name`. Records lineage, and is deliberately unversioned: the build follows the base family's `.current` pointer rather than a pin, so a version here would assert a guarantee the build does not make.\n\nA document that also declares `spec.build` or `spec.env` may only name a parent in the `system` namespace, and is refused otherwise — at validate and at publish. A build layers onto the parent's IMAGE, and a hypervisor resolves a parent only in the catalogue compiled into it: a build request carries the child document alone, so there is nothing that could tell it what `acme/base` is. Layering onto a template of your own is not supported yet. A document with no build steps is under no such rule — it records lineage for an image something else produced, and may name any parent.",
"not": {
"pattern": "\\s"
},
"pattern": "^(?:$|[a-z0-9]([a-z0-9-]*[a-z0-9])?/[a-z0-9]([a-z0-9-]*[a-z0-9])?)$",
"type": "string"
},
"hardware": {
"additionalProperties": false,
"description": "The shape a computer gets when the create names no numbers. Not a constraint — what a computer may actually be given is bounded by the plan, and any size the plan allows is reachable at create or by a later resize.",
"properties": {
"cpu": {
"description": "Virtual CPUs.",
"maximum": 2147483647,
"minimum": 1,
"type": "integer"
},
"disk_gb": {
"description": "Disk, in GiB. Unlike cpu and ram_mb this is a FLOOR rather than a default: a computer's disk is an overlay over the family's image and cannot be smaller than it, so a create asking for less is raised to this and charged for it.",
"maximum": 2147483647,
"minimum": 1,
"type": "integer"
},
"ram_mb": {
"description": "Memory, in MiB.",
"maximum": 2147483647,
"minimum": 512,
"type": "integer"
}
},
"required": [
"cpu",
"ram_mb",
"disk_gb"
],
"type": "object"
},
"icon": {
"description": "A slug naming a mark the console has an asset for (\"claude\", \"debian\"), shown on the templates grid in place of a generic desktop icon. Purely cosmetic and never validated against a list published here: an unrecognised value, or none, just falls back to the plain icon. Not a URL — a document points at one of a small set the console ships assets for, rather than handing it an image to fetch and render.",
"maxLength": 32,
"not": {
"pattern": "\\s"
},
"pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$",
"type": "string"
},
"os": {
"description": "The guest's operating system. This is the field a plan is checked against: an account whose plan does not admit the OS cannot launch the template and is not shown it.",
"enum": [
"linux",
"windows"
]
},
"readiness": {
"additionalProperties": false,
"description": "How this template says it has finished coming up: a command run in the guest, polled until it exits zero. Readiness is a property of what YOU installed — a desktop that has drawn, a server that is listening — and only you can state it, which is why there is a field for it rather than a heuristic. Omit it and the host will not capture a booted machine of this template, because nothing can tell it when to look; declare one and a launch resumes a running machine instead of cold-booting. What is checked HERE is the SHAPE — that there is a command, that `as` is a user name, that the timeout is in range. Whether the command can ever report your template ready is a question about your image, and nothing here runs it: `exec: false` is a valid document and a check that never passes.",
"properties": {
"as": {
"description": "Run the check as this user, through a login shell. Omit for root. A desktop's readiness is almost never visible to root — `systemctl --user`, the session bus and the process list of a graphical session all belong to the account that logged in.",
"not": {
"pattern": "\\s"
},
"pattern": "^$|^[a-z_][a-z0-9_-]*$",
"type": "string"
},
"exec": {
"description": "The command, run in the guest with `bash`. Exit zero means ready. A port check and an HTTP probe are both this — `ss -ltn | grep -q :8080`, `curl -fsS localhost:8080/health` — which is why there is one field here and not three. Without `as` it runs as root and does NOT get a login shell, so it inherits the guest agent's environment rather than a profile's — name binaries by path, or set `as` and get a login shell for that user.",
"not": {
"pattern": "\u0000"
},
"pattern": "[^\\s
]",
"type": "string"
},
"timeout_sec": {
"description": "How long to keep asking before giving up, in seconds. Omit it, or give 0, for the default of 300 seconds; the most that can be asked for is 1800. It is a ceiling and not an expectation: the poll stops at the first exit zero, so a template ready in ten seconds waits ten seconds whatever this says.",
"maximum": 1800,
"minimum": 0,
"type": "integer"
}
},
"required": [
"exec"
],
"type": [
"object",
"null"
]
}
},
"required": [
"os",
"family",
"hardware"
],
"type": "object"
}
},
"required": [
"apiVersion",
"kind",
"metadata",
"spec"
],
"title": "Mandala Computer template",
"type": "object"
}