Atlas Bots
Bots that live in ordinary Atlas chats. A bot is an Atlas bot account plus an outbound-only message stream on your own server — no webhook, no endpoint registration, no Atlas source code, and the default chat UI is the whole interface. One BotFather token is the entire configuration.
The SDK is on npm: npm install atlas-bot-sdk — compiled ESM with full type declarations, zero runtime dependencies, Node 18+. Get a token from BotFather and you're running; everything documented on these pages ships in the package.
The model, in one paragraph
Your bot's server connects out: it pulls its message stream as the bot's account and calls the platform LLM with the same token. Nothing anywhere registers your endpoint, because there isn't one — membership is the routing table (your bot receives exactly the chats it's a member of), the chat history is the queue (downtime loses nothing; the bot catches up on restart), and the chat timeline is the durable record.
The building blocks
| Concept | On Atlas |
|---|---|
| Creating a bot | BotFather — /newbot in an ordinary chat, backed by the platform's provisioning service |
| Credentials | The bot's access token — chat identity, LLM access, and push access in one credential |
| Receiving messages | An outbound message stream (webhook latency, pull semantics); a push delivery lane is the graduation path for breakout bots |
| Acting in chat | Chat actions with a rendered meaning in the app: messages, media, choice buttons, invites, chat renames, calls, notifications — the full catalog |
| Buttons | Choice buttons — tappable chips under a bot message; the tap comes back as an ordinary reply |
| Commands | The invisible routing layer: button taps arrive as command values and bot.onCommand("claim", …) routes them — humans tap, they never type |
| Bot-to-bot loops | The 🤖 marker loop-breaker, shared with the Atlas Auto Responder lanes — bots never react to bot messages |
| Privacy & groups | /setprivacy, /setjoingroups — enforced by the SDK from the bot's platform-stored settings |
Chat is the UI — and the API
Bots ship no interface. Everything user-visible is a chat event Atlas already knows how to render:
- Messages render as normal chat bubbles from the bot's identity — and they push to the lock screen like any other message.
- Media is first-class: photos, videos, voice notes, documents, contact cards, and locations, sent and received exactly as the app renders them.
- Choice buttons are THE interaction model — tappable chips under the bubble (
ctx.sendChoices). Every choice a bot offers is a button; commands exist only as the values taps send. Older app versions see a numbered list. - Membership changes render as system rows — inviting a matched doctor produces "Dr. Chen joined" with no bot code beyond the invite.
- Chat-name changes render as system rows too — the no-app-changes channel for showing details like "Doctor Finder — Dr. Chen · dermatology".
- Read ticks just work: the SDK marks messages read as it handles them, so people see the same ticks they get with humans.
This is the deeper design rule: membership is the API. When your server decides something (a match, an assignment), it expresses the decision as an invite or a chat event, and the app renders the consequences.
Bot chats behave differently
The app treats a chat with a directory bot as the bot's surface, not yours:
- Your Atlas stays out. The local Atlas agent is forced off in bot chats and the "A" toggle is inert — you're talking to their bot, not your assistant, and the two can never talk over each other.
- No call button. Bot chats are text-only surfaces — see below.
- App commands are curated, not open. A bot can ask the user's Atlas to do things for it — send a message as the user, look up a contact, list to-dos, place a call — but only through a hard catalog of sanitized commands, and every outcome is echoed back to the bot as a result. Bulk wipes, profile changes, and confirm-gated destruction stay off-catalog. See the command catalog.
When people need to talk: make a group
Bot chats are text-only — there is no call button in them, and no permission machinery pretending otherwise. When a conversation needs voice, or two humans need each other, the bot creates a group:
bot.createGroup(chatId, title, members)creates a chat the bot owns and pulls the members in with platform join tokens their apps auto-accept — the group just appears for everyone.- A group is a normal human chat: the call button works there out of the box, no flags, no unlocking.
- Doctor Finder is the worked example: claiming a case opens a group with the patient and the doctor — that's where they talk and call.
Guard rails, on by default
Every bot inherits the deterministic loop-safety rules proven by the Atlas Auto Responder — they are SDK defaults, not advice:
| Guard | Rule |
|---|---|
| 🤖 marker loop-breaker | Bot sends carry the marker; marked inbound never triggers a handler. Two responders can never ping-pong. |
| Stale guard | Catch-up events older than 5 minutes are dropped — a bot that was down all night never answers old mail. |
| Dedupe | An event id is handled at most once across stream overlaps. |
| Cooldown | Sends into one chat are spaced (1s default). |
| Consecutive cap | After 8 unanswered sends in 30 minutes, the bot goes quiet in that chat until a human speaks. |
Scaling story
A streaming bot is indistinguishable from a phone to the platform — bots scale exactly like users, and the platform runs zero delivery infrastructure for them (no retry queues, no webhook health tracking). A single breakout bot in thousands of busy chats eventually outgrows per-account streaming; the platform's answer is a push-based delivery lane, and because the SDK owns the transport under the same onMessage interface, graduating a bot is a config change, not a rewrite.