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

# Templates

> Name an image once, then create every instance from that name.

A template is a named image, plus (optionally) the port its main service listens on. Instances are always created from template names: pass `template` on [`POST /v1/instances`](/docs/agents-api/instances), or omit it for the default `agent37-hermes`. Direct image references are rejected with `400 invalid_request`; register a template first, then pass its name.

```bash curl theme={null}
curl -X POST https://api.agent37.com/v1/instances \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "template": "agent37-hermes" }'
```

Templates come in two scopes. `system` templates are the built-in catalog, the same for every workspace and read-only. `workspace` templates are ones you register from your own image, either a registry reference (public, or private with `registry_auth`) or a [cloud build](/docs/agents-api/custom-image#build-it-in-the-cloud), and they are visible only to your workspace.

<Note>
  Registering a workspace template copies your image once into private storage and pins it by digest. That snapshot is what instances run: registration takes up to a few minutes for a large image, the image is stored privately and never republished, and re-pushing the same registry tag later has no effect until you [update the template](#update-a-template).
</Note>

## System catalog

The `agent37-` prefix is reserved for system templates. The catalog has eight entries today:

| Template               | What it runs                                                                                                                                                                      |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent37-hermes`       | Hermes, the general agent: chat, browsing, code, files. The default.                                                                                                              |
| `agent37-hermes-small` | Hermes, lean: chat, code, files, shell. No browser or desktop.                                                                                                                    |
| `agent37-openclaw`     | OpenClaw, the general agent: chat, headless browsing, code, files.                                                                                                                |
| `agent37-claude-code`  | Claude Code, Anthropic's coding agent, on your own Anthropic account: chat, code, files, shell. No browser or desktop.                                                            |
| `agent37-codex`        | Codex, OpenAI's coding agent, on your own OpenAI account: chat, code, files, shell. No browser or desktop.                                                                        |
| `agent37-grok`         | Grok, xAI's coding agent, on your own xAI API key: chat, code, files, shell. No browser or desktop.                                                                               |
| `agent37-opencode`     | OpenCode, SST's open-source coding agent, on the managed model out of the box: chat, code, files, shell. No browser or desktop.                                                   |
| `agent37-n8n`          | n8n, the workflow automation tool, on its own always-on instance: visual editor, webhooks, app integrations, with the managed model ready for its AI nodes. Not an agent harness. |

Every template, system or your own, offers the same three shapes and starts at 2 vCPU / 4 GB, \$4.76 per month; see [sizing](/docs/agents-api/instances). `agent37-hermes-small` is worth picking when your agent does not need a browser or a desktop: the image is far smaller, so it pulls and boots faster and leaves more of the shape to your own work. `agent37-claude-code` runs on your own Anthropic account: connect it before the first chat turn; see [Host Claude Code](/docs/agents-api/claude-code). `agent37-codex` runs on your own OpenAI account, same rule; see [Host Codex](/docs/agents-api/codex). `agent37-grok` runs on your own xAI API key, same rule; see [Host Grok](/docs/agents-api/grok). `agent37-opencode` runs on the managed model out of the box, no account or key needed; point it at your own provider when you want a specific model, see [Host OpenCode](/docs/agents-api/opencode). `agent37-n8n` runs a web app, not an agent: there is no chat API on it, and its editor gets a public URL on create; see [Host n8n](/docs/agents-api/n8n).

<Note>
  Passing any other `agent37-` name on instance create returns `400 invalid_request`, because the prefix is reserved for system templates.
</Note>

On every system template but one, port `3737` is the gateway: the [chat API](/docs/agents-api/chat) at `https://{instanceId}.agent37.app`. Any other port an instance serves, including the built-in dashboard, terminal, and file browser, is reachable at its derivable `https://{instanceId}-{port}.agent37.app` URL; see [Instance and preview URLs](/docs/agents-api/urls). `agent37-n8n` is the exception: its default port is `5678`, there is no gateway, and it serves the n8n editor on a public URL; see [Host n8n](/docs/agents-api/n8n).

System templates track the newest platform image: their `image_ref` moves forward as new versions ship, and each create uses whatever the template points at in that moment. Existing instances are unaffected: an instance keeps the image it was created from until you [update it](/docs/agents-api/instances#update).

## Pin a template version

If you need every instance you create to be identical (say a setup script runs after each create and depends on exact paths or versions), pin the version: append `@<tag>` (system templates) or `@<revision>` (workspace templates) to the template name on create.

```bash curl theme={null}
curl -X POST https://api.agent37.com/v1/instances \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "template": "agent37-hermes@<tag>" }'
```

Every release is an immutable dated tag, never republished, so a pin is a freeze. The published tags are on GHCR: [hermes](https://github.com/orgs/agent37-platform/packages/container/package/hermes), [hermes-small](https://github.com/orgs/agent37-platform/packages/container/package/hermes-small), [openclaw](https://github.com/orgs/agent37-platform/packages/container/package/openclaw), [claude-code](https://github.com/orgs/agent37-platform/packages/container/package/claude-code), [codex](https://github.com/orgs/agent37-platform/packages/container/package/codex), [opencode](https://github.com/orgs/agent37-platform/packages/container/package/opencode); the current tag is the one in each template's `image_ref` on `GET /v1/templates`. An unpublished tag returns `400 invalid_request`, and so does `@latest`, because the bare name already follows the latest release. To check a version before creating from it, `GET /v1/templates/agent37-hermes@<tag>`: it returns the template with the pinned `image_ref`, or `404` if that version isn't published.

Workspace templates pin by revision number: every image publish (a changed `image_ref` or a successful [cloud build](#build-an-image-in-the-cloud)) appends a release, `GET /v1/templates/{name}` lists them under [`revisions`](#the-template-object), and `my-agent@2` freezes revision `2`. `GET /v1/templates/my-agent@2` probes one the same way as a system tag, returning `404` if it was never published, and an unpublished revision on create or update returns `400 invalid_request` naming the published ones.

The pin sticks: the instance object's `template` field carries it (`"agent37-hermes@2026.07.02b"`, `"my-agent@2"`), and every lifecycle operation, including [`update`](/docs/agents-api/instances#update), stays on the pinned version. To pin an existing instance, move its pin, or clear it, pass a `template` to update: the name with an `@<tag>` or `@<revision>` sets or switches the pin, the bare name clears it and follows latest again.

## Endpoints

| Method   | Path                             | Returns                                                                                       |
| -------- | -------------------------------- | --------------------------------------------------------------------------------------------- |
| `GET`    | `/v1/templates`                  | `200` `{ "data": [...] }`, system catalog first, then your workspace templates sorted by name |
| `POST`   | `/v1/templates`                  | `201` with the new workspace template                                                         |
| `GET`    | `/v1/templates/{name}`           | `200` with one template, system or workspace                                                  |
| `PATCH`  | `/v1/templates/{name}`           | `200` with the updated workspace template                                                     |
| `DELETE` | `/v1/templates/{name}`           | `200` `{ "name": "...", "deleted": true }`                                                    |
| `POST`   | `/v1/template-builds`            | `201` with a new cloud build and a presigned URL for its build context                        |
| `POST`   | `/v1/template-builds/{id}/start` | `200` with the build now `building`                                                           |
| `GET`    | `/v1/template-builds/{id}`       | `200` with the build, including the published template once it succeeds                       |
| `GET`    | `/v1/template-builds/{id}/logs`  | `200` with the build plus an incremental chunk of its build log                               |

```bash curl theme={null}
curl https://api.agent37.com/v1/templates \
  -H "Authorization: Bearer sk_live_..."
```

## Build an image in the cloud

To publish a local or private image, you don't upload the image; you upload a small **build context** (a `Dockerfile` plus the files it `COPY`s) and Agent37 builds the image on its own infrastructure. No local Docker needed, and nothing has to be public except what the build fetches. The supported path is the Agent37 CLI:

```bash theme={null}
export AGENT37_API_KEY=sk_live_...
npx agent37 templates build ./my-agent --name my-agent
```

The directory defaults to `.` and must have a `Dockerfile` at its root. The CLI packs the **whole folder**, minus `.git` and your `.dockerignore` patterns (plain patterns only; `!` negations are ignored), gzips it (1 GB cap), and streams the live build log to your terminal; on failure it exits non-zero with the failing step visible. Every unexcluded file ships with the context, a stray `.env` included, and a `COPY . .` bakes it into the image, so check the folder, or add a `.dockerignore`, before you build. `--name` defaults to the folder name, and `--default-port <port>` sets the published template's default port. Re-building an existing name publishes a new template revision; existing instances never change. Ctrl-C does not cancel the build; it keeps running server-side. `--secret id=NAME` hands a value to `RUN --mount=type=secret,id=NAME` steps and `--registry-auth HOST=USER:PASSWORD` lets `FROM` pull from a private registry; see [build secrets](/docs/agents-api/custom-image#build-secrets).

See [Build a custom image](/docs/agents-api/custom-image) for the walkthrough. The raw flow below is for clients scripting the same thing without the CLI.

### 1. Create a build

`POST /v1/template-builds` with the template name and the exact byte count of your gzipped context:

<ParamField body="name" type="string" required>
  The template the build publishes, following the same rules as [template names](#register-a-workspace-template). A new name is created at revision `1` when the build succeeds; an existing name gets a new revision.
</ParamField>

<ParamField body="size_bytes" type="integer" required>
  Exact size of the gzipped context, from `1` through `1,000,000,000` bytes (1 GB).
</ParamField>

<ParamField body="default_port" type="integer">
  Applied to the published template, with the same semantics as on [`POST /v1/templates`](#register-a-workspace-template).
</ParamField>

<ParamField body="description" type="string">
  Optional free-text description for the published template.
</ParamField>

```bash curl theme={null}
tar --exclude-vcs -czf context.tar.gz -C ./my-agent .
BYTES=$(wc -c < context.tar.gz | tr -d ' ')

curl --fail-with-body -sS -X POST https://api.agent37.com/v1/template-builds \
  -H "Authorization: Bearer $AGENT37_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"my-agent\",\"size_bytes\":$BYTES}" > build.json
```

```json response theme={null}
{
  "id": "tb_7d3a90b81f264bc2a113",
  "name": "my-agent",
  "status": "created",
  "upload_url": "https://uploads.example.com/...signed...",
  "expires_at": 1784192400,
  "max_bytes": 1000000000
}
```

<ResponseField name="id" type="string">
  The workspace-scoped build id, `tb_` followed by 20 lowercase hex characters.
</ResponseField>

<ResponseField name="upload_url" type="string">
  A presigned `PUT` URL for the gzipped context, valid for one hour. It is a bearer credential: send no Agent37 key to it, and do not log or share it.
</ResponseField>

<ResponseField name="expires_at" type="integer">
  When `upload_url` expires, in epoch seconds.
</ResponseField>

<ResponseField name="max_bytes" type="integer">
  The context cap: exactly `1,000,000,000` bytes (1 GB).
</ResponseField>

### 2. Upload the context

`PUT` the gzipped tar to `upload_url`. The archive must have `Dockerfile` at its root: pack the folder's contents, not the folder. Clear curl's default `Expect: 100-continue` header; it is incompatible with these presigned uploads:

```bash curl theme={null}
curl --fail-with-body -X PUT -H 'Expect:' \
  --data-binary @context.tar.gz "$(jq -r '.upload_url' build.json)"
```

### 3. Start the build

The body is optional. Send one when a `RUN` step needs a credential or `FROM` pulls from a private registry; both are used for this build only and are not stored.

<ParamField body="secrets" type="object">
  Build secrets, `{ "<id>": "<value>" }`. Each id (1 to 64 letters, digits, `_`, `.` or `-`, starting with a letter or `_`) is mounted for `RUN --mount=type=secret,id=<id>` steps at `/run/secrets/<id>` during that step only and never written into the image. Up to 32 entries, 256 KB in total.
</ParamField>

<ParamField body="registry_auth" type="object">
  Registry logins for `FROM`, keyed by host: `{ "registry.acme.com": { "username": "...", "password": "..." } }`. Up to 8 registries.
</ParamField>

```bash curl theme={null}
BUILD_ID=$(jq -r '.id' build.json)

curl --fail-with-body -sS -X POST \
  "https://api.agent37.com/v1/template-builds/$BUILD_ID/start" \
  -H "Authorization: Bearer $AGENT37_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"secrets\": { \"GITHUB_TOKEN\": \"$GITHUB_TOKEN\" },
    \"registry_auth\": { \"registry.acme.com\": { \"username\": \"bot\", \"password\": \"$REGISTRY_PASSWORD\" } }
  }"
```

```json response theme={null}
{ "id": "tb_7d3a90b81f264bc2a113", "status": "building" }
```

Starting a build whose context was never uploaded, or is over 1 GB, returns `400 invalid_request`; so does a malformed `secrets` or `registry_auth`. Up to three builds run at once per workspace: a build that already started, or a start while three are already running, returns `409 build_conflict`.

### 4. Poll it and tail the log

`GET /v1/template-builds/{id}` returns the build; `GET /v1/template-builds/{id}/logs?offset=N` returns the same fields plus a chunk of build log starting at `offset`. Pass the returned `offset` back to tail incrementally. Poll rather than wait silently: the poll that finds the build finished is what triggers template registration (an unpolled build is still completed by a platform sweep within about five minutes).

```bash curl theme={null}
curl -sS "https://api.agent37.com/v1/template-builds/$BUILD_ID/logs?offset=0" \
  -H "Authorization: Bearer $AGENT37_API_KEY"
```

```json response theme={null}
{
  "id": "tb_7d3a90b81f264bc2a113",
  "name": "my-agent",
  "status": "succeeded",
  "template": {
    "name": "my-agent",
    "revision": 1,
    "image_digest": "sha256:9b2e...c41f"
  },
  "created": 1784190000,
  "started": 1784190010,
  "finished": 1784190190,
  "offset": 5120,
  "data": "#8 exporting layers\n#8 DONE 1.2s\n"
}
```

<ResponseField name="status" type="string">
  `created` (waiting for the context and `start`), `building` (the Docker build is running), `ingesting` (build done; the image is being copied into private storage), `succeeded`, or `failed`.
</ResponseField>

<ResponseField name="error" type="object">
  On `failed`, the reason as `{ "code", "message" }`. See [the failure codes](#build-limits-and-failures).
</ResponseField>

<ResponseField name="template" type="object">
  On `succeeded`, the published result: the template `name`, its new `revision`, and the pinned `image_digest`.
</ResponseField>

<ResponseField name="created" type="integer">
  When the build was created, in epoch seconds.
</ResponseField>

<ResponseField name="started" type="integer">
  When the build started, in epoch seconds. Set once it starts.
</ResponseField>

<ResponseField name="finished" type="integer">
  When the build finished, in epoch seconds. Set once it succeeds or fails.
</ResponseField>

<ResponseField name="offset" type="integer">
  Logs endpoint only: the next offset to request.
</ResponseField>

<ResponseField name="data" type="string">
  Logs endpoint only: the log chunk starting at your offset, the real Docker build output. It stays readable after the build finishes.
</ResponseField>

### Build limits and failures

Builds are free. Up to three run at once per workspace, a build times out after 45 minutes, and the gzipped context is capped at 1 GB. The built image is capped at 8 GB decimal, like a registry import; if you need more, [talk to the team](https://cal.com/vishnukool/30min).

Your Dockerfile and files upload straight to the build, so a private codebase never needs a registry. A `FROM` on a private registry takes its login as `registry_auth` on the start call, and a `RUN` step that needs a token (a private repository, a private package index) reads it from a build secret passed the same way; see [build secrets](/docs/agents-api/custom-image#build-secrets). A template published by a build has an `image_digest` and no `image_ref`, because no public reference exists.

A failed build carries its reason in `error.code`:

| Code                    | What happened                                                                                                                     |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `build_failed`          | A Dockerfile step failed; the failing step is at the end of the build log                                                         |
| `build_timeout`         | The build ran past the 45-minute limit                                                                                            |
| `invalid_request`       | The context is unusable: not a gzipped tar, or no `Dockerfile` at its root                                                        |
| `invalid_image_archive` | The built image failed validation: not a readable single `linux/amd64` image                                                      |
| `image_too_large`       | The built image is over the 8 GB decimal cap                                                                                      |
| `image_ingest_failed`   | The copy into private storage failed for a reason on our side; re-run the build, or [contact support](mailto:support@agent37.com) |
| `internal_error`        | The build failed for a reason on our side; re-run the build, or [contact support](mailto:support@agent37.com)                     |

## Register a workspace template

`POST /v1/templates` registers an amd64 image under a name you choose. The platform runs it as a managed instance with the same lifecycle, billing, [exec](/docs/agents-api/exec), and routed port URLs as the catalog; what the image serves on its ports is up to it. The image can be anything, built from scratch or on [the Hermes base image](#build-on-the-hermes-base-image). Give the image as an `image_ref`; for a private registry, add `registry_auth` beside it. An image that exists nowhere yet is a [cloud build](#build-an-image-in-the-cloud) instead, which publishes the template itself, so there is nothing to register afterward. If you need an image larger than the 8 GB cap allows, [talk to the team](https://cal.com/vishnukool/30min).

<ParamField body="name" type="string" required>
  2 to 63 characters: lowercase letters, digits, and hyphens, starting with a letter (`^[a-z][a-z0-9-]{1,62}$`). The `agent37-` prefix is reserved. A name that already exists returns `409 template_conflict`.
</ParamField>

<ParamField body="image_ref" type="string" required>
  A fully qualified image reference with a registry or namespace path, like `ghcr.io/acme/my-agent:v1`. Up to 255 characters. The image must be built for `linux/amd64` and at most 8 GB, and either public or reachable with `registry_auth`. On registration the platform pulls it once into private storage and pins it by digest; a reference it cannot pull, one that isn't `linux/amd64`, or one over the cap is rejected then.
</ParamField>

<ParamField body="registry_auth" type="object">
  The login for the image's registry when it is private: `{ "username": "...", "password": "..." }` (on GHCR, a personal access token with `read:packages` as the password). The platform uses it once for the copy into private storage and does not store it, so a later `PATCH` that changes `image_ref` sends it again. Not accepted for `ghcr.io/agent37-platform/` images, which are public.
</ParamField>

<ParamField body="description" type="string">
  Optional free-text description.
</ParamField>

<ParamField body="default_port" type="integer">
  The port the bare instance URL (`https://{instanceId}.agent37.app`) routes to, and the port the platform probes at boot to decide the create succeeded. Your service must bind it on `0.0.0.0`, not only localhost. An integer 1 to 65535; the reserved ports `7681`, `8080`, `6080`, `7890`, `9119`, and `22022` ([SSH](/docs/agents-api/ssh)) are rejected (they belong to the managed runtime). Omitted, the bare URL falls back to `3737` (right for images built on [the Hermes base image](#build-on-the-hermes-base-image), whose gateway serves it) and no TCP boot probe runs. A private sandbox can listen on nothing and be driven by [exec](/docs/agents-api/exec), but its main process must still stay running. Every other port needs no declaration: it is already reachable at `https://{instanceId}-{port}.agent37.app`. With [auto-sleep](/docs/agents-api/instances#auto-sleep) on, a sandbox nothing ever requests only wakes by an explicit [`start`](/docs/agents-api/instances#start).
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.agent37.com/v1/templates \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "name": "my-agent",
      "image_ref": "ghcr.io/acme/my-agent:v1",
      "description": "My agent",
      "default_port": 8000
    }'
  ```

  ```python python theme={null}
  import requests

  resp = requests.post(
      "https://api.agent37.com/v1/templates",
      headers={"Authorization": "Bearer sk_live_..."},
      json={
          "name": "my-agent",
          "image_ref": "ghcr.io/acme/my-agent:v1",
          "description": "My agent",
          "default_port": 8000,
      },
  )
  print(resp.json())
  ```

  ```javascript node theme={null}
  const resp = await fetch("https://api.agent37.com/v1/templates", {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_live_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "my-agent",
      image_ref: "ghcr.io/acme/my-agent:v1",
      description: "My agent",
      default_port: 8000,
    }),
  });
  console.log(await resp.json());
  ```
</CodeGroup>

Registration copies the image into private storage before it returns, so `POST` and `PATCH` are synchronous and can take up to a few minutes for a large image. A reference the platform cannot pull (private without `registry_auth`, or a wrong login), one that isn't `linux/amd64`, or one over the cap is rejected at registration with `400 invalid_request` or `400 image_too_large`, so a broken image never reaches instance create.

<Tip>
  Pin a tag on `image_ref`; do not rely on `latest`. A pinned tag makes rollback a one-field `PATCH` and keeps every instance you create reproducible. The template is a snapshot regardless; pinning is about which build you re-register next.
</Tip>

<Note>
  Build for `linux/amd64` even on an Apple Silicon Mac (`docker build --platform linux/amd64 ...`). An arm64 image is rejected during registration.
</Note>

## Build on the Hermes base image

[`ghcr.io/agent37-platform/hermes-base`](https://github.com/orgs/agent37-platform/packages/container/package/hermes-base) is the published FROM target for custom templates. It ships Hermes with its browser stack (Chromium, Playwright), the gateway that serves the [chat API](/docs/agents-api/chat), and a general toolchain: git, Python 3 with uv, Node.js, build tools. The image itself wires no model and no integrations; you add your layers on top. It does not need to carry credentials, because the platform puts a working LLM endpoint and a Composio MCP server in every instance's environment; see [Managed services in your image](/docs/agents-api/managed-services). Point your agent at those, or bring your own model keys.

```dockerfile theme={null}
FROM ghcr.io/agent37-platform/hermes-base:latest

USER root
RUN apt-get update && apt-get install -y postgresql-client
USER node
```

Publish it with a [cloud build](#build-an-image-in-the-cloud) (`npx agent37 templates build` from the folder holding the Dockerfile), or build it yourself for `linux/amd64` and push it to a registry. OpenClaw builds have the same shape: the FROM target is [`ghcr.io/agent37-platform/openclaw-base`](https://github.com/orgs/agent37-platform/packages/container/package/openclaw-base), published with the same dated tags as `agent37-openclaw`. So do [Claude Code](/docs/agents-api/claude-code) builds: the FROM target is [`ghcr.io/agent37-platform/claude-code-base`](https://github.com/orgs/agent37-platform/packages/container/package/claude-code-base), the clean base with no managed integrations, published with the same dated tags as `agent37-claude-code`. [Codex](/docs/agents-api/codex) builds follow suit: the FROM target is [`ghcr.io/agent37-platform/codex-base`](https://github.com/orgs/agent37-platform/packages/container/package/codex-base), published with the same dated tags as `agent37-codex`. [Grok](/docs/agents-api/grok) builds as well: the FROM target is [`ghcr.io/agent37-platform/grok-base`](https://github.com/orgs/agent37-platform/packages/container/package/grok-base), published with the same dated tags as `agent37-grok`. [OpenCode](/docs/agents-api/opencode) builds too: the FROM target is [`ghcr.io/agent37-platform/opencode-base`](https://github.com/orgs/agent37-platform/packages/container/package/opencode-base), published with the same dated tags as `agent37-opencode`.

See [Build a custom image](/docs/agents-api/custom-image) for the step-by-step walkthrough, and [agent37-platform/custom-agent-image](https://github.com/agent37-platform/custom-agent-image) for a working app that builds its own image and installs a skill into every agent it creates.

The contract:

* Your built image freezes the base it was built from; the FROM tag only matters when you rebuild. `latest` tracks the newest base, so getting-started builds never go stale. For reproducible rebuilds, pin a date tag: `hermes-base` publishes the same dated tags as `agent37-hermes` ([list on GHCR](https://github.com/orgs/agent37-platform/packages/container/package/hermes-base)), and the current one is the tag in `agent37-hermes`'s `image_ref` on `GET /v1/templates`.
* Keep the entrypoint. It starts Hermes and the gateway; a Dockerfile that overrides `ENTRYPOINT` loses the chat API.
* Bake outside `/home/node` and `/home/linuxbrew`. Both are persistent volumes, so anything the image writes there is masked at runtime. Use `/usr/local` or `/opt`.
* An image ships binaries, not skills. Hermes reads skills from `~/.hermes/skills`, on that same masked volume, so write them into a running instance over [exec](/docs/agents-api/exec) or the [files API](/docs/agents-api/files) instead, once per instance, at whatever point your app creates one. They persist across restarts and updates.
* The image runs as the `node` user, and `sudo` does not elevate at runtime (the sandbox ignores setuid). Switch to `USER root` for installs in the Dockerfile and back to `USER node` at the end; on a running instance, get root with [`ssh root@<id>`](/docs/agents-api/ssh).
* Leave the default port to the gateway. The runtime tells the gateway which port to bind (`default_port`, or `3737` when you omit it), so the bare instance URL serves the chat API; do not bind your own service there. Your own services need no declaration: each is reachable at `https://{instanceId}-{port}.agent37.app`.
* Bring your own model keys. Instances boot with no LLM provider configured, and chat returns errors until you add one. Write your provider credentials into `~/.hermes/config.yaml`, Hermes' standard config file, over [exec](/docs/agents-api/exec) or the terminal. The file lives on the persistent volume, so it survives restarts and updates.

## Update a template

`PATCH /v1/templates/{name}` changes any of `name`, `image_ref`, `description`, or `default_port` (send `null` to clear it back to the `3737` fallback). Send at least one field. A changed `image_ref` is copied into private storage (send `registry_auth` again for a private registry), so an image update is synchronous like create, and each successful image update increments the platform-controlled `revision`. Sending the exact same `image_ref` string reuses the existing snapshot and does not increment it; renaming or changing only `description` or `default_port` also leaves it unchanged. To ship a new image on a template published by a [cloud build](#build-an-image-in-the-cloud) (no `image_ref`), run a new build under the same name; every successful build publishes a new revision, even when the built image's digest is unchanged. Renaming onto an existing name returns `409 template_conflict`; system templates return `403`.

```bash curl theme={null}
curl -X PATCH https://api.agent37.com/v1/templates/my-agent \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "image_ref": "ghcr.io/acme/my-agent:v2" }'
```

To roll back, [pin instances to the earlier revision](#pin-a-template-version) with `POST /v1/instances/{id}/update` and `{ "template": "my-agent@1" }`. No re-publish needed. To make an old release the template's *current* one again, point `image_ref` at the older tag (or, for a build-born template, re-run the [build](#build-an-image-in-the-cloud) from the older source); that publishes it as a new revision. Existing instances keep running on the image they already pulled; [update each instance](/docs/agents-api/instances) to recreate it from the template's current image.

Workspace templates start at `revision: 1`. Compare that value with an instance's `template_revision` to find instances that have not installed the current template release.

## Roll out a release to your fleet

After an image update, list your instances and [update](/docs/agents-api/instances#update) the ones still on an older revision. An instance's `template_revision` is what it has installed; the template's `revision` is the current release. Missing instance revisions read as `1`.

```bash curl theme={null}
TEMPLATE=my-agent
REV=$(curl -s https://api.agent37.com/v1/templates/$TEMPLATE \
  -H "Authorization: Bearer sk_live_..." | jq .revision)

curl -s https://api.agent37.com/v1/instances \
  -H "Authorization: Bearer sk_live_..." |
  jq -r --arg t "$TEMPLATE" --argjson r "$REV" \
    '.data[] | select(.template == $t and (.template_revision // 1) != $r) | .id' |
  head -n 3 |
  xargs -I{} curl -s -X POST "https://api.agent37.com/v1/instances/{}/update" \
    -H "Authorization: Bearer sk_live_..."
```

The `head -n 3` is the canary: update a small slice first, verify the release on those instances, then rerun without it to finish the fleet. Rerunning is always safe, the filter only picks up instances still behind. A `sleeping` instance cannot be updated; wake it with [`start`](/docs/agents-api/instances#start) first.

## Delete a template

`DELETE /v1/templates/{name}` removes the workspace template and acts once: the first call returns `200` with `{ "name": "my-agent", "deleted": true }`, and a repeat returns `404`. System templates return `403`. Existing instances created from it keep running, so deletion does not immediately purge the private image copy they still reference. Contact support if you need the stored image purged after its instances are gone.

```bash curl theme={null}
curl -X DELETE https://api.agent37.com/v1/templates/my-agent \
  -H "Authorization: Bearer sk_live_..."
```

## The template object

```json response theme={null}
{
  "name": "agent37-hermes",
  "scope": "system",
  "agents": ["hermes"],
  "revision": null,
  "image_ref": "ghcr.io/agent37-platform/hermes:2026.07.02b",
  "description": "Hermes general agent: browser, code, files.",
  "default_port": 3737,
  "created": null,
  "updated": null
}
```

A workspace template registered from a registry reference echoes that reference as `image_ref` and adds the pinned `image_digest`; one published by a [cloud build](#build-an-image-in-the-cloud) has an `image_digest` and no `image_ref`:

```json response theme={null}
{
  "name": "my-agent",
  "scope": "workspace",
  "agents": [],
  "revision": 2,
  "image_digest": "sha256:9b2e...c41f",
  "description": "My agent",
  "default_port": 8000,
  "created": 1752460800,
  "updated": 1752547200,
  "revisions": [
    { "revision": 2, "image_digest": "sha256:9b2e...c41f", "created": 1752547200 },
    { "revision": 1, "image_digest": "sha256:41d8...77aa", "created": 1752460800 }
  ]
}
```

<ResponseField name="name" type="string">
  The name you pass as `template` when creating an instance.
</ResponseField>

<ResponseField name="scope" type="string">
  `system` for built-in `agent37-` templates, `workspace` for ones you register.
</ResponseField>

<ResponseField name="agents" type="string[]">
  The agents the template installs: `["hermes"]` on `agent37-hermes`, `["claude-code"]` on `agent37-claude-code`, `["codex"]` on `agent37-codex`, `["grok"]` on `agent37-grok`, `["opencode"]` on `agent37-opencode`, and `[]` on `agent37-n8n`, which runs a web app rather than an agent. Workspace templates report `[]`, because the platform does not inspect what your image installs.
</ResponseField>

<ResponseField name="revision" type="integer | null">
  The platform-controlled workspace image revision. It starts at `1` and increments for a changed `image_ref` or every successful [cloud build](#build-an-image-in-the-cloud), even when the digest is unchanged. Metadata-only updates and the same exact `image_ref` leave it unchanged. `null` for system templates.
</ResponseField>

<ResponseField name="revisions" type="array">
  Every published release of a workspace template, newest first: `{ "revision": 2, "image_digest": "sha256:...", "created": 1752547200 }`. [Pin one](#pin-a-template-version) with `name@<revision>` on instance create or update. Returned on `GET /v1/templates/{name}` only; absent from the list endpoint and from system templates. Templates that predate release history list only the revisions published since.
</ResponseField>

<ResponseField name="image_ref" type="string">
  For system templates and workspace templates registered from a registry, the image reference. **Absent** on a workspace template published by a [cloud build](#build-an-image-in-the-cloud), whose image lives only in Agent37's private storage, identified by `image_digest`.
</ResponseField>

<ResponseField name="image_digest" type="string">
  On workspace templates, the `sha256:...` digest of the private snapshot instances run, the immutable identity of the registered image. Absent on system templates.
</ResponseField>

<ResponseField name="description" type="string | null">
  Your optional description.
</ResponseField>

<ResponseField name="default_port" type="integer | null">
  The declared default port, or `null` when the template relies on the `3737` fallback. See [Instance and preview URLs](/docs/agents-api/urls).
</ResponseField>

<ResponseField name="created" type="integer | null">
  Creation time in epoch seconds. `null` for system templates.
</ResponseField>

<ResponseField name="updated" type="integer | null">
  Last update time in epoch seconds. `null` for system templates.
</ResponseField>

## Errors

| Status | Code                  | When                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid_request`     | Bad `name`, `image_ref`, or `default_port`; a template create with no `image_ref`; a registry reference the platform cannot pull (private without `registry_auth`, or a wrong login); a malformed `registry_auth`; the legacy `ports` array; a direct image reference passed as `template` on instance create; a bad [pin](#pin-a-template-version) on create or update (unpublished system tag, `@latest`, non-numeric or unpublished workspace revision); a [build](#build-an-image-in-the-cloud) `start` with no uploaded context, one over 1 GB, or a malformed `secrets` or `registry_auth` body |
| `400`  | `image_too_large`     | The staged image is over the 8 GB decimal cap                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `403`  | `forbidden`           | `PATCH` or `DELETE` on a system template: they are read-only                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `404`  | `not_found`           | Unknown template name or build id; a pinned `GET` for an unpublished version or revision; repeat `DELETE`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `409`  | `template_conflict`   | Create or rename onto a name that already exists                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `409`  | `build_conflict`      | A build `start` on a build that already started, or while three builds are already running in your workspace                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `502`  | `image_ingest_failed` | The copy into private storage failed for a reason on our side; retry, or [contact support](mailto:support@agent37.com)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

A build that fails after starting is not an HTTP error: `GET /v1/template-builds/{id}` returns `200` with `status: "failed"` and the reason in its `error` field. See [build limits and failures](#build-limits-and-failures).

See [Errors](/docs/agents-api/errors) for the envelope and the full catalog.
