# Conversations

Conversations handle incoming messages and route them based on channel. They're the primary interface between users and your bot, providing AI-powered responses and integration with workflows.

## Basic Concepts

### What are Conversations?
- **Channel-specific**: Each conversation handles a specific channel (chat, Slack, etc.)
- **Stateful**: Maintain state per conversation
- **AI-powered**: Built-in support for AI execution with tools and knowledge
- **Interactive**: Can trigger workflows and handle workflow requests

### File Location
- **Location**: `src/conversations/*.ts`
- **Auto-registration**: Files automatically register for their specified channel

## Creating Conversations

### Basic Structure

```typescript
import { Conversation, z } from "@botpress/runtime";

export const Chat = new Conversation({
  channel: "chat.channel", // Required: channel ID

  // Optional: Per-conversation state
  state: z.object({
    count: z.number().default(0),
    activeWorkflowId: z.string().optional()
  }),

  async handler({ message, state, conversation, execute, type, event, request, completion }) {
    // Access conversation state via the state parameter
    state.count += 1;

    // Send messages via conversation instance
    await conversation.send({
      type: "text",
      payload: { text: "Hello!" }
    });

    // AI-powered response
    await execute({
      instructions: "You are a helpful assistant",
      tools: [searchTool],
      knowledge: [MyKnowledgeBase]
    });
  }
});
```

### Handler Parameters

```typescript
async handler({ message, state, conversation, execute, type, event, request, completion, client }) {
  // message - Incoming message object
  // state - Conversation state (mutable, automatically tracked)
  // conversation - Conversation instance with send() and other methods
  // execute - AI execution function
  // type - Event type: "message" | "event" | "workflow_request" | "workflow_callback"
  // event - Raw event (typed per handler type — see workflow sections below)
  // request - Workflow request object (when type === "workflow_request")
  // completion - Workflow callback object (when type === "workflow_callback")
  // client - BotClient for API calls
}
```

> **Important:** Inside conversation handlers, always use the `conversation` parameter provided by the handler. Do **not** use `context.get("conversation")` — that pattern is for Actions, Tools, and Triggers that may optionally run within a conversation context. The handler parameter is already properly scoped and typed.

## Common Channel IDs

Different platforms have different channel IDs:

```typescript
// Botpress Chat
channel: "chat.channel"

// Slack
channel: "slack.dm"      // Direct messages
channel: "slack.channel" // Channel messages

// Discord
channel: "discord.dm"
channel: "discord.channel"

// WhatsApp
channel: "whatsapp.channel"

// Webchat
channel: "webchat.channel"

// Teams
channel: "teams.dm"
channel: "teams.channel"
```

## Message Handling

### Basic Message Processing

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ message, conversation, execute }) {
    // Check message type
    if (message?.type === "text") {
      const text = message.payload.text;

      // Command handling
      if (text.startsWith("/help")) {
        await conversation.send({
          type: "text",
          payload: { text: "Available commands:\n/help - Show help\n/status - Check status" }
        });
        return;
      }

      // Regular message - use AI
      await execute({
        instructions: "Help the user with their request"
      });
    }

    // Handle other message types
    if (message?.type === "image") {
      await conversation.send({
        type: "text",
        payload: { text: "I received your image!" }
      });
    }
  }
});
```

See **[Messages](./messages.md)** for complete guide on all message types, metadata, and sending patterns.

## Conversation Instance Methods

The `conversation` object provides methods for interacting with the current conversation.

### conversation.send()

Send messages to the conversation:

```typescript
// Send text message
await conversation.send({
  type: "text",
  payload: { text: "Hello!" }
});

// Send choice message
await conversation.send({
  type: "choice",
  payload: {
    text: "Choose an option:",
    options: [
      { label: "Option 1", value: "opt1" },
      { label: "Option 2", value: "opt2" }
    ]
  }
});
```

See **[Messages](./messages.md)** for all message types and payloads.

### conversation.startTyping()

Show typing indicator to the user:

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ conversation }) {
    // Start typing indicator
    await conversation.startTyping();

    // Do some work (API call, processing, etc.)
    await fetchData();

    // Send message (stops typing automatically)
    await conversation.send({
      type: "text",
      payload: { text: "Here's the data!" }
    });
  }
});
```

### conversation.stopTyping()

Manually stop typing indicator:

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ conversation }) {
    await conversation.startTyping();

    try {
      const result = await riskyOperation();
      await conversation.send({
        type: "text",
        payload: { text: result }
      });
    } catch (error) {
      // Stop typing if sending fails
      await conversation.stopTyping();
      throw error;
    }
  }
});
```

### conversation.tags

Access and modify conversation tags:

```typescript
// Read tags
const priority = conversation.tags.priority;
const category = conversation.tags.category;

// Set tags
conversation.tags.priority = "high";
conversation.tags.category = "support";
```

See **[Tags](./tags.md)** for complete tag documentation.

### Conversation Properties

```typescript
// Conversation ID
const id = conversation.id;

// Channel information
const channel = conversation.channel;        // e.g., "webchat.channel"
const integration = conversation.integration; // e.g., "webchat"
const alias = conversation.alias;            // Integration alias
```

## AI Execution

### Basic AI Integration

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ message, execute }) {
    await execute({
      instructions: "You are a helpful customer support assistant",

      // Optional: Add tools
      tools: [searchTool, createTicketTool],

      // Optional: Add knowledge bases
      knowledge: [DocsKnowledgeBase, FAQKnowledgeBase],

      // Optional: Model configuration
      model: "openai:gpt-4o",
      temperature: 0.7,

      // Optional: Execution hooks
      hooks: {
        onTrace: ({ trace }) => console.log(trace),
        onIterationEnd: async (iteration) => {
          if (iteration.isFailed()) {
            console.log("Iteration failed:", iteration.error);
          }
        }
      }
    });
  }
});
```

See **[Actions](./actions.md#converting-actions-to-tools)** for converting actions to tools with `.asTool()`.

## State Management

### Conversation State

Each conversation maintains its own state:

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  state: z.object({
    messageCount: z.number().default(0),
    userName: z.string().optional(),
    preferences: z.object({
      language: z.string().default("en"),
      theme: z.string().default("light")
    }).default({})
  }),

  async handler({ message, state, conversation }) {
    // Access and modify state via the state parameter
    state.messageCount += 1;

    if (!state.userName && message?.type === "text") {
      state.userName = message.payload.text;
      await conversation.send({
        type: "text",
        payload: { text: `Nice to meet you, ${state.userName}!` }
      });
    }

    // State persists across messages
    if (state.messageCount > 10) {
      await conversation.send({
        type: "text",
        payload: { text: "Thanks for being an active user!" }
      });
    }
  }
});
```

## Workflow Integration

### Starting Workflows from Conversations

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",
  state: z.object({
    activeWorkflowId: z.string().optional()
  }),

  async handler({ message, state, conversation }) {
    const text = message?.payload.text || "";

    if (text.startsWith("/process")) {
      // Start workflow with conversation context
      const instance = await ProcessWorkflow.start({
        conversationId: conversation.id, // Critical for communication!
        userId: user.id,
        data: text.substring(8)
      });

      // Store workflow ID
      state.activeWorkflowId = instance.id;

      await conversation.send({
        type: "text",
        payload: { text: `Started processing: ${instance.id}` }
      });
      return;
    }

    if (text === "/status" && state.activeWorkflowId) {
      // Check workflow status
      const instance = await ProcessWorkflow.getOrCreate({
        key: state.activeWorkflowId,
        input: { /* required input */ }
      });

      await conversation.send({
        type: "text",
        payload: {
          text: `Status: ${instance?.status || "Not found"}`
        }
      });
      return;
    }
  }
});
```

### Handling Workflow Requests

Workflows can request data from conversations using `step.request()`. The conversation receives a `workflow_request` type with the request object.

**Request Object Structure:**
```typescript
type WorkflowRequest = {
  type: `${WorkflowName}:${RequestName}`;  // e.g., "processOrder:email"
  workflow: BaseWorkflowInstance;           // Workflow instance
  step: string;                             // Step name
}
```

**Using the Type Discriminant (Recommended)**

When `type === "workflow_request"`, both `request` and `event` are properly typed:

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ type, request, event, conversation, message }) {
    if (type === "workflow_request") {
      // request is typed as WorkflowRequest
      // event is typed as WorkflowDataRequestEventType
      // event.payload.message contains the prompt from step.request()

      await conversation.send({
        type: "text",
        payload: { text: event.payload.message }
      });

      // Check specific request type
      if (request.type === "processOrder:email") {
        const email = message?.payload.text || "user@example.com";
        await request.workflow.provide("email", { email });
      }
      return;
    }
  }
});
```

**Legacy: Using `isWorkflowDataRequest` Type Guard (Deprecated)**

> **Deprecated:** Use the `type === "workflow_request"` discriminant instead. It provides the same typed event access plus the `request` object for additional context.

```typescript
import { isWorkflowDataRequest } from "@botpress/runtime";

export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ event, conversation }) {
    // ⚠️ Deprecated — prefer type === "workflow_request"
    if (isWorkflowDataRequest(event)) {
      await conversation.send({
        type: "text",
        payload: { text: event.payload.message }
      });

      const userInput = getUserInput();
      await MyWorkflow.provide(event, { data: userInput });
      return;
    }
  }
});
```

### Handling Workflow Callbacks (Completion Events)

When a workflow completes, fails, is canceled, or times out, the conversation receives a `workflow_callback` event. Use the `completion` object to inspect the result.

**Completion Object Structure:**
```typescript
type WorkflowCallback = {
  type: string;                              // Workflow name
  workflow: BaseWorkflowInstance;             // Workflow instance
  status: "completed" | "failed" | "canceled" | "timed_out";
  output?: z.infer<WorkflowOutput>;          // Workflow output (if completed)
  error?: string;                            // Error message (if failed)
}
```

**Using the Type Discriminant (Recommended)**

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ type, completion, event, conversation }) {
    if (type === "workflow_callback") {
      // completion is typed as WorkflowCallback
      // event is typed as WorkflowCallbackEventType

      switch (completion.status) {
        case "completed":
          await conversation.send({
            type: "text",
            payload: {
              text: `Workflow "${completion.type}" completed successfully!`
            }
          });
          // Access workflow output
          if (completion.output) {
            console.log("Output:", completion.output);
          }
          break;

        case "failed":
          await conversation.send({
            type: "text",
            payload: {
              text: `Workflow "${completion.type}" failed: ${completion.error}`
            }
          });
          break;

        case "canceled":
          await conversation.send({
            type: "text",
            payload: { text: `Workflow "${completion.type}" was canceled.` }
          });
          break;

        case "timed_out":
          await conversation.send({
            type: "text",
            payload: { text: `Workflow "${completion.type}" timed out.` }
          });
          break;
      }
      return;
    }
  }
});
```

**Legacy: Using `isWorkflowCallback` Type Guard (Deprecated)**

> **Deprecated:** Use the `type === "workflow_callback"` discriminant instead. It provides a typed `completion` object with workflow instance, status, output, and error information.

```typescript
import { isWorkflowCallback } from "@botpress/runtime";

export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ event, conversation }) {
    // ⚠️ Deprecated — prefer type === "workflow_callback"
    if (isWorkflowCallback(event)) {
      console.log("Workflow completed:", event.payload);
    }
  }
});
```

### Combined Workflow Event Handling

A single conversation handler can handle all workflow event types:

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",
  state: z.object({
    activeWorkflowId: z.string().optional()
  }),

  async handler({ type, message, request, completion, event, conversation, execute }) {
    // Handle workflow data requests
    if (type === "workflow_request") {
      await conversation.send({
        type: "text",
        payload: { text: event.payload.message }
      });
      // Wait for user input, then provide it back
      return;
    }

    // Handle workflow completion/failure
    if (type === "workflow_callback") {
      if (completion.status === "completed") {
        await conversation.send({
          type: "text",
          payload: { text: `Done! Result: ${JSON.stringify(completion.output)}` }
        });
      } else {
        await conversation.send({
          type: "text",
          payload: { text: `Workflow ended with status: ${completion.status}` }
        });
      }
      return;
    }

    // Handle regular messages
    if (type === "message") {
      await execute({
        instructions: "You are a helpful assistant"
      });
    }
  }
});
```

See **[Workflows](./workflows.md#conversation-communication)** for complete workflow request patterns including state management and multiple request types.

## Command Handling

### Implementing Commands

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ message, conversation, execute }) {
    if (message?.type !== "text") return;

    const text = message.payload.text;
    const command = text.split(" ")[0].toLowerCase();
    const args = text.substring(command.length).trim();

    switch (command) {
      case "/help":
        await conversation.send({
          type: "text",
          payload: {
            text: `Available commands:
/help - Show this help
/search <query> - Search knowledge base
/refresh - Refresh knowledge base
/workflow <data> - Start workflow`
          }
        });
        break;

      case "/search":
        const results = await MyKnowledgeBase.search({ query: args, limit: 5 });
        await conversation.send({
          type: "text",
          payload: {
            text: results.length > 0
              ? `Found ${results.length} results:\n${results.join("\n")}`
              : "No results found"
          }
        });
        break;

      case "/refresh":
        await MyKnowledgeBase.refresh();
        await conversation.send({
          type: "text",
          payload: { text: "Knowledge base refreshed!" }
        });
        break;

      case "/workflow":
        const instance = await MyWorkflow.getOrCreate({
          key: user.id,
          input: { userId: user.id, data: args }
        });
        await conversation.send({
          type: "text",
          payload: { text: `Workflow ${instance.id} started` }
        });
        break;

      default:
        // Not a command, use AI
        await execute({
          instructions: "You are a helpful assistant",
          knowledge: [MyKnowledgeBase]
        });
    }
  }
});
```

## Advanced Patterns

### Multi-Channel Support

Create separate conversations for different channels:

```typescript
// src/conversations/slack-dm.ts
export const SlackDM = new Conversation({
  channel: "slack.dm",

  async handler({ message, execute }) {
    // Slack-specific handling
    if (message?.payload.thread_ts) {
      // Handle threaded messages
    }

    await execute({
      instructions: "You are a Slack assistant. Use Slack markdown formatting."
    });
  }
});

// src/conversations/slack-channel.ts
export const SlackChannel = new Conversation({
  channel: "slack.channel",

  async handler({ message, execute }) {
    // Only respond when mentioned
    if (!message?.payload.text?.includes("@bot")) {
      return; // Don't respond
    }

    await execute({
      instructions: "You are in a public Slack channel. Be concise."
    });
  }
});
```

### Conversation Context

Maintain context across messages:

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",
  state: z.object({
    context: z.array(z.object({
      role: z.string(),
      content: z.string()
    })).default([])
  }),

  async handler({ message, state, execute }) {
    // Add user message to context
    if (message?.type === "text") {
      state.context.push({
        role: "user",
        content: message.payload.text
      });
    }

    // Keep last 10 messages
    if (state.context.length > 10) {
      state.context = state.context.slice(-10);
    }

    // Use context in AI execution
    await execute({
      instructions: `You are a helpful assistant.
Previous context:
${state.context.map(m => `${m.role}: ${m.content}`).join("\n")}`
    });
  }
});
```

### Context API & Chat Instance

Access the chat instance to read conversation history, register custom components, and build adaptive experiences.

#### Accessing the Chat Instance

```typescript
import { context, Conversation } from "@botpress/runtime";

export const Chat = new Conversation({
  channel: "webchat.channel",

  async handler({ message, execute, conversation }) {
    // Get the chat instance
    const chat = context.get("chat");

    // Fetch conversation transcript
    const transcript = await chat.fetchTranscript();

    // Analyze conversation history
    const messageCount = transcript.length;
    const hasImages = transcript.some(
      (msg) => msg.role === "user" && msg.attachments && msg.attachments.length > 0
    );

    console.log(`Conversation has ${messageCount} messages, includes images: ${hasImages}`);
  }
});
```

#### Adaptive Model Selection

Choose models dynamically based on conversation content (e.g., handle images with vision models):

```typescript
import { context, Conversation } from "@botpress/runtime";

export const Chat = new Conversation({
  channel: "webchat.channel",

  async handler({ message, execute, conversation }) {
    const chat = context.get("chat");
    const transcript = await chat.fetchTranscript();

    // Check if any user messages contain images
    const hasImages = transcript.some(
      (msg) => msg.role === "user" && msg.attachments?.length > 0
    );

    // Select appropriate model
    const model = hasImages
      ? "openai:gpt-4.1"        // Vision-capable model for images
      : "cerebras:gpt-oss-120b"; // Fast, cost-effective for text-only

    await execute({
      instructions: "You are a helpful assistant...",
      model,
      tools: [myTool]
    });
  }
});
```

**Use Cases:**
- **Cost Optimization**: Use cheaper/faster models for text-only conversations
- **Capability Matching**: Switch to vision models only when images are present
- **Context-Aware Responses**: Adapt behavior based on conversation history
- **Quality Control**: Use more powerful models for complex or lengthy conversations

#### Custom UI Components

Custom components are React `.bp.tsx` files that render rich UI in webchat. Register them in `src/components/index.ts` with `CustomComponent`, then list them in a conversation's `components` array:

```typescript
import { Conversation } from "@botpress/runtime";
import { TicketCardComponent } from "../components";

export const Chat = new Conversation({
  channel: "webchat.channel",

  components: [TicketCardComponent],

  async handler({ execute, conversation }) {
    // The LLM can yield TicketCard autonomously
    await execute({
      instructions: "Always use the TicketCard component to display ticket details.",
      tools: [lookupTicket]
    });
  }
});
```

You can also send a component explicitly:

```typescript
import { WelcomeBannerComponent } from "../components";

await conversation.send({
  type: "customComponent",
  payload: {
    component: WelcomeBannerComponent,
    props: {},
  }
});
```

See the component registry reference for the full component creation flow (`.bp.tsx` files, metadata, and registration).

**Component Use Cases:**
- **Rich Answers**: Formatted responses with citations and styling
- **Interactive Elements**: Buttons, forms, and action cards
- **Data Visualization**: Charts, tables, and structured data display
- **Custom Workflows**: Multi-step interactions with state

#### Chat Instance Methods

The chat instance provides methods for managing conversation transcripts and components.

**Transcript Management:**

```typescript
import { context } from "@botpress/runtime";

const chat = context.get("chat");

// Fetch transcript (loads from API)
const transcript = await chat.fetchTranscript();

// Get transcript (returns cached copy)
const cached = await chat.getTranscript();

// Set/replace entire transcript
await chat.setTranscript([
  { id: "1", role: "user", content: "Hello" },
  { id: "2", role: "assistant", content: "Hi there!" }
]);

// Clear all messages
await chat.clearTranscript();

// Prepend messages to beginning
await chat.prependToTranscript([
  { id: "0", role: "summary", content: "Previous conversation summary..." }
]);

// Remove specific message by ID
const removed = await chat.removeMessage("msg-123");

// Remove messages by predicate
const count = await chat.removeMessages(
  (item) => item.role === "assistant" && item.content.includes("error")
);

// Compact transcript (summarize/compress)
await chat.compactTranscript();

// Save transcript changes
await chat.saveTranscript();
```

**Component Management:**

```typescript
const chat = context.get("chat");

// Register component with handler
chat.registerComponent({
  component: myComponent,
  handler: async (props) => {
    // Handle component rendering
  }
});

// Remove component by name
chat.removeComponent("Answer");

// Get all registered components
const components = await chat.getComponents();
```

**Transcript Item Types:**

```typescript
type TranscriptUserMessage = {
  id: string;
  role: "user";
  content: string;
  name?: string;
  createdAt?: string;
  attachments?: Array<{ type: "image"; url: string }>;
};

type TranscriptAssistantMessage = {
  id: string;
  role: "assistant";
  content: string;
  name?: string;
  createdAt?: string;
};

type TranscriptEventMessage = {
  id: string;
  role: "event";
  name: string;
  payload: unknown;
  createdAt?: string;
};

type TranscriptSummaryMessage = {
  id: string;
  role: "summary";
  content: string;
};
```

#### Transcript Analysis Patterns

```typescript
import { context, Conversation } from "@botpress/runtime";

export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ message, execute }) {
    const chat = context.get("chat");
    const transcript = await chat.fetchTranscript();

    // Detect conversation patterns
    const userMessages = transcript.filter(msg => msg.role === "user");
    const hasRepeatedQuestion = userMessages.some((msg, idx) =>
      idx > 0 && msg.content === userMessages[idx - 1].content
    );

    if (hasRepeatedQuestion) {
      // User asked the same question twice - may need clarification
      await execute({
        instructions: `The user seems frustrated or confused.
        Provide extra clarification and ask if they need more help.`,
        tools: [searchTool]
      });
    } else {
      // Normal conversation flow
      await execute({
        instructions: "You are a helpful assistant",
        tools: [searchTool]
      });
    }
  }
});
```

**Analysis Patterns:**
- **Detect frustration**: Repeated questions, negative sentiment
- **Identify complexity**: Long conversations may need escalation
- **Track engagement**: Message frequency and length analysis
- **Content-based routing**: Images, files, code snippets

### Error Handling

```typescript
export const Chat = new Conversation({
  channel: "chat.channel",

  async handler({ message, conversation, execute }) {
    try {
      await execute({
        instructions: "You are a helpful assistant",
        tools: [riskyTool]
      });
    } catch (error) {
      console.error("Execution failed:", error);

      // Send error message to user
      await conversation.send({
        type: "text",
        payload: {
          text: "I apologize, but I encountered an error. Please try again later."
        }
      });

      // Optionally notify admins
      await notifyAdmins({
        error: error.message,
        conversationId: conversation.id
      });
    }
  }
});
```

## Best Practices

### 1. Handle All Message Types
```typescript
async handler({ message, conversation }) {
  switch (message?.type) {
    case "text":
      // Handle text
      break;
    case "image":
      // Handle image
      break;
    case "choice":
      // Handle choice selection
      break;
    default:
      // Handle unknown types
  }
}
```

### 2. Validate Input
```typescript
if (message?.type === "text") {
  const text = message.payload.text?.trim();
  if (!text || text.length > 1000) {
    await conversation.send({
      type: "text",
      payload: { text: "Please provide a valid message (1-1000 characters)" }
    });
    return;
  }
}
```

### 3. Use Early Returns
```typescript
async handler({ message, state, conversation, execute }) {
  // Handle commands first
  if (message?.payload.text?.startsWith("/")) {
    // Handle command inline
    return; // Early return
  }

  // Handle special cases
  if (state.waitingForInput) {
    // Handle input
    return; // Early return
  }

  // Default AI handling
  await execute({ instructions: "..." });
}
```

### 4. Provide Clear Instructions
```typescript
await execute({
  instructions: `You are a customer support assistant for ACME Corp.

Guidelines:
- Be helpful and professional
- Keep responses concise (under 100 words)
- Always verify customer identity before sharing sensitive info
- Escalate to human agent if customer seems frustrated

Current user: ${user.name}
Account type: ${user.accountType}`
});
```

### 5. Clean Up State
```typescript
// Reset state when conversation ends
async handler({ message, state, conversation }) {
  if (message?.payload.text === "/end") {
    // Reset state properties
    state.count = 0;
    state.userName = undefined;
    state.activeWorkflowId = undefined;

    await conversation.send({
      type: "text",
      payload: { text: "Conversation ended. State cleared." }
    });
  }
}
```