Skip to content

Fix/meta cloud api chatbot#2429

Open
sosamilton wants to merge 2 commits intoEvolutionAPI:mainfrom
sosamilton:fix/meta-cloud-api-chatbot
Open

Fix/meta cloud api chatbot#2429
sosamilton wants to merge 2 commits intoEvolutionAPI:mainfrom
sosamilton:fix/meta-cloud-api-chatbot

Conversation

@sosamilton
Copy link

@sosamilton sosamilton commented Feb 16, 2026

PR: fix(meta): Fix Cloud API chatbot auto-trigger and Chatwoot integration

📋 Description

Fixes two related bugs in the Meta WhatsApp Cloud API (BusinessStartupService) that prevent chatbot integrations (Typebot, OpenAI, etc.) from working correctly when combined with Chatwoot.

Bug 1: Closed bot sessions block new conversations

When a chatbot session completes (status = closed), the emit() method in BaseChatbotController returned early on any new message, permanently preventing the bot from re-activating for that contact.

Root cause: The guard if (session.status === 'closed') return was intended to skip sessions not awaiting input, but it also blocked the creation of new sessions after a flow completed.

Fix: Nullify the closed session instead of returning, so processBot enters the !session branch and creates a fresh session. Also adds null guards in getConversationMessage (returns '' instead of undefined) and findBotByTrigger (handles null/empty content gracefully, only matching all/none triggers).

Files changed:

  • src/api/integrations/chatbot/base-chatbot.controller.ts
  • src/utils/getConversationMessage.ts
  • src/utils/findBotByTrigger.ts

Bug 2: Cloud API processes Chatwoot AFTER bot and assigns wrong IDs

In the Baileys channel, message processing follows the order: Chatwoot first → webhook → bot. But in the Meta Cloud API channel (BusinessStartupService), the order was reversed: webhook → bot → Chatwoot. This inconsistency caused:

  1. The bot received messages before Chatwoot created the conversation, so chatwootConversationId and chatwootInboxId were not yet available during bot processing.
  2. When Chatwoot did process the message, both chatwootInboxId and chatwootConversationId were incorrectly assigned chatwootSentMessage.id (the message ID) instead of .inbox_id and .conversation_id respectively.

Fix: Reorder to Chatwoot-first (consistent with Baileys) and use the correct property names from the Chatwoot API response.

Files changed:

  • src/api/integrations/channel/meta/whatsapp.business.service.ts

🔗 Related Issue

This fixes chatbot integrations (Typebot, OpenAI, Dify, etc.) not auto-triggering on messages received via the Meta WhatsApp Cloud API, and incorrect Chatwoot metadata being persisted to the database.

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced
  • Tested with different connection types (if applicable)

Test scenarios verified:

  1. Cloud API + Typebot auto-trigger: New message from WhatsApp → Typebot bot starts correctly
  2. Cloud API + Chatwoot: Messages appear in Chatwoot with correct inbox_id and conversation_id
  3. Bot flow completion + re-trigger: After bot flow ends (session = closed), sending a new message correctly starts a fresh bot session
  4. Baileys channel regression: Verified Baileys channel continues to work identically (no regression)
  5. Null content messages: Media-only messages (images, audio) with no text content correctly match all/none trigger bots without errors

📸 Screenshots (if applicable)

N/A - Backend-only changes.

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

📝 Additional Notes

Changes summary (4 files, minimal diff)

File Change
base-chatbot.controller.ts session = null instead of return when session is closed (+2 lines)
getConversationMessage.ts return messageContent ?? '' (+1 char)
findBotByTrigger.ts Single all/none query + early return for empty/whitespace content (+8 lines)
whatsapp.business.service.ts Reorder Chatwoot-first + fix .inbox_id/.conversation_id + safer guard (+1/-0 net lines)

Summary by Sourcery

Fix Meta WhatsApp Cloud API chatbot handling so bot sessions can restart correctly and Chatwoot metadata is processed consistently with other channels.

Bug Fixes:

  • Allow new chatbot sessions to start after a previous session is closed instead of permanently blocking re-activation for that contact.
  • Handle null or empty message content safely when resolving chatbot triggers, including media-only messages.
  • Normalize Meta Cloud API processing order to send messages to Chatwoot before the bot, aligning with the Baileys channel.
  • Store the correct Chatwoot inbox and conversation identifiers from the API response instead of using the message ID for all fields.
  • Ensure conversation message extraction always returns a string, avoiding undefined values in downstream processing.

Sourcery AI review feedback (addressed)

All suggestions from the automated review have been incorporated:

  1. Duplicate all/none query in findBotByTrigger → Refactored to a single query at the top, with early return for empty content after the check.
  2. Whitespace-only content not treated as empty → Now uses content?.trim() || '' to normalize before checks.
  3. Guard condition chatwootSentMessage?.id → Changed to chatwootSentMessage since inbox_id and conversation_id are now different fields.
  4. Centralize closed session semantics → Acknowledged as a valid architectural improvement for a future refactor; out of scope for this minimal bug-fix PR.

Why this matters

Without these fixes, any Evolution API deployment using the Meta WhatsApp Cloud API with chatbot integrations will experience:

  • Bots not triggering on incoming messages (or only triggering once)
  • Incorrect chatwootInboxId/chatwootConversationId stored in the database
  • Inconsistent behavior between Baileys and Cloud API channels

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 16, 2026

Reviewer's Guide

Fixes two Meta WhatsApp Cloud API chatbot issues by allowing closed chatbot sessions to be recreated and by reordering Cloud API message handling to run Chatwoot first (and store correct Chatwoot IDs) before invoking webhooks and chatbot processing.

Sequence diagram for updated Meta Cloud API message processing order

sequenceDiagram
  actor WhatsAppUser
  participant MetaCloudAPI
  participant BusinessStartupService
  participant ChatwootService
  participant WebhookDispatcher
  participant BaseChatbotController

  WhatsAppUser->>MetaCloudAPI: send message
  MetaCloudAPI->>BusinessStartupService: webhook payload
  BusinessStartupService->>BusinessStartupService: build messageRaw
  BusinessStartupService->>BusinessStartupService: sendTelemetry

  Note over BusinessStartupService: Chatwoot first
  BusinessStartupService->>ChatwootService: eventWhatsapp MESSAGES_UPSERT messageRaw
  ChatwootService-->>BusinessStartupService: chatwootSentMessage
  BusinessStartupService->>BusinessStartupService: set messageRaw.chatwootMessageId
  BusinessStartupService->>BusinessStartupService: set messageRaw.chatwootInboxId from inbox_id
  BusinessStartupService->>BusinessStartupService: set messageRaw.chatwootConversationId from conversation_id

  BusinessStartupService->>WebhookDispatcher: sendDataWebhook MESSAGES_UPSERT messageRaw

  BusinessStartupService->>BaseChatbotController: emit instance remoteJid msg pushName
  BaseChatbotController-->>BusinessStartupService: chatbot processing complete

  BusinessStartupService->>BusinessStartupService: persist messageRaw to database
Loading

Sequence diagram for chatbot emit with closed session recreation

sequenceDiagram
  participant BusinessStartupService
  participant BaseChatbotController
  participant SessionStore
  participant GetConversationMessageUtil
  participant BotRepository

  BusinessStartupService->>BaseChatbotController: emit instance remoteJid msg pushName
  BaseChatbotController->>SessionStore: getSession remoteJid instance
  SessionStore-->>BaseChatbotController: session
  BaseChatbotController->>GetConversationMessageUtil: getConversationMessage msg
  GetConversationMessageUtil-->>BaseChatbotController: content or empty string

  alt session exists and status is closed
    BaseChatbotController->>BaseChatbotController: session = null
  end

  alt no session
    BaseChatbotController->>BotRepository: findBotByTrigger botRepository content instanceId
    BotRepository-->>BaseChatbotController: bot
    BaseChatbotController->>BaseChatbotController: processBot with new session
  else session not null and not closed
    BaseChatbotController->>BaseChatbotController: processBot with existing session
  end

  BaseChatbotController-->>BusinessStartupService: emit complete
Loading

Class diagram for BaseChatbotController and related utilities

classDiagram
  class BaseChatbotController {
    <<abstract>>
    +emit(instance remoteJid msg pushName) Promise~void~
    +getSession(remoteJid instance) Promise~BotSession~
    +processBot(instance remoteJid msg session content) Promise~void~
    +checkIgnoreJids(ignoreJids remoteJid) boolean
  }

  class BotSession {
    +status string
  }

  class GetConversationMessageUtil {
    +getConversationMessage(msg) string
  }

  class FindBotByTriggerUtil {
    +findBotByTrigger(botRepository content instanceId) Promise~BotType~
  }

  class BotRepository {
    +findFirst(where) Promise~BotType~
  }

  BaseChatbotController --> BotSession : uses
  BaseChatbotController --> GetConversationMessageUtil : uses
  BaseChatbotController --> FindBotByTriggerUtil : uses
  FindBotByTriggerUtil --> BotRepository : queries
Loading

File-Level Changes

Change Details Files
Allow chatbot flows to restart after a session is closed instead of being permanently blocked.
  • Change session variable from const to let so it can be nulled when closed
  • When a session has status 'closed', set the session to null so downstream logic creates a new session
  • Ensure conversation content resolution always returns a string, using an empty string when no content exists
src/api/integrations/chatbot/base-chatbot.controller.ts
src/utils/getConversationMessage.ts
Handle null/empty message content safely when resolving bots by trigger, especially for media-only messages.
  • Add an early guard that, for null/undefined/empty content, only searches for bots with 'all' or 'none' trigger types for the given instance
  • Preserve existing trigger resolution logic for non-empty content
src/utils/findBotByTrigger.ts
Normalize Meta Cloud API message flow to process Chatwoot before bot/webhook and store correct Chatwoot metadata on messages.
  • Move Chatwoot processing ahead of webhook dispatch and chatbot emit to match Baileys ordering
  • Map chatwootInboxId from inbox_id and chatwootConversationId from conversation_id instead of using the Chatwoot message id for all three fields
  • Send the webhook with the enriched message and only then invoke the chatbot controller
src/api/integrations/channel/meta/whatsapp.business.service.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In findBotByTrigger, you now hit the repository twice for triggerType in ['all','none'] when content is falsy; consider extracting a small helper or reusing the same query so the “all/none” lookup is performed only once and its precedence is clearer.
  • The content === '' guard in findBotByTrigger won’t treat whitespace-only messages as empty; if those should behave like media-only/no-text messages, consider normalizing with content?.trim() before the checks.
  • Now that closed sessions are nulled in BaseChatbotController.emit, review other call sites or session-related logic to see if this behavior should be centralized (e.g., inside getSession or a helper) so the semantics of closed sessions are consistent across the codebase.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `findBotByTrigger`, you now hit the repository twice for `triggerType in ['all','none']` when `content` is falsy; consider extracting a small helper or reusing the same query so the “all/none” lookup is performed only once and its precedence is clearer.
- The `content === ''` guard in `findBotByTrigger` won’t treat whitespace-only messages as empty; if those should behave like media-only/no-text messages, consider normalizing with `content?.trim()` before the checks.
- Now that closed sessions are nulled in `BaseChatbotController.emit`, review other call sites or session-related logic to see if this behavior should be centralized (e.g., inside `getSession` or a helper) so the semantics of closed sessions are consistent across the codebase.

## Individual Comments

### Comment 1
<location> `src/api/integrations/channel/meta/whatsapp.business.service.ts:679-683` </location>
<code_context>

           if (chatwootSentMessage?.id) {
             messageRaw.chatwootMessageId = chatwootSentMessage.id;
-            messageRaw.chatwootInboxId = chatwootSentMessage.id;
-            messageRaw.chatwootConversationId = chatwootSentMessage.id;
+            messageRaw.chatwootInboxId = chatwootSentMessage.inbox_id;
+            messageRaw.chatwootConversationId = chatwootSentMessage.conversation_id;
           }
         }
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Guard condition still depends on `id` even though inbox/conversation IDs now come from different fields.

Previously the guard on `chatwootSentMessage?.id` was equivalent for all three assignments because they shared the same value. Now `chatwootInboxId` and `chatwootConversationId` use different fields, a response with `inbox_id`/`conversation_id` but no `id` would be skipped. Unless the API guarantees `id` is always present with those fields, consider guarding on `chatwootSentMessage` or on the specific fields you’re assigning instead of `id`.

```suggestion
          if (chatwootSentMessage) {
            messageRaw.chatwootMessageId = chatwootSentMessage.id;
            messageRaw.chatwootInboxId = chatwootSentMessage.inbox_id;
            messageRaw.chatwootConversationId = chatwootSentMessage.conversation_id;
          }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

When a chatbot session exists with status='closed', the emit() method
returned early, preventing the bot from re-activating on new messages.

Root cause: the guard 'if (session.status === closed) return' was meant
to skip sessions not awaiting user input, but it also prevented new
conversations from starting after a bot flow completed.

Fix: nullify the session instead of returning, so processBot enters the
'!session' branch and creates a fresh session.

Also adds null guards:
- getConversationMessage: return empty string instead of undefined
- findBotByTrigger: handle null/undefined content gracefully
Two bugs in BusinessStartupService message processing:

1. Execution order: Chatwoot was processed AFTER the bot emit(), but
   Baileys channel processes Chatwoot FIRST. This inconsistency meant
   the bot could not access chatwootConversationId/chatwootInboxId
   when processing messages from the Cloud API.

2. chatwootIds assignment: chatwootInboxId and chatwootConversationId
   were both incorrectly set to chatwootSentMessage.id instead of
   .inbox_id and .conversation_id respectively.

Fix: reorder to Chatwoot-first (consistent with Baileys) and use the
correct property names from the Chatwoot response object.
@sosamilton sosamilton force-pushed the fix/meta-cloud-api-chatbot branch from 77b558a to cb4a14d Compare February 16, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant