# Messages & Events Understanding how to send messages and events is critical for building conversational AI agents. This guide covers all message types, when to use them, and common pitfalls. ## Table of Contents **Quick Links:** - [Core Concepts](#core-concepts) - Messages vs Events, Agnostic vs Channel-Specific - [Sending Messages in Conversations](#sending-messages-in-conversations) - Using `conversation.send()` - [Message Metadata](#message-metadata-chat-integration) - Adding custom data to messages - [All Message Types](#all-message-types) - Complete reference (text, image, file, card, etc.) - [File Messages & Metadata](#file-message-metadata) - File-specific metadata usage - [Custom Component (Webchat)](#custom-component-webchat) - Webchat-only custom UI components - [Accessing Messages on Frontend](#accessing-messages-on-the-frontend) - Webchat SDK integration - [Sending from Workflows](#sending-messages-from-workflows) - Using `client.createMessage()` - [Sending Events (Ephemeral)](#sending-events-ephemeral) - Non-persistent notifications - [Channel Compatibility](#channel-compatibility) - What works where - [Common Mistakes](#common-mistakes) - Pitfalls to avoid - [Best Practices](#best-practices) - Recommended patterns ## Core Concepts ### Messages vs Events **Messages** are persistent conversation history items that users can see: - Appear in chat history - Survive page reloads - Part of conversation context - Sent using `conversation.send()` in conversations **Events** are ephemeral signals that don't persist: - NOT in conversation history - Lost on page reload - Used for real-time notifications - Sent using integration actions (e.g., `bp.webchat.actions.customEvent()`) ### Agnostic vs Channel-Specific **Agnostic Messages** work across multiple channels (defined in `@botpress/sdk`): - `text`, `image`, `audio`, `video`, `file`, `location` - `card`, `carousel`, `choice`, `dropdown`, `bloc`, `markdown` **Channel-Specific Messages** only work in specific integrations: - `customComponent` (webchat only) - Platform-specific fields (e.g., Slack threads) ## Sending Messages in Conversations ### Basic Syntax In conversation handlers, use `conversation.send()`: ```typescript import { Conversation } from "@botpress/runtime"; export const Chat = new Conversation({ channel: "chat.channel", async handler({ message, conversation }) { // Send a message await conversation.send({ type: "text", payload: { text: "Hello!" } }); } }); ``` **CRITICAL**: Never use `client.createMessage()` directly in conversations. Always use `conversation.send()`. ```typescript // ❌ WRONG - Don't use client directly await client.createMessage({ conversationId: conversation.id, type: "text", payload: { text: "Hello" } }); // ✅ CORRECT - Use conversation.send() await conversation.send({ type: "text", payload: { text: "Hello" } }); ``` ### Why Use `conversation.send()`? - Automatically handles conversation context - Type-safe for the channel - Manages message tracking - Integrates with ADK runtime ## Message Metadata (Chat Integration) The **chat** integration supports an optional `metadata` field on **all message types**. This allows you to attach arbitrary custom data that will be available to the frontend. ```typescript await conversation.send({ type: "text", payload: { text: "Your order has been confirmed", metadata: { orderId: "order_123", customerId: "cust_456", trackingNumber: "TRACK789", internalRef: "ref_abc", // Any custom data you need } } }); ``` **Metadata Schema**: - Type: `Record` (key-value pairs) - Keys: strings - Values: any type (string, number, boolean, object, array) - Optional on all message types **Use Cases**: - File identifiers for tracking - Unique keys for deduplication - Custom display properties - Analytics tracking data - Frontend state management **Channel Support**: - ✅ **chat**: Full metadata support on all message types - ❓ **webchat**: Check generated types after adding dependency - ❓ **Other channels**: Varies by integration ## All Message Types ### Text Basic text message - works everywhere. ```typescript await conversation.send({ type: "text", payload: { text: "Hello, world!" } }); ``` ### Markdown Formatted text with markdown - **channel support varies**. ```typescript await conversation.send({ type: "markdown", payload: { markdown: "**Bold** _italic_ `code` [link](https://example.com)" } }); ``` **Note**: Markdown support depends on the channel. Use `text` for guaranteed cross-channel compatibility. ### Image Display an image from a URL. ```typescript await conversation.send({ type: "image", payload: { imageUrl: "https://example.com/image.png" } }); ``` ### Audio Play an audio file. ```typescript await conversation.send({ type: "audio", payload: { audioUrl: "https://example.com/audio.mp3" } }); ``` ### Video Display a video player. ```typescript await conversation.send({ type: "video", payload: { videoUrl: "https://example.com/video.mp4" } }); ``` ### File Share a downloadable file with optional metadata. ```typescript await conversation.send({ type: "file", payload: { fileUrl: "https://example.com/document.pdf", title: "Invoice", // optional metadata: { // optional - see Message Metadata section fileId: "file_123", uniqueKey: "invoice-2024-001" } } }); ``` ### Location Share a geographic location. ```typescript await conversation.send({ type: "location", payload: { latitude: 40.7128, longitude: -74.0060, address: "New York, NY", // optional title: "Meeting Location" // optional } }); ``` ### Card Rich card with image, title, and action buttons. ```typescript await conversation.send({ type: "card", payload: { title: "Product Name", subtitle: "Product description", // optional imageUrl: "https://example.com/product.jpg", // optional actions: [ { action: "postback", // or "url" or "say" label: "Buy Now", value: "buy_product_123" }, { action: "url", label: "Learn More", value: "https://example.com/product" } ] } }); ``` **Action Types**: - `postback`: Sends a value back to the bot (triggers new message) - `url`: Opens a URL in browser - `say`: Makes user "say" text (sends as user message) ### Carousel Multiple cards in a scrollable carousel. ```typescript await conversation.send({ type: "carousel", payload: { items: [ { title: "Product 1", subtitle: "Description 1", imageUrl: "https://example.com/product1.jpg", actions: [ { action: "postback", label: "Select", value: "product_1" } ] }, { title: "Product 2", subtitle: "Description 2", imageUrl: "https://example.com/product2.jpg", actions: [ { action: "postback", label: "Select", value: "product_2" } ] } ] } }); ``` ### Choice (Buttons) Quick reply buttons for user selection. ```typescript await conversation.send({ type: "choice", payload: { text: "What would you like to do?", options: [ { label: "Option 1", value: "opt1" }, { label: "Option 2", value: "opt2" }, { label: "Option 3", value: "opt3" } ], disableFreeText: false // optional - prevent typing } }); ``` ### Dropdown Dropdown/select menu for user selection. ```typescript await conversation.send({ type: "dropdown", payload: { text: "Select your country:", options: [ { label: "United States", value: "us" }, { label: "Canada", value: "ca" }, { label: "United Kingdom", value: "uk" } ], disableFreeText: true // optional } }); ``` ### Bloc Multiple messages grouped together (composite message). ```typescript await conversation.send({ type: "bloc", payload: { items: [ { type: "text", payload: { text: "Here's your order summary:" } }, { type: "image", payload: { imageUrl: "https://example.com/product.jpg" } }, { type: "text", payload: { text: "Total: $99.99" } } ] } }); ``` **Allowed item types in bloc**: - `text`, `markdown`, `image`, `audio`, `video`, `file`, `location` ### Custom Component (Webchat) Send a custom React component in webchat conversations using the `customComponent` message type: ```typescript import { WelcomeBannerComponent } from '../components' await conversation.send({ type: "customComponent", payload: { component: WelcomeBannerComponent, props: {}, } }); ``` Components are `.bp.tsx` files registered in `src/components/index.ts` with `new CustomComponent()`. See the component registry reference for the full creation flow. When components are listed in a `Conversation`'s `components` array, the LLM can also yield them autonomously during `execute()` — no explicit `send()` needed. **Note**: Custom components only work in webchat channels. For other channels, use agnostic message types like `card` or `carousel`. ## Accessing Messages on the Frontend Messages (including metadata) are available to frontend clients via the webchat SDK. ### Listening to New Messages ```javascript // React to new messages as they arrive window.botpressWebChat.onMessage((message) => { console.log('New message:', message); if (message.type === 'file') { console.log('File URL:', message.payload.fileUrl); console.log('File title:', message.payload.title); console.log('Metadata:', message.payload.metadata); // Access custom metadata const { fileId, uniqueKey, customData } = message.payload.metadata || {}; // Fetch file contents if needed fetch(message.payload.fileUrl) .then(response => response.text()) .then(content => { console.log('File content:', content); }); } }); ``` ### Reconstructing State from Message History On page reload, iterate through conversation history to reconstruct application state: ```javascript // Get all messages from conversation history window.botpressWebChat.getMessages().then(messages => { // Filter for file messages with specific metadata const componentFiles = messages.filter(msg => msg.type === 'file' && msg.payload.metadata?.fileType === 'component' ); // Reconstruct state from persisted messages componentFiles.forEach(async (msg) => { const { fileUrl, metadata } = msg.payload; const { componentId, stepId } = metadata; // Fetch and restore component const content = await fetch(fileUrl).then(r => r.text()); restoreComponent(componentId, stepId, content); }); }); ``` ### Message Persistence Benefits Messages (unlike events) are: - ✅ Persisted in conversation history - ✅ Replayed on page reload - ✅ Available via `getMessages()` API - ✅ Include all metadata This makes messages ideal for: - State reconstruction after page reload - File tracking and retrieval - Workflow progress indicators - Audit trails ## Sending Messages from Workflows Workflows can't use `conversation.send()` - they must use `client.createMessage()`: ```typescript import { Workflow } from "@botpress/runtime"; export const NotifyWorkflow = new Workflow({ name: "notify", input: z.object({ conversationId: z.string(), // REQUIRED for messaging! message: z.string() }), handler: async ({ input, client, step }) => { await step("send-notification", async () => { await client.createMessage({ conversationId: input.conversationId, type: "text", payload: { text: input.message } }); }); } }); ``` **CRITICAL**: Always pass `conversationId` to workflows that need to send messages (see Common Mistakes section for details). ## Sending Events (Ephemeral) Events are **NOT messages** - they don't persist in conversation history. Use integration actions to send ephemeral events (see [Integration Actions for Messages](#integration-actions-for-messages) section for examples). ### When to Use Events vs Messages **Use Messages** (persistent) when: - User needs to see it in history - It's part of the conversation flow - You want it to survive page reloads - Examples: chat responses, confirmations, results **Use Events** (ephemeral) when: - Real-time notifications (typing indicators, progress) - UI state changes (show/hide widget, config updates) - Temporary feedback (loading, processing) - Examples: "Agent is typing...", "File uploading..." ## Integration Actions for Messages Some integrations provide actions for special message types and events: ```typescript // Webchat - Send custom event (ephemeral, not persisted) await client.callAction({ type: "webchat:customEvent", input: { conversationId: conversation.id, event: JSON.stringify({ type: "notification", message: "Processing..." }) } }); // Webchat - Show/hide widget await client.callAction({ type: "webchat:showWebchat", input: { conversationId: conversation.id } }); await client.callAction({ type: "webchat:hideWebchat", input: { conversationId: conversation.id } }); // Webchat - Update configuration await client.callAction({ type: "webchat:configWebchat", input: { conversationId: conversation.id, config: JSON.stringify({ theme: { primaryColor: "#ff0000" } }) } }); ``` **Frontend event handling**: ```javascript window.botpressWebChat.onEvent((event) => { if (event.type === 'notification') { showToast(event.message); } }, ['TRIGGER']); ``` ## Channel Compatibility Not all message types work in all channels: | Message Type | Chat | Webchat | Slack | WhatsApp | Teams | |-------------|------|---------|-------|----------|-------| | text | ✅ | ✅ | ✅ | ✅ | ✅ | | markdown | ✅ | ✅ | ✅ | ❌ | ✅ | | image | ✅ | ✅ | ✅ | ✅ | ✅ | | audio | ✅ | ✅ | ❌ | ✅ | ❌ | | video | ✅ | ✅ | ❌ | ✅ | ❌ | | file | ✅ | ✅ | ✅ | ✅ | ✅ | | location | ✅ | ✅ | ❌ | ✅ | ❌ | | card | ✅ | ✅ | ✅ | ❌ | ✅ | | carousel | ✅ | ✅ | ❌ | ❌ | ❌ | | choice | ✅ | ✅ | ✅ | ✅ | ✅ | | dropdown | ✅ | ✅ | ❌ | ✅ | ❌ | | bloc | ✅ | ✅ | ❌ | ❌ | ❌ | | customComponent | ❌ | ✅ (webchat only) | ❌ | ❌ | ❌ | **Tip**: Use `text` and `image` for maximum compatibility across channels. ## Common Mistakes ### 1. Using client.createMessage() in Conversations ```typescript // ❌ WRONG export const Chat = new Conversation({ async handler({ client }) { await client.createMessage({ /* ... */ }); } }); // ✅ CORRECT export const Chat = new Conversation({ async handler({ conversation }) { await conversation.send({ /* ... */ }); } }); ``` ### 2. Using client.createEvent() for Messages ```typescript // ❌ WRONG - Events don't persist! await client.createEvent({ conversationId: conversation.id, type: "text", payload: { text: "Hello" } }); // ✅ CORRECT - Use messages await conversation.send({ type: "text", payload: { text: "Hello" } }); ``` ### 3. Missing conversationId in Workflows ```typescript // ❌ WRONG - Can't send messages without conversationId export const MyWorkflow = new Workflow({ handler: async ({ input, client }) => { await client.createMessage({ // Where's conversationId? This will fail! type: "text", payload: { text: "Hello" } }); } }); // ✅ CORRECT - Pass conversationId in input export const MyWorkflow = new Workflow({ input: z.object({ conversationId: z.string() }), handler: async ({ input, client }) => { await client.createMessage({ conversationId: input.conversationId, type: "text", payload: { text: "Hello" } }); } }); ``` ### 4. Using Custom Components in Non-Webchat Channels ```typescript // ❌ WRONG - customComponent only works in webchat export const SlackChat = new Conversation({ channel: "slack.dm", async handler({ conversation }) { await conversation.send({ type: "customComponent", // Will fail in Slack! payload: { component: TicketCardComponent, props: { /* ... */ } } }); } }); // ✅ CORRECT - Use agnostic message types for non-webchat channels export const SlackChat = new Conversation({ channel: "slack.dm", async handler({ conversation }) { await conversation.send({ type: "card", // Works in Slack payload: { title: "Ticket", subtitle: "Details here", actions: [] } }); } }); ``` ## Best Practices ### 1. Type Safety TypeScript will help you with type-safe payloads: ```typescript // TypeScript knows the payload structure based on type await conversation.send({ type: "card", payload: { title: "Required", subtitle: "Optional", // IntelliSense will suggest all valid fields! } }); ``` ### 2. Graceful Degradation When supporting multiple channels: ```typescript import { TicketCardComponent } from "../components"; export const Chat = new Conversation({ async handler({ conversation }) { if (conversation.channel === "webchat.channel") { // Webchat supports custom components await conversation.send({ type: "customComponent", payload: { component: TicketCardComponent, props: { ticketId: "TKT-001", title: "VPN issue", priority: "high", status: "open" } } }); } else { // Fall back to card for other channels await conversation.send({ type: "card", payload: { title: "TKT-001: VPN issue", subtitle: "Priority: high · Status: open", actions: [] } }); } } }); ``` ### 3. Error Handling Always wrap message sending in try-catch: ```typescript try { await conversation.send({ type: "image", payload: { imageUrl: maybeInvalidUrl } }); } catch (error) { console.error("Failed to send image:", error); // Fall back to text await conversation.send({ type: "text", payload: { text: "Image unavailable" } }); } ``` ### 4. Message Composition Break complex messages into parts: ```typescript // Instead of one giant text await conversation.send({ type: "text", payload: { text: "Title\n\nBody text here\n\nFooter" } }); // Use bloc for better structure await conversation.send({ type: "bloc", payload: { items: [ { type: "markdown", payload: { markdown: "**Title**" } }, { type: "text", payload: { text: "Body text here" } }, { type: "text", payload: { text: "Footer" } } ] } }); ``` ## Summary **In Conversations**: Use `conversation.send()` for messages (persistent, type-safe) **In Workflows**: Use `client.createMessage()` with `conversationId` in input **Events**: Use integration actions for ephemeral notifications (don't persist) **Key Rules**: 1. ✅ Use `conversation.send()` in conversations, `client.createMessage()` in workflows 2. ✅ Always pass `conversationId` to workflows that send messages 3. ✅ Use messages for persistent content, events for ephemeral notifications 4. ✅ In webchat conversations, use `customComponent` message type with a `CustomComponent` reference 5. ❌ Never use `client.createMessage()` in conversations 6. ❌ Check channel compatibility for message types (see table above)