| name | add-telegram |
| description | Add Telegram as a channel. Can replace WhatsApp entirely or run alongside it. Also configurable as a control-only channel (triggers actions) or passive channel (receives notifications only). |
Add Telegram Channel
This skill adds Telegram support to NanoClaw. Users can choose to:
- Replace WhatsApp - Use Telegram as the only messaging channel
- Add alongside WhatsApp - Both channels active
- Control channel - Telegram triggers agent but doesn't receive all outputs
- Notification channel - Receives outputs but limited triggering
Prerequisites
1. Install Grammy
npm install grammy
Grammy is a modern, TypeScript-first Telegram bot framework.
2. Create Telegram Bot
Tell the user:
I need you to create a Telegram bot:
- Open Telegram and search for
@BotFather
- Send
/newbot and follow prompts:
- Bot name: Something friendly (e.g., "Andy Assistant")
- Bot username: Must end with "bot" (e.g., "andy_ai_bot")
- Copy the bot token (looks like
123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11)
Wait for user to provide the token.
3. Get Chat ID
Tell the user:
To register a chat, you need its Chat ID. Here's how:
For Private Chat (DM with bot):
- Search for your bot in Telegram
- Start a chat and send any message
- I'll add a
/chatid command to help you get the ID
For Group Chat:
- Add your bot to the group
- Send any message
- Use the
/chatid command in the group
Questions to Ask
Before making changes, ask:
-
Mode: Replace WhatsApp or add alongside it?
- If replace: Set
TELEGRAM_ONLY=true
- If alongside: Both will run
-
Chat behavior: Should this chat respond to all messages or only when @mentioned?
- Main chat: Responds to all
- Other chats: Can configure
respondToAll: true in registered_groups.json
Implementation
Step 1: Update Configuration
Read src/config.ts and add Telegram config exports:
export const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || "";
export const TELEGRAM_ONLY = process.env.TELEGRAM_ONLY === "true";
These should be added near the top with other configuration exports.
Step 2: Add storeMessageDirect to Database
Read src/db.ts and add this function (place it near the storeMessage function):
export function storeMessageDirect(msg: {
id: string;
chat_jid: string;
sender: string;
sender_name: string;
content: string;
timestamp: string;
is_from_me: boolean;
}): void {
db.prepare(
`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?, ?)`,
).run(
msg.id,
msg.chat_jid,
msg.sender,
msg.sender_name,
msg.content,
msg.timestamp,
msg.is_from_me ? 1 : 0,
);
}
Also update the db.ts exports to include storeMessageDirect.
Step 3: Create Telegram Module
Create src/telegram.ts with this content:
import { Bot } from "grammy";
import pino from "pino";
import {
ASSISTANT_NAME,
TRIGGER_PATTERN,
MAIN_GROUP_FOLDER,
} from "./config.js";
import { RegisteredGroup, NewMessage } from "./types.js";
import { storeChatMetadata, storeMessageDirect } from "./db.js";
const logger = pino({
level: process.env.LOG_LEVEL || "info",
transport: { target: "pino-pretty", options: { colorize: true } },
});
export interface TelegramCallbacks {
onMessage: (
msg: NewMessage,
group: RegisteredGroup,
) => Promise<string | null>;
getRegisteredGroups: () => Record<string, RegisteredGroup>;
}
let : | = ;
: | = ;
(): <> {
callbacks = cbs;
bot = (botToken);
bot.(, {
chatId = ctx..;
chatType = ctx..;
chatName =
chatType ===
? ctx.?. ||
: (ctx. ). || ;
ctx.(
,
{ : },
);
});
bot.(, {
ctx.();
});
bot.(, (ctx) => {
(ctx...()) ;
chatId = ;
content = ctx..;
timestamp = (ctx.. * ).();
senderName =
ctx.?. ||
ctx.?. ||
ctx.?..() ||
;
sender = ctx.?..() || ;
msgId = ctx...();
chatName =
ctx.. ===
? senderName
: (ctx. ). || chatId;
(chatId, timestamp, chatName);
registeredGroups = callbacks!.();
group = registeredGroups[chatId];
(!group) {
logger.(
{ chatId, chatName },
,
);
;
}
({
: msgId,
: chatId,
sender,
: senderName,
content,
timestamp,
: ,
});
isMain = group. === ;
respondToAll = (group ). === ;
botUsername = ctx.?.?.();
entities = ctx.. || [];
isBotMentioned = entities.( {
(entity. === ) {
mentionText = content
.(entity., entity. + entity.)
.();
mentionText === ;
}
;
});
(
!isMain &&
!respondToAll &&
!isBotMentioned &&
!.(content)
) {
;
}
logger.(
{ chatId, chatName, : senderName },
,
);
ctx.();
: = {
: msgId,
: chatId,
sender,
: senderName,
content,
timestamp,
};
{
response = callbacks!.(msg, group);
(response) {
ctx.();
}
} (err) {
logger.({ err, chatId }, );
}
});
bot.( {
logger.({ : err. }, );
});
bot.({
: {
logger.(
{ : botInfo., : botInfo. },
,
);
.();
.(
,
);
},
});
}
(): <> {
(!bot) {
logger.();
;
}
{
numericId = chatId.(, );
bot..(numericId, text);
logger.({ chatId, : text. }, );
} (err) {
logger.({ chatId, err }, );
}
}
(): {
bot !== ;
}
(): {
(bot) {
bot.();
bot = ;
callbacks = ;
logger.();
}
}
Step 4: Update Main Application
Modify src/index.ts:
- Add imports at the top:
import {
connectTelegram,
sendTelegramMessage,
isTelegramConnected,
} from "./telegram.js";
import { TELEGRAM_BOT_TOKEN, TELEGRAM_ONLY } from "./config.js";
- Update
sendMessage function to route by channel. Find the sendMessage function and replace it with:
async function sendMessage(jid: string, text: string): Promise<void> {
if (jid.startsWith("tg:")) {
await sendTelegramMessage(jid, text);
} else {
try {
await sock.sendMessage(jid, { text });
logger.info({ jid, length: text.length }, "Message sent");
} catch (err) {
logger.error({ jid, err }, "Failed to send message");
}
}
}
- Update
main() function. Find the main() function and update it to support Telegram. Add this before the connectWhatsApp() call:
const hasTelegram = !!TELEGRAM_BOT_TOKEN;
if (hasTelegram) {
await connectTelegram(TELEGRAM_BOT_TOKEN, {
onMessage: async (msg, group) => {
const sinceTimestamp = lastAgentTimestamp[msg.chat_jid] || "";
const missedMessages = getMessagesSince(
msg.chat_jid,
sinceTimestamp,
ASSISTANT_NAME,
);
const lines = missedMessages.map((m) => {
const escapeXml = (s: string) =>
s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
return `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${escapeXml(m.content)}</message>`;
});
const prompt = `<messages>\n${lines.join("\n")}\n</messages>`;
const group = registeredGroups[msg.];
isMain = group. === ;
output = (group, {
prompt,
: sessions[group.],
: group.,
: msg.,
isMain,
: ,
});
(output.) {
sessions[group.] = output.;
(path.(, ), sessions);
}
lastAgentTimestamp[msg.] = msg.;
();
output. === ? output. : ;
},
: registeredGroups,
});
}
- Wrap the
connectWhatsApp() call to support Telegram-only mode. Replace:
await connectWhatsApp();
With:
if (!TELEGRAM_ONLY) {
await connectWhatsApp();
} else {
startSchedulerLoop({
sendMessage,
registeredGroups: () => registeredGroups,
getSessions: () => sessions,
});
startIpcWatcher();
logger.info(
`NanoClaw running (Telegram-only, trigger: @${ASSISTANT_NAME})`,
);
}
Step 5: Update Environment
Add to .env:
TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_HERE
Step 6: Register a Telegram Chat
After installing and starting the bot, tell the user:
- Send
/chatid to your bot (in private chat or in a group)
- Copy the chat ID (e.g.,
tg:123456789 or tg:-1001234567890)
- I'll add it to registered_groups.json
Then update data/registered_groups.json:
For private chat:
{
"tg:123456789": {
"name": "Personal",
"folder": "main",
"trigger": "@Andy",
"added_at": "2026-02-05T12:00:00.000Z"
}
}
For group chat (note the negative ID for groups):
{
"tg:-1001234567890": {
"name": "My Telegram Group",
"folder": "telegram-group",
"trigger": "@Andy",
"added_at": "2026-02-05T12:00:00.000Z",
"respondToAll": false
}
}
Set respondToAll: true if you want the bot to respond to all messages in that chat (not just when @mentioned or triggered).
Step 7: Build and Restart
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
Or for systemd:
npm run build
systemctl --user restart nanoclaw
Step 8: Test
Tell the user:
Send a message to your registered Telegram chat:
- For main chat: Any message works
- For non-main:
@Andy hello or @mention the bot
Check logs: tail -f logs/nanoclaw.log
Replace WhatsApp Entirely
If user wants Telegram-only:
- Set
TELEGRAM_ONLY=true in .env
- The WhatsApp connection code is automatically skipped
- Optionally remove
@whiskeysockets/baileys dependency (but it's harmless to keep)
Features
Chat ID Formats
- WhatsApp:
120363336345536173@g.us (groups) or 1234567890@s.whatsapp.net (DM)
- Telegram:
tg:123456789 (positive for private) or tg:-1001234567890 (negative for groups)
Trigger Options
The bot responds when:
- Message is in the main chat (folder: "main")
- Chat has
respondToAll: true in registered_groups.json
- Bot is @mentioned using native Telegram mention (e.g., @your_bot_username)
- Message matches TRIGGER_PATTERN (e.g., starts with @Andy)
Commands
/chatid - Get chat ID for registration
/ping - Check if bot is online
Troubleshooting
Bot not responding
Check:
TELEGRAM_BOT_TOKEN is set in .env
- Chat is registered in
data/registered_groups.json with tg: prefix
- For non-main chats: message includes trigger or @mention
- Service is running:
launchctl list | grep nanoclaw
Getting chat ID
If /chatid doesn't work:
- Verify bot token is valid:
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe"
- Check bot is started:
tail -f logs/nanoclaw.log
Service conflicts
If running npm run dev while launchd service is active:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
Agent Swarms (Teams)
After completing the Telegram setup, ask the user:
Would you like to add Agent Swarm support? Without it, Agent Teams still work — they just operate behind the scenes. With Swarm support, each subagent appears as a different bot in the Telegram group so you can see who's saying what and have interactive team sessions.
If they say yes, invoke the /add-telegram-swarm skill.
Removal
To remove Telegram integration:
- Delete
src/telegram.ts
- Remove Telegram imports from
src/index.ts
- Remove
sendTelegramMessage logic from sendMessage() function
- Remove
connectTelegram() call from main()
- Remove
storeMessageDirect from src/db.ts
- Remove Telegram config from
src/config.ts
- Uninstall:
npm uninstall grammy
- Rebuild:
npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw