May 03, 2026 β€’ Version: 2026.2.26

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

PlatformBot-to-Bot Message EventsNative Multi-Bot Support
DiscordMESSAGE_CREATE events delivered to all botsβœ… Yes
FeishuHuman messages only via im.message.receive_v1❌ No
TelegramHuman 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:

  1. Multi-Agent Context Sharing β€” One bot cannot provide context to another
  2. Discussion Chains β€” Multiple bots cannot discuss/debate in shared group
  3. Specialized Agent Handoffs β€” Hand-off patterns require human relay
  4. 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:

ParameterTypeDefaultDescription
enabledbooleanfalseEnable cross-bot broadcast
maxConsecutiveBotTurnsinteger20Anti-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; consecutiveBotTurns: Map<string, number>; }

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 1: Send message via Feishu API (normal delivery to group) const messageId = await this.feishuApi.sendGroupMessage(groupId, content);

// 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 { const otherBotIds = this.getOtherBotsInGroup(senderBotId, groupId);

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 { const isHumanMessage = event.message?.sender?.sender_type !== ‘bot’;

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 { // Check message ID to prevent duplicate processing const messageKey = ${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 } } } }

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 { const registration = this.botRegistry.get(botId);

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 { const otherBotIds = this.getOtherBotsInGroup(senderBotId, groupId);

// 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 { return new Promise(resolve => setTimeout(resolve, ms)); }

Cross-Channel Multi-Bot Issues

IssuePlatformRoot CauseStatus
#42410FeishuMulti-bot @mention matching fails when multiple bots with similar names existOpen
#40768Feishuapp_scope open_id causes bot messages to be invisible to other botsOpen
#38654TelegramMultiple bots in same group cannot see each other’s messagesOpen
#29173TelegramTelegram multi-bot mention collision β€” @Bot matches multiple botsClosed

Platform Parity Matrix

FeatureDiscordFeishuTelegramStatus
Native bot-to-bot eventsβœ… Yes❌ Filtered❌ FilteredFeature Gap
@mention resolutionβœ… Robust⚠️ Partial⚠️ PartialIn Progress
Group multi-bot supportβœ… Full❌ None❌ NoneNeeds Solution
Cross-bot broadcast (proposed)N/AπŸ”„ ImplementingπŸ”„ ImplementingThis Issue

Architectural Patterns for Cross-Bot Support

  1. Poll-Based Broadcast (Alternative to send-time)

    • Periodic polling of bot’s sent messages via API
    • Higher latency, additional API calls
    • Example: Telegram getUpdates polling loop
  2. Event Bus Pattern (General solution)

    • Internal OpenClaw event bus
    • All channel adapters publish events
    • Receivers subscribed regardless of platform
  3. Gateway Relay (Discord-style)

    • Single gateway instance routes all messages
    • Works for centralized platforms only
Error CodeDescriptionResolution
ERR_FEISHU_BOT_MESSAGE_FILTEREDFeishu webhook rejected bot-originated messageUse crossBotBroadcast
ERR_TELEGRAM_BOT_ISOLATIONTelegram bot-only updates not deliveredImplement broadcast pattern
ERR_CROSSBOT_LOOP_DETECTEDAnti-loop threshold exceededWait for human message or increase threshold
ERR_BOT_NOT_REGISTEREDRecipient bot not in registryEnsure all bots register on startup
ERR_RATE_LIMIT_EXCEEDEDFeishu API throttlingAdd delay between broadcasts

Documentation References

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.