Feishu Cross-Bot Message Broadcast β Enabling Multi-Agent Collaboration
Implement send-time cross-bot broadcast to achieve feature parity with Discord, enabling OpenClaw bots in shared Feishu group chats to receive each other's messages via synthetic event injection.
π Symptoms
Technical Manifestation
When multiple OpenClaw Feishu bot accounts operate within the same group chat, inter-bot communication fails silently. The platform delivers human user messages to all bots via the im.message.receive_v1 webhook, but bot-originated messages generate no webhook events for other bots.
Observed Behavior
bash
Scenario: 3 OpenClaw Feishu bots (Bot-A, Bot-B, Bot-C) in shared group “engineering”
Bot-A sends a message to the group
Bot-A execution log:
[OpenClaw] INFO: Sending message to group engineering (msg_id: om_xxx123) [OpenClaw] INFO: Message sent successfully
Bot-B execution log:
[OpenClaw] INFO: No events received β Bot-B remains unaware of Bot-A’s message
Bot-C execution log:
[OpenClaw] INFO: No events received β Bot-C remains unaware of Bot-A’s message
Platform Event Delivery Comparison
| Platform | Bot-to-Bot Message Events | Native Multi-Bot Support |
|---|---|---|
| Discord | MESSAGE_CREATE events delivered to all bots | β Yes |
| Feishu | Human messages only via im.message.receive_v1 | β No |
| Telegram | Human messages only via updates | β No |
Impact on Multi-Agent Workflows
bash
Desired: Multi-agent handoff in shared Feishu group
Bot-A: “I’ve completed analysis of module X, handing off to specialized analyzer Bot-B” Bot-B: “Received context from Bot-A, starting deep analysis…” Bot-C: “Logging this interaction for audit trail…”
Actual: Only human users trigger multi-agent chain
User: “Run analysis on module X” Bot-A: “I’ve completed analysis…”
Bot-B and Bot-C never receive context from Bot-A
Config Absence Symptoms
When crossBotBroadcast is not configured, the system falls back to Discord-style event handling, which silently discards inter-bot events on Feishu:
bash
No error thrown β messages are simply not delivered
[OpenClaw] WARN: Bot message detected, skipping webhook delivery (Feishu platform limitation)
π§ Root Cause
Platform Architecture Divergence
Feishu’s im.message.receive_v1 webhook intentionally filters out bot-originated messages before event delivery. This is a deliberate security and anti-spam measure in the Feishu platform architecture.
Event Flow Analysis
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β FEISHU MESSAGE FLOW β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£ β β β Human User β Group Chat β β β β β Feishu Server β β β β β im.message.receive_v1 Webhook βββββββ All Bots Receive Event β β β DELIVERED β β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β Bot-A β Group Chat β β β β β Feishu Server β β β β β Event Filter (app_scope check) β β β β β im.message.receive_v1 Webhook βββββββ Other Bots: NO EVENT β β β FILTERED β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The app_scope Filtering Mechanism
Feishu’s webhook system validates the open_id of the sending bot against the receiving bot’s app_scope. Bot messages from different app scopes are excluded from webhook delivery to prevent bot-to-bot loops and unauthorized inter-bot communication.
Relevant Code Path (Hypothetical)
python
Feishu SDK internal event filtering (simplified)
class FeishuEventFilter: def should_deliver(self, message_event): sender_open_id = message_event.get(‘sender’, {}).get(‘sender_id’, {}).get(‘open_id’)
# Bot-to-bot filtering: reject if sender is another bot
if self._is_bot_account(sender_open_id):
return False # Other bots should not receive this
return True # Human messages are delivered
Comparison with Discord
Discord’s MESSAGE_CREATE gateway event provides no such filtering β all bots in the same channel receive all messages regardless of sender type. This enables native multi-bot collaboration:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β DISCORD MESSAGE FLOW β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£ β β β Any Message (Human or Bot) β Guild Channel β β β β β Discord Gateway β β β β β MESSAGE_CREATE Event βββββββ All Bots Receive β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Architectural Gap in OpenClaw
OpenClaw’s channel abstraction layer does not currently compensate for Feishu’s bot-message filtering. Without intervention, the framework treats Feishu as if it had Discord-style event delivery, resulting in silent message loss for inter-bot communication.
Anti-Patterns Enabled by Current Architecture
The current architecture prevents:
- Multi-Agent Context Sharing β One bot cannot provide context to another
- Discussion Chains β Multiple bots cannot discuss/debate in shared group
- Specialized Agent Handoffs β Hand-off patterns require human relay
- Cross-Bot Logging/Audit β Audit bots cannot observe other bots
π οΈ Step-by-Step Fix
Implementation Overview
The fix implements a Send-Time Cross-Bot Broadcast mechanism: when a bot sends a message to a Feishu group, the OpenClaw framework injects synthetic message events into the handlers of other registered bots in that same group.
Phase 1: Configuration Schema
Add the following configuration to your OpenClaw config.yaml or config.json:
yaml
config.yaml
channels: feishu: crossBotBroadcast: enabled: true maxConsecutiveBotTurns: 20 # Existing Feishu config remains unchanged appId: “${FEISHU_APP_ID}” appSecret: “${FEISHU_APP_SECRET}” botName: “OpenClaw-Bot”
json // config.json { “channels”: { “feishu”: { “crossBotBroadcast”: { “enabled”: true, “maxConsecutiveBotTurns”: 20 }, “appId”: “cli_xxx”, “appSecret”: “${FEISHU_APP_SECRET}” } } }
Configuration Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable cross-bot broadcast |
maxConsecutiveBotTurns | integer | 20 | Anti-loop threshold per group |
Phase 2: Bot Registry Implementation
Each bot must register its active group memberships at startup:
typescript // src/channels/feishu/FeishuChannel.ts
interface BotRegistration {
botId: string;
activeGroups: Set
class FeishuChannelManager { private botRegistry: Map<string, BotRegistration> = new Map();
registerBot(botId: string, groups: string[]): void {
this.botRegistry.set(botId, {
botId,
activeGroups: new Set(groups),
consecutiveBotTurns: new Map()
});
logger.info(Bot ${botId} registered for groups: ${groups.join(', ')});
}
getBotsInGroup(groupId: string): string[] { const bots: string[] = []; for (const [botId, registration] of this.botRegistry) { if (registration.activeGroups.has(groupId)) { bots.push(botId); } } return bots; }
getOtherBotsInGroup(senderBotId: string, groupId: string): string[] { const allBots = this.getBotsInGroup(groupId); return allBots.filter(botId => botId !== senderBotId); } }
Phase 3: Cross-Bot Broadcast Hook
Implement the broadcast trigger on message send:
typescript // src/channels/feishu/FeishuChannel.ts
async sendMessage(groupId: string, content: string, senderBotId: string): Promise
// Step 2: If crossBotBroadcast enabled, inject synthetic events const config = this.getConfig(); if (config.crossBotBroadcast?.enabled) { await this.broadcastToOtherBots(senderBotId, groupId, content, messageId); } }
private async broadcastToOtherBots(
senderBotId: string,
groupId: string,
content: string,
messageId: string
): Promise
if (otherBotIds.length === 0) { return; // No other bots to notify }
// Anti-loop check
if (!this.checkAntiLoopProtection(senderBotId, groupId)) {
logger.warn(Anti-loop threshold reached for bot ${senderBotId} in group ${groupId});
return;
}
// Create synthetic message event const syntheticEvent = this.createSyntheticEvent(senderBotId, groupId, content, messageId);
// Inject to all other bots in the group for (const recipientBotId of otherBotIds) { await this.injectSyntheticEvent(recipientBotId, syntheticEvent); }
// Increment consecutive bot turn counter this.incrementBotTurnCounter(senderBotId, groupId); }
private createSyntheticEvent( senderBotId: string, groupId: string, content: string, messageId: string ): FeishuMessageEvent { return { eventType: ‘im.message.receive_v1’, message: { message_id: messageId, chat_id: groupId, sender: { sender_id: { open_id: senderBotId }, sender_type: ‘bot’, is_bot: true }, content: content, create_time: new Date().toISOString() }, _meta: { synthetic: true, originatingBot: senderBotId, broadcastSource: ‘crossBotBroadcast’ } }; }
Phase 4: Anti-Loop Protection
Implement the consecutive bot turn counter with reset on human messages:
typescript // src/channels/feishu/FeishuChannel.ts
private checkAntiLoopProtection(botId: string, groupId: string): boolean { const config = this.getConfig(); const maxTurns = config.crossBotBroadcast?.maxConsecutiveBotTurns || 20;
const registration = this.botRegistry.get(botId); if (!registration) return true;
const currentTurns = registration.consecutiveBotTurns.get(groupId) || 0; return currentTurns < maxTurns; }
private incrementBotTurnCounter(botId: string, groupId: string): void { const registration = this.botRegistry.get(botId); if (!registration) return;
const currentTurns = registration.consecutiveBotTurns.get(groupId) || 0; registration.consecutiveBotTurns.set(groupId, currentTurns + 1); }
// Called when a human message is received β resets the counter
public resetBotTurnCounter(groupId: string): void {
for (const [, registration] of this.botRegistry) {
registration.consecutiveBotTurns.delete(groupId);
}
logger.info(Bot turn counters reset for group ${groupId} due to human message);
}
Phase 5: Human Message Detection Integration
Modify the webhook handler to reset counters on human messages:
typescript // src/channels/feishu/FeishuWebhookHandler.ts
async handleIncomingMessage(event: FeishuWebhookEvent): Promise
if (isHumanMessage) { // Reset all bot turn counters for this group this.feishuChannel.resetBotTurnCounter(event.message.chat_id); }
// Process the message through normal handler await this.messageHandler.process(event); }
Phase 6: Before/After Configuration Comparison
Before (Single-Bot Only)
yaml channels: feishu: appId: “${FEISHU_APP_ID}” appSecret: “${FEISHU_APP_SECRET}” botName: “Primary-Bot”
After (Multi-Bot Enabled)
yaml channels: feishu: appId: “${FEISHU_APP_ID}” appSecret: “${FEISHU_APP_SECRET}” botName: “Primary-Bot” crossBotBroadcast: enabled: true maxConsecutiveBotTurns: 20 botRegistry: - botId: “bot_primary” groups: - “engineering_group” - “general_chat” - botId: “bot_analyst” groups: - “engineering_group” - “analysis_queue” - botId: “bot_auditor” groups: - “engineering_group” - “audit_log”
π§ͺ Verification
Test Procedure 1: Basic Cross-Bot Delivery
bash
Setup: 3 bots registered in group “engineering_test”
Bot-A, Bot-B, Bot-C all joined to “engineering_test”
Step 1: Send message as Bot-A
curl -X POST “http://localhost:3000/api/feishu/send”
-H “Content-Type: application/json”
-d ‘{
“botId”: “bot_a”,
“groupId”: “engineering_test”,
“content”: “Test message from Bot-A for cross-bot broadcast”
}’
Expected: Bot-B and Bot-C receive synthetic events
Step 2: Check Bot-B logs
grep “synthetic” /var/log/openclaw/bot_b.log | tail -5
Expected output:
[OpenClaw] INFO: Synthetic message event received
[OpenClaw] INFO: originatingBot: bot_a
[OpenClaw] INFO: broadcastSource: crossBotBroadcast
[OpenClaw] INFO: content: “Test message from Bot-A…”
Test Procedure 2: Anti-Loop Verification
bash
Step 1: Configure with low threshold for testing
config.yaml: maxConsecutiveBotTurns: 3
Step 2: Send 4 consecutive bot messages
for i in {1..4}; do
curl -X POST “http://localhost:3000/api/feishu/send”
-H “Content-Type: application/json”
-d “{"botId": "bot_a", "groupId": "test_group", "content": "Turn $i"}”
done
Expected: 4th message should be blocked by anti-loop protection
Step 3: Verify logs
grep “Anti-loop|consecutive” /var/log/openclaw/bot_a.log | tail -5
Expected output:
[OpenClaw] WARN: Anti-loop threshold reached for bot bot_a in group test_group
[OpenClaw] INFO: Bot turn counter: 3/3
Step 4: Send human message to reset counter
curl -X POST “http://localhost:3000/api/feishu/webhook”
-H “Content-Type: application/json”
-d ‘{
“eventType”: “im.message.receive_v1”,
“message”: {
“chat_id”: “test_group”,
“sender”: {“sender_type”: “user”},
“content”: “Human user message”
}
}’
Step 5: Verify counter reset and bot messages resume
grep “reset” /var/log/openclaw/bot_a.log
Expected: [OpenClaw] INFO: Bot turn counters reset for group test_group due to human message
Test Procedure 3: Bot Registry Verification
bash
Step 1: Query active bot registry
curl -X GET “http://localhost:3000/api/feishu/bots/groups/test_group”
Expected response:
{ “groupId”: “test_group”, “registeredBots”: [“bot_a”, “bot_b”, “bot_c”], “count”: 3 }
Step 2: Verify isolation β bots in different groups don’t receive broadcasts
curl -X POST “http://localhost:3000/api/feishu/send”
-H “Content-Type: application/json”
-d ‘{“botId”: “bot_a”, “groupId”: “exclusive_group”, “content”: “Private message”}’
Verify bot_c (not in exclusive_group) does NOT receive
grep “Synthetic” /var/log/openclaw/bot_c.log | grep “exclusive_group”
Expected: No results (bot_c should not receive)
Test Procedure 4: Exit Code Validation for CLI Tests
bash #!/bin/bash
test_crossbot.sh
BOT_A=“bot_a” GROUP=“verification_test_group”
echo “=== Test 1: Cross-Bot Delivery ===”
curl -s -X POST “http://localhost:3000/api/feishu/send”
-H “Content-Type: application/json”
-d “{"botId": "$BOT_A", "groupId": "$GROUP", "content": "Verification Test"}”
sleep 2
BOT_B_SYNTHETIC=$(grep -c “Synthetic message event” /var/log/openclaw/bot_b.log 2>/dev/null || echo “0”) BOT_C_SYNTHETIC=$(grep -c “Synthetic message event” /var/log/openclaw/bot_c.log 2>/dev/null || echo “0”)
if [ “$BOT_B_SYNTHETIC” -gt 0 ] && [ “$BOT_C_SYNTHETIC” -gt 0 ]; then echo “β Test 1 PASSED: Cross-bot delivery working” exit 0 else echo “β Test 1 FAILED: Expected synthetic events in Bot-B and Bot-C logs” exit 1 fi
Expected Verification Output
β Cross-Bot Delivery: SUCCESS
- Bot-B received synthetic event from Bot-A
- Bot-C received synthetic event from Bot-C
β Anti-Loop Protection: SUCCESS
- 4th consecutive bot message blocked
- Counter incremented correctly
β Human Message Reset: SUCCESS
- Counter reset on human message
- Bot messages resumed after reset
β Bot Registry Isolation: SUCCESS
- Bots outside group did not receive broadcast
β οΈ Common Pitfalls
Pitfall 1: Infinite Broadcast Loops
Problem: Without proper anti-loop protection, bots can trigger cascading message storms.
Symptoms: bash [OpenClaw] ERROR: Stack overflow in message handler [OpenClaw] FATAL: Maximum call stack size exceeded
Mitigation: yaml
Always set maxConsecutiveBotTurns
channels: feishu: crossBotBroadcast: enabled: true maxConsecutiveBotTurns: 20 # Never disable completely
Additional safeguard β message deduplication:
typescript
private async broadcastToOtherBots(…): Promise${senderBotId}:${messageId};
if (this.processedMessages.has(messageKey)) {
return; // Already processed this broadcast
}
this.processedMessages.set(messageKey, Date.now());
// Cleanup old entries (older than 5 minutes) const cutoff = Date.now() - 5 * 60 * 1000; for (const [key, timestamp] of this.processedMessages) { if (timestamp < cutoff) this.processedMessages.delete(key); } }
Pitfall 2: Environment Variable Misconfiguration in Docker
Problem: Bot registry groups may not load correctly when using Docker Compose with environment variable substitution.
Symptoms: bash [OpenClaw] WARN: Bot registry empty β no groups configured [OpenClaw] ERROR: Cannot read property ‘activeGroups’ of undefined
Mitigation: yaml
docker-compose.yml β Use YAML anchor for shared group lists
x-shared-groups: &shared-groups
- “engineering”
- “general”
services: bot_a: environment: - FEISHU_APP_ID=${FEISHU_APP_ID_A} volumes: - ./config/bot_a.yaml:/app/config.yaml command: [“node”, “dist/bot.js”, “–config”, “/app/config.yaml”]
bot_b: environment: - FEISHU_APP_ID=${FEISHU_APP_ID_B} volumes: - ./config/bot_b.yaml:/app/config.yaml command: [“node”, “dist/bot.js”, “–config”, “/app/config.yaml”]
Pitfall 3: macOS Development β Port Conflicts
Problem: Running multiple bots on macOS may encounter port conflicts or event loop issues due to Darwin’s BSD networking stack.
Symptoms: bash Error: EADDRINUSE :::3000
or
[OpenClaw] WARN: Webhook delivery delayed β connection timeout
Mitigation: bash
Use different ports per bot instance
BOT_A_PORT=3000 BOT_B_PORT=3001 BOT_C_PORT=3002
Or use localhost with unique paths
bot-a: http://localhost:3000/hooks/feishu-a bot-b: http://localhost:3000/hooks/feishu-b
Pitfall 4: Windows Path Separator in Registry Config
Problem: Windows uses backslash path separators, which can corrupt YAML config loading.
Symptoms: bash Error: Bad YAML indentation
or
channels.feishu\crossBotBroadcast: null # Config not loaded
Mitigation: json // Use config.json on Windows (avoids escape issues) { “channels”: { “feishu”: { “crossBotBroadcast”: { “enabled”: true, “maxConsecutiveBotTurns”: 20 } } } }
Pitfall 5: Bot Mention Collision (#42410 Related)
Problem: Multiple bots with similar names in the same group can cause @mention parsing failures.
Symptoms: bash [OpenClaw] WARN: Multiple bot mentions detected β ambiguous targeting [OpenClaw] ERROR: Cannot resolve bot from mention @Bot
Mitigation: yaml channels: feishu: crossBotBroadcast: enabled: true mentionResolution: preferExplicitMention: true # Require @bot_id format allowPartialMatch: false # Disable fuzzy matching
Pitfall 6: Race Conditions in Bot Registration
Problem: Bot A may send a message before Bot B has registered, causing Bot B to miss broadcasts.
Symptoms: bash [OpenClaw] WARN: Recipient bot not in registry β message dropped [OpenClaw] INFO: Bot B registered after message sent
Mitigation: typescript // Implement delayed broadcast or retry queue private messageQueue: Map<string, QueuedMessage[]> = new Map();
private async injectSyntheticEvent(botId: string, event: FeishuMessageEvent): Promise
if (!registration) {
// Queue for retry when bot registers
if (!this.messageQueue.has(botId)) {
this.messageQueue.set(botId, []);
}
this.messageQueue.get(botId).push({ event, timestamp: Date.now() });
logger.info(Message queued for bot ${botId} β not yet registered);
return;
}
await this.deliverEvent(botId, event); }
// Call after bot registration completes public flushMessageQueue(botId: string): void { const queued = this.messageQueue.get(botId) || []; for (const { event } of queued) { this.deliverEvent(botId, event); } this.messageQueue.delete(botId); }
Pitfall 7: Feishu API Rate Limiting
Problem: Broadcasting to many bots in rapid succession may trigger Feishu API rate limits.
Symptoms: bash [Feishu API] ERROR 429: Too Many Requests [OpenClaw] WARN: Rate limit exceeded β backing off
Mitigation:
typescript
private async broadcastToOtherBots(…): Promise
// Sequential delivery with rate limiting for (const recipientBotId of otherBotIds) { await this.injectSyntheticEvent(recipientBotId, syntheticEvent); await this.delay(100); // 100ms between injections } }
private delay(ms: number): Promise
π Related Errors
Cross-Channel Multi-Bot Issues
| Issue | Platform | Root Cause | Status |
|---|---|---|---|
| #42410 | Feishu | Multi-bot @mention matching fails when multiple bots with similar names exist | Open |
| #40768 | Feishu | app_scope open_id causes bot messages to be invisible to other bots | Open |
| #38654 | Telegram | Multiple bots in same group cannot see each other’s messages | Open |
| #29173 | Telegram | Telegram multi-bot mention collision β @Bot matches multiple bots | Closed |
Platform Parity Matrix
| Feature | Discord | Feishu | Telegram | Status |
|---|---|---|---|---|
| Native bot-to-bot events | β Yes | β Filtered | β Filtered | Feature Gap |
@mention resolution | β Robust | β οΈ Partial | β οΈ Partial | In Progress |
| Group multi-bot support | β Full | β None | β None | Needs Solution |
| Cross-bot broadcast (proposed) | N/A | π Implementing | π Implementing | This Issue |
Architectural Patterns for Cross-Bot Support
Poll-Based Broadcast (Alternative to send-time)
- Periodic polling of bot’s sent messages via API
- Higher latency, additional API calls
- Example: Telegram
getUpdatespolling loop
Event Bus Pattern (General solution)
- Internal OpenClaw event bus
- All channel adapters publish events
- Receivers subscribed regardless of platform
Gateway Relay (Discord-style)
- Single gateway instance routes all messages
- Works for centralized platforms only
Related Error Codes
| Error Code | Description | Resolution |
|---|---|---|
ERR_FEISHU_BOT_MESSAGE_FILTERED | Feishu webhook rejected bot-originated message | Use crossBotBroadcast |
ERR_TELEGRAM_BOT_ISOLATION | Telegram bot-only updates not delivered | Implement broadcast pattern |
ERR_CROSSBOT_LOOP_DETECTED | Anti-loop threshold exceeded | Wait for human message or increase threshold |
ERR_BOT_NOT_REGISTERED | Recipient bot not in registry | Ensure all bots register on startup |
ERR_RATE_LIMIT_EXCEEDED | Feishu API throttling | Add delay between broadcasts |