| name | add-telegram-swarm |
| description | Add Agent Swarm (Teams) support to Telegram. Each subagent gets its own bot identity in the group. Requires Telegram channel to be set up first (use /add-telegram). Triggers on "agent swarm", "agent teams telegram", "telegram swarm", "bot pool". |
| allowed-tools | Bash(*), Read, Edit, Write, Glob, Grep, AskUserQuestion |
Add Agent Swarm to Telegram
UX Rule: Use AskUserQuestion for ALL interactions with the user. Never just output questions as text — always use the tool so the user gets structured prompts with selectable options.
This skill adds Agent Teams (Swarm) support to an existing Telegram channel. Each subagent in a team gets its own bot identity in the Telegram group, so users can visually distinguish which agent is speaking.
Prerequisite: Telegram must already be set up via the /add-telegram skill. If src/telegram.ts does not exist or TELEGRAM_BOT_TOKEN is not configured, tell the user to run /add-telegram first.
Phase 0: Detection & Options
Detect existing swarms
Query all existing swarms across channels:
sqlite3 store/messages.db "SELECT jid, name, folder, container_config FROM registered_groups WHERE jid LIKE 'tg:%' AND container_config LIKE '%webhookUrl%'"
sqlite3 store/messages.db "SELECT jid, name, folder, container_config FROM registered_groups WHERE jid LIKE 'dc:%' AND container_config LIKE '%webhookUrl%'"
sqlite3 store/messages.db "SELECT COUNT(*) FROM registered_groups WHERE (jid LIKE 'dc:%' AND container_config LIKE '%webhookUrl%') OR (jid LIKE 'tg:%' AND container_config LIKE '%webhookUrl%')"
If existing Telegram swarms found, AskUserQuestion with 3 options:
- Add a new swarm (default) — Create a new Telegram group with its own team
- Replace an existing swarm — Delete the old config and start fresh for that group
- Update an existing swarm — Modify team composition or CLAUDE.md for an existing swarm
If no swarms found, proceed directly to the resource warning.
Resource warning (always show)
Each swarm runs in its own container VM using approximately 1GB of RAM. The default limit is 5 concurrent containers (shared across all channels). You currently have N swarm(s) configured. To increase the limit, set MAX_CONCURRENT_CONTAINERS in your .env on the host.
Present via AskUserQuestion to confirm the user understands before proceeding.
Naming
AskUserQuestion: "What theme name for this swarm? Name it based on the group's interest or purpose — e.g., 'marketing', 'stocks', 'research', 'dev', 'personal'. The group folder will be telegram-{theme}."
Never default to "telegram-main" — always require a theme name.
Phase 1: Create Telegram Group & Add Bots
Step 1: Create or choose a group
AskUserQuestion: "Do you already have a Telegram group for this swarm, or do you need to create one?"
- Create a new group (Recommended) — I'll walk you through creating a dedicated group
- Use an existing group — I already have a group ready
If creating a new group:
Guide the user through creating a Telegram group named after the swarm theme:
- Open Telegram and tap the pencil/compose icon (top right)
- Select New Group
- Name it after your swarm theme — e.g., "Research", "Marketing", "Stocks"
- Add your main bot (search for its @username) as a member
- Tap Create
After creation, you'll add the pool bots in the Prerequisites step below.
Step 2: Security reminder
Present this to the user via AskUserQuestion (confirm they understand before proceeding):
Security note: Anyone in this Telegram group can command the bot — triggering agent containers that run with your API key and have access to tools, memory, and scheduled tasks.
Keep the group private. Do not share invite links. If you want shared access, create a separate group registered with requiresTrigger: true so others must @mention the bot and get limited (non-admin) access.
Unlike private DMs, group messages are visible to all group members. Be mindful of what information you ask the bot to process in a group setting.
Step 3: Ensure main bot has Group Privacy disabled
The main bot must be able to see all messages in the group (not just @mentions):
- Open
@BotFather in Telegram
- Send
/mybots and select your main bot
- Go to Bot Settings > Group Privacy
- If it says "enabled", select Turn off
- Remove and re-add the main bot to the group — this is required for the change to take effect
Skip this step if the main bot already has Group Privacy disabled (you can check in BotFather).
How It Works
- The main bot receives messages and sends lead agent responses (already set up by
/add-telegram)
- Pool bots are send-only — each gets a Grammy
Api instance (no polling)
- When a subagent calls
send_message with a sender parameter, the host assigns a pool bot and renames it to match the sender's role
- Messages appear in Telegram from different bot identities
Subagent calls send_message(text: "Found 3 results", sender: "Researcher")
→ MCP writes IPC file with sender field
→ Host IPC watcher picks it up
→ Assigns pool bot #2 to "Researcher" (round-robin, stable per-group)
→ Renames pool bot #2 to "Researcher" via setMyName
→ Sends message via pool bot #2's Api instance
→ Appears in Telegram from "Researcher" bot
Prerequisites
1. Create Pool Bots
Tell the user:
I need you to create 3-5 Telegram bots to use as the agent pool. These will be renamed dynamically to match agent roles.
- Open Telegram and search for
@BotFather
- Send
/newbot for each bot:
- Give them any placeholder name (e.g., "Bot 1", "Bot 2")
- Usernames like
myproject_swarm_1_bot, myproject_swarm_2_bot, etc.
- Copy all the tokens
Wait for user to provide the tokens.
2. Disable Group Privacy for Pool Bots
Tell the user:
Important: Each pool bot needs Group Privacy disabled so it can send messages in groups.
For each pool bot in @BotFather:
- Send
/mybots and select the bot
- Go to Bot Settings > Group Privacy > Turn off
3. Add Pool Bots to the Swarm Group
Tell the user:
Now add all pool bots to the Telegram group you created for this swarm:
- Open the group in Telegram
- Tap the group name at the top to open group info
- Tap Add Members
- Search for each pool bot by its @username and add it
- Verify all bots appear in the member list
Note: After adding a bot whose Group Privacy setting was just changed, you may need to remove and re-add it for the change to take effect.
3. Image Generation Setup (if applicable)
Most content and research teams benefit from image generation. Check if GEMINI_API_KEY is already configured:
grep -q "GEMINI_API_KEY" .env 2>/dev/null && echo "GEMINI_KEY=yes" || echo "GEMINI_KEY=no"
If the key is missing, and the team includes a Visual Director or any role that would produce images, guide the user:
AskUserQuestion: "Your team includes a visual role. To generate actual images (not just descriptions), you need a Gemini API key. Would you like to set that up now?"
- Yes, set it up — Walk through the steps below
- Skip for now — The Visual Director will provide text-based recommendations only
If the user wants to set it up:
-
Get a Gemini API key:
- Go to https://aistudio.google.com/apikey
- Sign in with your Google account
- Click Create API key
- Select or create a Google Cloud project when prompted
- Copy the generated key
-
Add to .env:
AskUserQuestion: "Paste your Gemini API key here."
Then append to .env:
echo "GEMINI_API_KEY=<their_key>" >> .env
-
Add to container allowlist so the key is passed into container agents:
Check if it's already in the allowlist:
grep -q "GEMINI_API_KEY" src/container-runner.ts && echo "ALREADY_ALLOWED" || echo "NEEDS_ADDING"
If NEEDS_ADDING, find the allowedVars array in src/container-runner.ts and add 'GEMINI_API_KEY' to it:
grep "allowedVars" src/container-runner.ts
Edit the array to include 'GEMINI_API_KEY'. For example, if it currently reads:
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY', 'TRANSCRIPT_API_KEY'];
Change it to:
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY', 'TRANSCRIPT_API_KEY', 'GEMINI_API_KEY'];
This ensures the key is passed via stdin to the container's SDK environment, where Bash tool calls can access it.
Implementation
Step 1: Update Configuration
Read src/config.ts and add the bot pool config near the other Telegram exports:
export const TELEGRAM_BOT_POOL = (process.env.TELEGRAM_BOT_POOL || '')
.split(',')
.map((t) => t.trim())
.filter(Boolean);
Step 2: Add Bot Pool to Telegram Module
Read src/telegram.ts and add the following:
- Update imports — add
Api to the Grammy import:
import { Api, Bot } from 'grammy';
- Add pool state after the existing
let bot declaration:
const poolApis: Api[] = [];
const senderBotMap = new Map<string, number>();
let nextPoolIndex = 0;
- Add pool functions — place these before the
isTelegramConnected function:
export async function initBotPool(tokens: string[]): Promise<void> {
for (const token of tokens) {
try {
const api = new Api(token);
const me = await api.getMe();
poolApis.push(api);
logger.info(
{ username: me.username, id: me.id, poolSize: poolApis.length },
'Pool bot initialized',
);
} catch (err) {
logger.error({ err }, 'Failed to initialize pool bot');
}
}
if (poolApis.length > 0) {
logger.info({ count: poolApis.length }, 'Telegram bot pool ready');
}
}
export async function sendPoolMessage(
chatId: string,
text: string,
sender: string,
groupFolder: string,
): Promise<void> {
if (poolApis.length === 0) {
await sendTelegramMessage(chatId, text);
return;
}
const key = `${groupFolder}:${sender}`;
let idx = senderBotMap.get(key);
if (idx === undefined) {
idx = nextPoolIndex % poolApis.length;
nextPoolIndex++;
senderBotMap.set(key, idx);
try {
await poolApis[idx].setMyName(sender);
await new Promise((r) => setTimeout(r, 2000));
logger.info({ sender, groupFolder, poolIndex: idx }, 'Assigned and renamed pool bot');
} catch (err) {
logger.warn({ sender, err }, 'Failed to rename pool bot (sending anyway)');
}
}
const api = poolApis[idx];
try {
const numericId = chatId.replace(/^tg:/, '');
const MAX_LENGTH = 4096;
if (text.length <= MAX_LENGTH) {
await api.sendMessage(numericId, text);
} else {
for (let i = 0; i < text.length; i += MAX_LENGTH) {
await api.sendMessage(numericId, text.slice(i, i + MAX_LENGTH));
}
}
logger.info({ chatId, sender, poolIndex: idx, length: text.length }, 'Pool message sent');
} catch (err) {
logger.error({ chatId, sender, err }, 'Failed to send pool message');
}
}
Step 3: Add sender Parameter to MCP Tool
Read container/agent-runner/src/ipc-mcp-stdio.ts and update the send_message tool to accept an optional sender parameter:
Change the tool's schema from:
{ text: z.string().describe('The message text to send') },
To:
{
text: z.string().describe('The message text to send'),
sender: z.string().optional().describe('Your role/identity name (e.g. "Researcher"). When set, messages appear from a dedicated bot in Telegram.'),
},
And update the handler to include sender in the IPC data:
async (args) => {
const data: Record<string, string | undefined> = {
type: 'message',
chatJid,
text: args.text,
sender: args.sender || undefined,
groupFolder,
timestamp: new Date().toISOString(),
};
writeIpcFile(MESSAGES_DIR, data);
return { content: [{ type: 'text' as const, text: 'Message sent.' }] };
},
Step 4: Update Host IPC Routing
Read src/ipc.ts and make these changes:
-
Add imports — add sendPoolMessage and initBotPool from the Telegram swarm module, and TELEGRAM_BOT_POOL from config.
-
Update IPC message routing — in src/ipc.ts, find where the sendMessage dependency is called to deliver IPC messages (inside processIpcFiles). The sendMessage is passed in via the IpcDeps parameter. Wrap it to route Telegram swarm messages through the bot pool:
if (data.sender && data.chatJid.startsWith('tg:')) {
await sendPoolMessage(
data.chatJid,
data.text,
data.sender,
sourceGroup,
);
} else {
await deps.sendMessage(data.chatJid, data.text);
}
Note: The assistant name prefix is handled by formatOutbound() in the router — Telegram channels have prefixAssistantName = false so no prefix is added for tg: JIDs.
- Initialize pool in
main() in src/index.ts — after creating the Telegram channel, add:
if (TELEGRAM_BOT_POOL.length > 0) {
await initBotPool(TELEGRAM_BOT_POOL);
}
Step 5: Update CLAUDE.md Files
5a. Add global message formatting rules
Read groups/global/CLAUDE.md and add a Message Formatting section:
## Message Formatting
NEVER use markdown. Only use WhatsApp/Telegram formatting:
- *single asterisks* for bold (NEVER **double asterisks**)
- _underscores_ for italic
- • bullet points
- ```triple backticks``` for code
No ## headings. No [links](url). No **double stars**.
5b. Update existing group CLAUDE.md headings
In any group CLAUDE.md that has a "WhatsApp Formatting" section (e.g. groups/main/CLAUDE.md), rename the heading to reflect multi-channel support:
## WhatsApp Formatting (and other messaging apps)
5c. Add Agent Teams instructions to Telegram groups
If the team includes a visual/image-producing role, copy the generate-image skill to the container skills directory so it's available as a proper invokable skill inside the container:
mkdir -p container/skills/generate-image
cp .claude/skills/generate-image/SKILL.md container/skills/generate-image/SKILL.md
Also copy the canonical generate-image script into each Telegram group's scripts directory so the container agent uses the exact same version as the host:
for FOLDER in $(sqlite3 store/messages.db "SELECT folder FROM registered_groups WHERE jid LIKE 'tg:%'"); do
mkdir -p "groups/${FOLDER}/scripts"
cp scripts/generate-image.js "groups/${FOLDER}/scripts/generate-image.js"
done
The container-runner syncs container/skills/ into each group's .claude/skills/ at spawn time, making them available to the SDK as invokable skills.
For each Telegram group that will use agent teams, create or update its groups/{folder}/CLAUDE.md with these instructions. Read the existing CLAUDE.md first (or groups/global/CLAUDE.md as a base) and add the Agent Teams section:
## Agent Teams
When creating a team to tackle a complex task, follow these rules:
### CRITICAL: Follow the user's prompt exactly
Create *exactly* the team the user asked for — same number of agents, same roles, same names. Do NOT add extra agents, rename roles, or use generic names like "Researcher 1". If the user says "a marine biologist, a physicist, and Alexander Hamilton", create exactly those three agents with those exact names.
### CRITICAL: How teammates communicate
Every teammate MUST call `mcp__bastionclaw__send_message` to post their work to the Telegram group. This is NOT optional. If a teammate does not call `send_message`, their work is invisible — the user never sees it, other teammates never see it, and the lead agent cannot reference it.
*The `send_message` call IS the deliverable.* Work that only exists as internal Task tool output is wasted — it never reaches the group chat.
### Team member instructions
Each team member MUST be instructed to:
1. Your PRIMARY job is to call `mcp__bastionclaw__send_message` with `sender` set to your exact role/character name (e.g., `sender: "Marine Biologist"`). This is how your output reaches the Telegram group. If you don't call it, your work doesn't exist.
2. You MUST call `send_message` at least once with your completed work.
3. Also communicate with teammates via `SendMessage` as normal for coordination.
4. Keep group messages *short* — 2-4 sentences max per message. Break longer content into multiple `send_message` calls. No walls of text.
5. Use the `sender` parameter consistently — always the same name so the bot identity stays stable.
6. Post the FULL content via `send_message` — do NOT just announce it's ready. Post the actual text.
7. NEVER use markdown formatting. Use ONLY WhatsApp/Telegram formatting: single *asterisks* for bold (NOT **double**), _underscores_ for italic, • for bullets, ```backticks``` for code. No ## headings, no [links](url), no **double asterisks**.
### Teammate prompt template (MUST use this)
When creating a teammate via the Task tool, you MUST include these exact instructions in their prompt. Do not omit or paraphrase — the teammate needs these to function:
\```
CRITICAL REQUIREMENT: You MUST call mcp__bastionclaw__send_message to post your work to the Telegram group. This is your primary deliverable — if you don't call send_message, your work is invisible and wasted.
You are the [ROLE]. Your job is [RESPONSIBILITY].
When your work is ready, post it to the group:
- Call mcp__bastionclaw__send_message with sender set to "[ROLE]" and text set to your full output
- Do NOT just return your work as text — you MUST use the send_message tool
- Keep each message to 2-4 sentences. Break longer content into multiple send_message calls
- ONLY use single *asterisks* for bold (never **double**), _underscores_ for italic, • for bullets. No markdown.
After posting via send_message, you may also return a brief summary to the orchestrator.
\```
### Synchronization rules for the lead agent
- *Never announce completion until all teammates have posted their output.* If you delegated to N agents, wait for N `send_message` posts in the group.
- *If a teammate completed their Task but didn't post to the group, the work is incomplete.* Re-delegate or post the work yourself with the teammate's sender name.
- *Never paraphrase what a teammate "would have said."* Only reference what they actually posted to the group.
- *Do NOT wrap up early.* Wait for every delegated agent to finish posting before your final summary.
- *Do NOT write the deliverable yourself.* If a specialist was supposed to produce output, they post it — not you.
### Lead agent behavior
As the lead agent who created the team:
- You do NOT need to react to or relay every teammate message. The user sees those directly from the teammate bots.
- Send your own messages only to comment, share thoughts, synthesize, or direct the team.
- When processing an internal update from a teammate that doesn't need a user-facing response, wrap your *entire* output in `<internal>` tags.
- Focus on high-level coordination and the final synthesis.
Step 6: Update Environment
Add pool tokens to .env:
TELEGRAM_BOT_POOL=TOKEN1,TOKEN2,TOKEN3,...
Also add TELEGRAM_BOT_POOL to the launchd plist (~/Library/LaunchAgents/com.bastionclaw.plist) in the EnvironmentVariables dict if using launchd.
Step 7: Rebuild and Restart
npm run build
./container/build.sh
launchctl unload ~/Library/LaunchAgents/com.bastionclaw.plist
launchctl load ~/Library/LaunchAgents/com.bastionclaw.plist
Must use unload/load (not just kickstart) because the plist env vars changed.
Step 8: Test
Tell the user:
Send a message in your Telegram group asking for a multi-agent task, e.g.:
"Assemble a team of a researcher and a coder to build me a hello world app"
You should see:
- The lead agent (main bot) acknowledging and creating the team
- Each subagent messaging from a different bot, renamed to their role
- Short, scannable messages from each agent
Check logs: tail -f logs/bastionclaw.log | grep -i pool
Architecture Notes
- Pool bots use Grammy's
Api class — lightweight, no polling, just send
- Bot names are set via
setMyName — changes are global to the bot, not per-chat
- A 2-second delay after
setMyName allows Telegram to propagate the name change before the first message
- Sender→bot mapping is stable within a group (keyed as
{groupFolder}:{senderName})
- Mapping resets on service restart — pool bots get reassigned fresh
- If pool runs out, bots are reused (round-robin wraps)
Troubleshooting
Pool bots not sending messages
- Verify tokens:
curl -s "https://api.telegram.org/botTOKEN/getMe"
- Check pool initialized:
grep "Pool bot" logs/bastionclaw.log
- Ensure all pool bots are members of the Telegram group
- Check Group Privacy is disabled for each pool bot
Bot names not updating
Telegram caches bot names client-side. The 2-second delay after setMyName helps, but users may need to restart their Telegram client to see updated names immediately.
Subagents not using send_message
Check the group's CLAUDE.md has the Agent Teams instructions. The lead agent reads this when creating teammates and must include the send_message + sender instructions in each teammate's prompt.
Removal
To remove Agent Swarm support while keeping basic Telegram:
- Remove
TELEGRAM_BOT_POOL from src/config.ts
- Remove pool code from
src/telegram.ts (poolApis, senderBotMap, initBotPool, sendPoolMessage)
- Remove pool routing from IPC handler in
src/index.ts (revert to plain sendMessage)
- Remove
initBotPool call from main()
- Remove
sender param from MCP tool in container/agent-runner/src/ipc-mcp-stdio.ts
- Remove Agent Teams section from group CLAUDE.md files
- Remove
TELEGRAM_BOT_POOL from .env and launchd plist
- Rebuild:
npm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.bastionclaw.plist && launchctl load ~/Library/LaunchAgents/com.bastionclaw.plist