template on POST /v1/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.
curl
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 — a public registry reference or a cloud build — visible only to your workspace.
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.
System catalog
Theagent37- prefix is reserved for system templates. The catalog has three entries today:
agent37-hermes-small is the only template that can run on the sub-floor 1 vCPU / 3 GB shape, from $3.44 per month; see sizing. Standard and custom templates start at 2 vCPU / 4 GB.
More system templates (Claude Code, Codex) are coming; they will appear in
GET /v1/templates when they are available. Passing any other agent37- name on instance create returns 400 invalid_request, because the prefix is reserved.3737 is the gateway: the chat API at https://{instanceId}.agent37.app. Any other port an instance serves — the built-in dashboard, terminal, and file browser included — is reachable at its derivable https://{instanceId}-{port}.agent37.app URL; see Instance and preview URLs.
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.
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.
curl
image_ref on GET /v1/templates. An unpublished tag returns 400 invalid_request, and so does @latest — 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) appends a release, GET /v1/templates/{name} lists them under revisions, and my-agent@2 freezes revision 2. GET /v1/templates/my-agent@2 probes one the same way as a system tag — 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, 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
curl
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 (aDockerfile plus the files it COPYs) 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:
. 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 (100 MB 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 — 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.
See Build a 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:
The template the build publishes, following the same rules as template names. A new name is created at revision
1 when the build succeeds; an existing name gets a new revision.Exact size of the gzipped context, from
1 through 100,000,000 bytes (100 MB).Applied to the published template, with the same semantics as on
POST /v1/templates.Optional free-text description for the published template.
curl
response
The workspace-scoped build id,
tb_ followed by 20 lowercase hex characters.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.When
upload_url expires, in epoch seconds.The context cap: exactly
100,000,000 bytes (100 MB).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:
curl
3. Start the build
curl
response
400 invalid_request. One build runs at a time per workspace: a build that already started, or a start while another build is 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).
curl
response
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.On
failed, the reason as { "code", "message" }. See the failure codes.On
succeeded, the published result: the template name, its new revision, and the pinned image_digest.When the build was created, in epoch seconds.
When the build started, in epoch seconds. Set once it starts.
When the build finished, in epoch seconds. Set once it succeeds or fails.
Logs endpoint only: the next offset to request.
Logs endpoint only: the log chunk starting at your offset — the real Docker build output. It stays readable after the build finishes.
Build limits and failures
Builds are free. One build runs at a time per workspace, a build times out after 45 minutes, and the gzipped context is capped at 100 MB. The built image is capped at 8 GB decimal like a registry import, though built images practically top out around 4 GB today; if you need more, talk to the team. Everything the build fetches — theFROM base image, anything RUN downloads — must be publicly reachable. There is no build-secret or private-registry-credential support yet; for a private base image, contact support. 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:
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, 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. Give the image as an image_ref, a public registry reference; for an image that isn’t public, use a cloud build instead — it 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.
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.A fully qualified public image reference with a registry or namespace path, like
ghcr.io/acme/my-agent:v1. Up to 255 characters. The image must be public (so the platform can copy it), built for linux/amd64, and at most 8 GB. On registration the platform pulls it once into private storage and pins it by digest; a reference that isn’t publicly pullable, isn’t linux/amd64, or is over the cap is rejected then.Optional free-text description.
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 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, whose gateway serves it — and no TCP boot probe runs. A private sandbox can listen on nothing and be driven by 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 on, a sandbox nothing ever requests only wakes by an explicit start.POST and PATCH are synchronous and can take up to a few minutes for a large image. A reference that isn’t publicly pullable, isn’t linux/amd64, or is over the cap is rejected at registration — 400 invalid_request or 400 image_too_large — so a broken image never reaches instance create.
Build for
linux/amd64 even on an Apple Silicon Mac (docker build --platform linux/amd64 ...). An arm64 image is rejected during registration.Build on the Hermes base image
ghcr.io/agent37-platform/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, and a general toolchain: git, Python 3 with uv, Node.js, build tools. There are no platform LLM credentials and no managed search or Composio integrations; you add your layers on top and bring your own model keys.
npx agent37 templates build from the folder holding the Dockerfile — or build it yourself for linux/amd64 and push it to a public registry. OpenClaw builds have the same shape: the FROM target is ghcr.io/agent37-platform/openclaw-base, published with the same dated tags as agent37-openclaw.
See Build a custom image for the step-by-step walkthrough, with a copy-able example — a Dockerfile, an example skill, a registration script, and a tiny bring-your-own-model proxy.
The contract:
- Your built image freezes the base it was built from; the FROM tag only matters when you rebuild.
latesttracks the newest base, so getting-started builds never go stale. For reproducible rebuilds, pin a date tag:hermes-basepublishes the same dated tags asagent37-hermes(list on GHCR), and the current one is the tag inagent37-hermes’simage_refonGET /v1/templates. - Keep the entrypoint. It starts Hermes and the gateway; a Dockerfile that overrides
ENTRYPOINTloses the chat API. - Bake outside
/home/nodeand/home/linuxbrew. Both are persistent volumes, so anything the image writes there is masked at runtime. Use/usr/localor/opt. Skills placed in/usr/local/share/agent37/default-skills/are copied into~/.hermes/skillsat boot. - The image runs as the
nodeuser, with passwordless sudo. Switch toUSER rootfor installs and back toUSER nodeat the end. - Leave the default port to the gateway. The runtime tells the gateway which port to bind —
default_port, or3737when 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 athttps://{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 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, 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 (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.
curl
POST /v1/instances/{id}/update with { "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 from the older source); that publishes it as a new revision. Existing instances keep running on the image they already pulled; update each instance 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 the ones still on an older revision. An instance’stemplate_revision is what it has installed; the template’s revision is the current release. Missing instance revisions read as 1.
curl
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 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.
curl
The template object
response
image_ref and adds the pinned image_digest; one published by a cloud build has an image_digest and no image_ref:
response
The name you pass as
template when creating an instance.system for built-in agent37- templates, workspace for ones you register.The agents the template installs:
["hermes"] on agent37-hermes. Workspace templates report [] — the platform does not inspect what your image installs.The platform-controlled workspace image revision. It starts at
1 and increments for a changed image_ref or every successful cloud build, even when the digest is unchanged. Metadata-only updates and the same exact image_ref leave it unchanged. null for system templates.Every published release of a workspace template, newest first:
{ "revision": 2, "image_digest": "sha256:...", "created": 1752547200 }. Pin one 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.For system templates and workspace templates registered from a registry, the image reference. Absent on a workspace template published by a cloud build — its image lives only in Agent37’s private storage, identified by
image_digest.On workspace templates, the
sha256:... digest of the private snapshot instances run — the immutable identity of the registered image. Absent on system templates.Your optional description.
The declared default port, or
null when the template relies on the 3737 fallback. See Instance and preview URLs.Creation time in epoch seconds.
null for system templates.Last update time in epoch seconds.
null for system templates.Errors
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.
See Errors for the envelope and the full catalog.