Skip to main content
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, 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
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, and they are 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

The agent37- prefix is reserved for system templates. The available templates are: Every template offers the same four CPU/RAM shapes. Most templates default to 2 vCPU / 4 GB on the default instance type at $4.76 per month. The VM-only templates have different defaults and pricing: agent37-ubuntu defaults to 2 vCPU / 4 GB / 12 GB disk, and agent37-android to 4 vCPU / 8 GB / 12 GB disk. See sizing and VM pricing. agent37-claude-code runs on your own Anthropic account: connect it before the first chat turn; see Host Claude Code. agent37-codex runs on your own OpenAI account, same rule; see Host Codex. agent37-grok runs on your own xAI API key, same rule; see Host 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. agent37-pi is the same out of the box, and takes a provider key or a Claude, ChatGPT or Copilot subscription when you want your own; see Host Pi. 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. agent37-ubuntu is a whole machine rather than a sandbox: create it with "type": "vm", and get in over SSH or through its desktop; see VM instances. agent37-android is a separate image with Android, ADB, and a browser screen, without the Ubuntu template’s Docker daemon or XFCE desktop; see Host an Android phone.
Passing any other agent37- name on instance create returns 400 invalid_request, because the prefix is reserved for system templates.
On agent harness templates, port 3737 is the gateway: the chat API 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. The three app and VM templates have no Agent API: agent37-n8n serves its editor on port 5678 through a public URL, agent37-android serves its authenticated screen on port 6901, and agent37-ubuntu has no default service port. Use exec or SSH to run commands on these instances. 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. Every system template is built from a public Dockerfile in agent37-platform/templates, so you, or your coding agent, can read exactly what an instance runs: what the image installs and how its entrypoint configures the agent at boot. Each published version is a git tag there named <key>/<tag>, for example b2b-hermes/<tag> for agent37-hermes@<tag>; the repo’s README maps every template to its key. main can be ahead of what your instances run, so read the tag in your instance’s image.

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
Every release is an immutable dated tag, never republished, so a pin is a freeze. The published tags are on GHCR: hermes, openclaw, claude-code, codex, 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) 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, 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.10.05a", "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

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 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:
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. 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:
string
required
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.
integer
required
Exact size of the gzipped context, from 1 through 1,000,000,000 bytes (1 GB).
integer
optional
Applied to the published template, with the same semantics as on POST /v1/templates.
string
optional
Optional free-text description for the published template.
Pack the context first, then choose a client below. The curl examples also use jq; all three clients read AGENT37_API_KEY from your environment and save the response to build.json for the upload and start steps.
response
string
The workspace-scoped build id, tb_ followed by 20 lowercase hex characters.
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.
integer
When upload_url expires, in epoch seconds.
integer
The context cap: exactly 1,000,000,000 bytes (1 GB).

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:

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.
object
optional
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.
object
optional
Registry logins for FROM, keyed by host: { "registry.acme.com": { "username": "...", "password": "..." } }. Up to 8 registries.
Set GITHUB_TOKEN and REGISTRY_PASSWORD in your environment and replace registry.acme.com with your registry. Omit either object when the build does not need it.
response
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. If no build machine came up, the start returns 503 try_again and the build is marked failed: upload the context to a new build and start that one.

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). Read the build status:
Read a log chunk, starting at byte 0 on the first request:
response
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.
object
On failed, the reason as { "code", "message" }. See the failure codes.
object
On succeeded, the published result: the template name, its new revision, and the pinned image_digest.
integer
When the build was created, in epoch seconds.
integer
When the build started, in epoch seconds. Set once it starts.
integer
When the build finished, in epoch seconds. Set once it succeeds or fails.
integer
Logs endpoint only: the next offset to request.
string
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. 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. 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. 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; for a private registry, add registry_auth beside it. An image that exists nowhere yet is a cloud build 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.
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.
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.
object
optional
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.
string
optional
Optional free-text description.
integer
optional
The port the bare instance URL (https://{instanceId}.agent37.app) routes to. Your service must bind it on 0.0.0.0, not only localhost. Creation never waits for it to open, so poll health (or your own endpoint) to know when your service is up. An integer 1 to 65535. Only 22022 (SSH) is rejected, because the platform injects its own SSH server there; every other port is yours, 8080 and 3000 included. Omitted, the bare URL falls back to 3737 (right for images built on the Hermes base image, whose gateway serves it). 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.
string[]
optional
The command the instance runs, replacing the image’s own ENTRYPOINT and CMD. Exactly what docker run --entrypoint does. An array of arguments, not a shell line: ["/bin/sh", "-c", "node server.js"]. Omitted, the image decides, which is what you want for an image you built. Set it to run a stock image you did not build, like node:24-slim, whose own command opens a REPL and exits. Changing it publishes a new revision, so a pin freezes the command along with the image.
The example includes private-registry credentials and a command override. Use your published tag and registry token; omit registry_auth for a public image and entrypoint to keep the image’s own command. If you use the override, /app/start.sh must exist in your image.

Run a stock image

An image you did not build usually starts the wrong thing. node:24-slim runs node, which opens a REPL and exits, so an instance from it would never serve anything. entrypoint replaces that without forking the image:
Instances from this template boot /app/start.sh, which you put on the instance’s disk with exec or SSH. Because the command belongs to the template rather than to a one-off call, it runs again on every start, restart, and wake from auto-sleep. 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.
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.
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. 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. Point your agent at those, or bring your own model keys. Its source is images/b2b/hermes/Dockerfile (--target base).
Publish it with a cloud build (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, published with the same dated tags as agent37-openclaw. So do Claude Code builds: the FROM target is ghcr.io/agent37-platform/claude-code-base, the clean base with no managed integrations, published with the same dated tags as agent37-claude-code. Codex builds follow suit: the FROM target is ghcr.io/agent37-platform/codex-base, published with the same dated tags as agent37-codex. Grok builds as well: the FROM target is ghcr.io/agent37-platform/grok-base, published with the same dated tags as agent37-grok. OpenCode builds too: the FROM target is ghcr.io/agent37-platform/opencode-base, published with the same dated tags as agent37-opencode. Pi as well: the FROM target is ghcr.io/agent37-platform/pi-base, published with the same dated tags as agent37-pi. See Build a custom image for the step-by-step walkthrough, and 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), 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. It is a persistent volume, 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 or the files API 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>.
  • 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 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, default_port (send null to clear it back to the 3737 fallback), or entrypoint (send null to go back to the image’s own command). 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. A changed entrypoint does increment it, since it changes what the template runs. 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. This example shows every editable field and the optional registry credentials. Send only the fields you want to change, using a new published tag when updating the image.
To roll back, pin instances to the earlier revision 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 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’s template_revision is what it has installed; the template’s revision is the current release. Missing instance revisions read as 1.
curl
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 is woken by the update and comes back running. That is the only way it picks up the release: waking a sleeper on its own restores it on the image it fell asleep with.

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.

The template object

response
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 has an image_digest and no image_ref:
response
string
The name you pass as template when creating an instance.
string
system for built-in agent37- templates, workspace for ones you register.
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, ["pi"] on agent37-pi, 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.
integer | null
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.
array
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.
string
For system templates and workspace templates registered from a registry, the image reference. Absent on a workspace template published by a cloud build, whose image lives only in Agent37’s private storage, identified by image_digest.
string
On workspace templates, the sha256:... digest of the private snapshot instances run, the immutable identity of the registered image. Absent on system templates.
string | null
Your optional description.
integer | null
The declared default port, or null when the template relies on the 3737 fallback. See Instance and preview URLs.
string[] | null
The command that replaces the image’s own, or null when the image decides. A pinned read reports the command that revision was published with, not the current one.
integer | null
Creation time in epoch seconds. null for system templates.
integer | null
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.