Skip to main content
OpenAI launched Dots on September 29, 2026: an always-on agent in ChatGPT with its own cloud computer, the first one included with a Pro or Business Premium plan. You hand it a responsibility instead of a prompt, and it keeps working between conversations, decides when to check back, and messages you when something needs you. This guide builds the same loop on Agent37: one instance per user, a persona that tells the agent it may schedule itself, platform crons that wake it, and a callback into your app for the messages it sends first.
Paste this into your coding agent

dots: this guide as a working app

Everything on this page, runnable: name an agent and pick a mascot and color, connect apps, chat with a live status line, watch Scheduled and Completed fill up as it schedules itself, get its first messages as notifications, and, with the desktop template, watch its computer and take it over. Express plus vanilla JS, no build step. Clone it, add your key, npm start.

One computer per user, and it keeps its own schedule

Each user’s agent is one instance: a persistent computer running Hermes, with its own disk, memory, and connected apps. Three things turn it from a chat app into something that owns work:
  1. It schedules itself. Every Agent37 agent image ships the agent37 cron CLI, which creates ordinary platform crons with the credential the instance already holds. A cron fires whether the instance is awake or asleep, so the agent can sleep between check-ins and still show up on time.
  2. Its persona says it may. The gateway tells the agent it cannot follow up once a reply ends. That is true of a single reply, so the persona you write into ~/.hermes/SOUL.md says how it does follow up: a scheduled check-in.
  3. It can reach the user. A check-in runs in a fresh session that nobody is watching. The agent calls your server with a token you planted at create, and your app shows the message.
1

Create the user's agent

POST /v1/instances with four things beyond the usual: a monthly budget (the agent spends on its own schedule, so a cap that resets suits it better than one-time credit), auto_sleep so it bills disk alone between check-ins, a random callback token in env, and that token’s SHA-256 in metadata.
The call returns once the computer is up. Poll GET /v1/health on the instance URL until it answers "healthy": true before writing files or sending the first message. env is write-only and fixed at create, so the raw token lives only inside the instance; the hash in metadata is what your server reads back later. The idle timeout sits above the longest turn you expect, because a turn whose browser went away can be checkpointed mid-run once the instance goes idle.
2

Give it a name and a job description

Hermes reads its persona from ~/.hermes/SOUL.md on the instance’s disk. A fresh instance already has one, so write it read-merge-write with the Files API: read the file, replace a block you own (marked so you can find it again when the user renames the agent), and write the whole file back. PUT replaces the file with the body you send, so never PUT a fragment.
The persona is where the product lives. The part that makes it own work:
the persona block, abridged
Seed what you already know about the user into ~/.hermes/memories/USER.md the same way (entries are separated by a line holding a single §). The app shows ~/.dots/responsibilities.md as the agent’s In progress list.
3

Connect their apps

Onboarding offers the user’s apps next, through managed Composio on their own instance: list the catalog, start a connection with a callbackUrl back into your app, and poll the connections list until the new account reads ACTIVE.
Open redirectUrl in a tab you opened during the click itself; a window opened after an await is blocked as a popup. The catalog includes no-auth entries (isNoAuth: true) that need no connect step, so filter those out of an onboarding grid.
4

Let it introduce itself

Dots speaks first. Send the first turn yourself, with an app brief as the input, and hide it when you render history. The gateway has no system-prompt field on POST /v1/responses, so app context rides as a marked preamble:
the hidden first turn
When you render a session, drop user messages that start with the marker, or show only the text after End of app context. when the user’s own words follow it. Store the session id the first stream event returns; see the next step.
5

Stream chat with a live status line

Send turns with stream: true through your server (the key never reaches the browser). The stream’s tool events are what make the agent feel present: map response.tool_call.started to a status line under its name and to rows in an Activity panel, and give the running row a stop button that calls POST /v1/responses/{id}/cancel.
node
When the agent schedules itself, its terminal tool call carries the agent37 cron add ... command as its label, so the status line can say so. Keep your own index of the sessions your app starts, recorded from response.created: GET /v1/sessions returns only the 100 most recent, and every cron firing opens one, so a busy schedule pushes chats out of that list.
6

Show what it has scheduled, and what already ran

Everything the agent scheduled for itself is an ordinary cron, so the profile’s Scheduled and Completed tabs are two reads. Your app can add tasks the same way, and tell the two apart by remembering the ids it created.
a cron the agent created for itself
Pause is a PATCH of every enabled cron to { "enabled": false }, and Resume turns back on the ones Pause turned off. Chat still works while paused, so the agent can add a cron; the example turns those off too, each time it reads the schedule and after every turn. A task your app adds is sent to the agent verbatim when it fires, with nothing that says nobody is watching that chat, so put the delivery instruction in the prompt itself: “Sam is not watching this chat: send the result with sh ~/.dots/notify.”
7

Let it message you first

Setup writes a small script to the instance that posts to your server with the planted token. AGENT37_INSTANCE_ID is set by the platform in every container, so the agent never has to know which instance it is:
~/.dots/notify
Encoding the JSON in node keeps quotes and newlines in the agent’s message from breaking the request. Your endpoint verifies the token against the instance’s metadata before it stores anything:
node
Show it as a new message with a badge and a toast. When the user replies, include the message in the reply’s app context, since it came from a check-in session and the chat they are replying in has never seen it. This endpoint is also where Web Push, email, or a text would go when no tab is open.
8

Show and edit its memory

Dots keeps its memory private; yours can show it. Hermes keeps notes about the user in ~/.hermes/memories/USER.md and its own in MEMORY.md. List the folder for each file’s modified, read the content, and save edits with X-Expected-Mtime so a note the agent wrote in the meantime is never silently overwritten:
Send modified back exactly as the listing returned it. It has a fractional part, and a rounded value never matches, so every write fails with 412 modified.

Watch and take over its computer

Dots puts its computer beside the chat: you watch it work, and when a site needs you (a sign-in, a code sent to your phone, a CAPTCHA), you take over the mouse and keyboard, then return control. The stock agent37-hermes browser is headless, so this needs an image with a screen. The example turns it on when DESKTOP_TEMPLATE is set in its .env, and runs exactly as above without it.
1

Build the desktop template

The hermes-vnc-desktop recipe is the stock Hermes image plus a visible Chromium, which the agent’s browser tool drives, and noVNC serving that screen on port 6901. Build it into a workspace template once; the build runs in the cloud, so you don’t need Docker:
Then set DESKTOP_TEMPLATE=hermes-vnc-desktop in the example’s .env. Each new user’s agent is created with the call from Create the user’s agent plus "template": "hermes-vnc-desktop"; everything else on this page works unchanged. Add a section to the persona so the agent knows the user can see its screen: browse in the visible browser, and when a site needs the user, ask them to take over instead of asking for a password in chat.
2

Mint a token for each connection

Your server mints a signed URL for port 6901 of the user’s own instance and hands the browser only a WebSocket URL built from it. The token rides in that URL’s query string, so the connection needs no cookie and works from your own origin: the browser connects straight to the instance from your page.
The token grants full control, whatever your page does with it, and it cannot be revoked. Mint it only for the instance’s owner, and keep it at 60 seconds, the minimum: it only has to be valid when the socket opens, an open socket keeps working after it expires, and every reconnect mints a fresh one.
3

Show the screen, take over, return control

noVNC draws the screen. It is plain ES modules, so the page imports a pinned release straight from a CDN, with nothing to install and no build step. Start in view-only mode. Take over cancels the turn in flight, so the two of you never fight over the mouse, and turns view-only off; Return control turns it back on:
You and the agent share one browser, so the page you leave open is the one it sees next. The example tells it so: the first message after a takeover carries a line of app context saying you used the computer and it should look at the browser before carrying on.Connect the view only while it is on screen. It streams even when nothing on the screen changes, and that traffic counts as activity, so an open view keeps an auto-sleep instance awake. The example closes the socket when the tab is hidden or the pane is closed, and opening it again wakes the instance: from asleep, the screen was back in 4 to 18 seconds in testing.
On a workspace template, a cron that names no agent records its run’s session_id only once the turn finishes. Name it, "agent": "hermes", and the run links its chat from the moment it fires, so your app can open a check-in while it is still working. Add it to the tasks your app creates. agent37 cron add has no flag for it, so PATCH the crons the agent schedules for itself with { "agent": "hermes" }; the example does that after every turn. Don’t put the signed URL itself in an iframe on your site: it authenticates with a SameSite=Lax cookie, which a cross-site frame does not send. Connecting noVNC to the WebSocket, as above, needs no cookie.

Reset

Reset is DELETE /v1/instances/{id} followed by a fresh create from onboarding. Delete is permanent: the computer, its files and memory, sessions, and crons go with it, and billing for it ends.

Worth knowing

  • Cost per user. The smallest shape is $4.76 per month if it never sleeps. With auto-sleep it bills disk alone while asleep, and crons and your messages wake it, so an agent that works a few minutes at a time costs a fraction of that. Managed LLM, search, and app calls draw the wallet up to each instance’s budget.
  • One user, one instance. Your instance limit is your user limit; it rises as you top up.
  • There is no one-shot cron. A follow-up is a cron pinned to one date and time (40 21 29 9 *). It fires again a year later and holds one of the instance’s 50 cron slots until it is deleted, and asking the agent to delete it in the check-in is not reliable. The example deletes it itself: whenever it reads the schedule, a cron the agent pinned to one date whose last_run is set has fired, so it goes. last_run is set only by a scheduled firing, never by Run now. The persona has the agent start a yearly one’s name with “Yearly” so it stays.
  • A deleted cron takes its history with it. Read GET /v1/instances/{id}/crons/{cronId}/runs and keep the runs before you delete a fired reminder, so Completed still shows it and opens its session.
  • A cron keeps its own timezone. Show a cron pinned to one date from its next_run in the user’s timezone, and name the zone next to a recurring one that is not theirs. The persona asks for the user’s timezone on every cron, and the agent works out times with TZ=... date.
  • Crons are fire and forget. A run’s status: "triggered" means the turn started. What the agent did is in the session it names.
  • Messages arrive only while a tab is open unless your notify endpoint pushes them somewhere. Agent37 has no push channel of its own.
  • Purchases are handed back. The persona has the agent gather options and return the checkout to the user; it never pays for anything. On the desktop template, the same goes for sign-ins: it asks the user to take over.
  • Texting it is its own guide: Text your agent on iMessage.
Not affiliated with OpenAI. Dots is a product of OpenAI; this guide only borrows the idea.