Setup
1
Create a bot
Talk to @BotFather on Telegram and create a new bot. Copy the token.
2
Set the token
Copy The
.env.example to .env at the repo root and set:.env file is gitignored and loaded automatically.3
Add the platform
In your entity YAML (
configs/entities/<name>.yaml):4
Run
run (not talk) — the daemon owns the bot connection.Commands
Use/commands for the full catalog. Common entries:
Session, privacy, compaction, and self-update commands (
/session_*, /privacy, /compact, /update, etc.) are documented in-app via /commands.
Media
- Photos and image documents — downloaded and passed to the model as vision input. Caption optional.
- Voice and audio messages — downloaded and passed to the harness as audio input (handling depends on your cognition / transcription setup).
Human-paced multi-bubble replies
Plain-text replies (the usual deliberate path) are not sent as a single giant message whenever they fit under Telegram’s 4096-character cap. The adapter splits outgoing text using harnesspresence settings so multi-line answers can read like a person texting in several bubbles.
Between bubbles the bot also triggers a typing action (
send_chat_action) so the gap does not look like a dead connection.
Configure these under presence: in configs/default.yaml or your entity YAML (same block as heartbeat_interval / initiative_cooldown).
Hard cap: individual Telegram messages are still limited by the API (~4096 characters); very long lines without newlines are split mechanically so nothing is dropped.
say() tool: each say() is still one outbound message from the tool path. Natural multi-bubble without multiple say() calls comes from the final reply going through send_plain_chunks with the settings above.
Busy indicator
While the harness runsperceive for an incoming Telegram message, the bot can show a short busy line (not limited to tool use — it covers the whole turn):
- Monospace (
<code>in HTML mode) with a braille-style spinner and a single random gerund in the style of Claude Code (e.g.thinking,reticulating,vibing). The spinner updates every edit; the word changes every ~20 spinner frames. - Posted as a reply to the user’s message when metadata allows.
- Pinned quietly (
disable_notification) so it stays visible. - The system “pinned a message” service line is deleted when it refers to this pin (whether Telegram attributes that line to the bot or the user).
- Unpinned and deleted when the turn finishes and the real reply is delivered.
observe tool message ring. Typing actions (send_chat_action) may still run in parallel.
Turning it off
Some chats prefer not to see the pinned line./busy toggles the indicator per chat (also /busy on, /busy off, /busy status). The choice is stored in the entity database and survives restarts.
Implementation: TelegramPlatform.run_busy_indicator + main.py on_inp (see the main repo).
/body command
/body prints the raw soma/body.md file as one or more Telegram HTML <pre> blocks — the same file Soma flushes for the agent, under the entity workspace (…/soma/body.md).
- It uses the execution client (
read_fileonsoma/body.md): local disk when the worker has a workspace, or HTTP execution RPC whenBUMBLEBEE_EXECUTION_RPC_URL/tools.execution.base_urlpoints at Railway — so hybrid setups read the worker’s file, not your laptop’s. - It does not go through the main LLM; if your deployment blocks file tools (e.g. hybrid without RPC),
/bodyis blocked with the same guidance asread_file.
Privacy and operators
Operators can lock the bot to approved users only.Configure operators
Set operator user IDs in entity YAML:canary.yaml isn’t in the image):
/whoami in chat to find your ID.
Privacy commands
The allowlist persists in the entity database and survives restarts. While locked, static
allowed_user_ids in YAML are ignored.
Optional settings
Inline mode
In BotFather, run/setinline for your bot and pick a placeholder. Users can then type @YourBot ... in any chat for quick identity cards.