# Triggers Triggers allow your bot to react to events from the bot itself or integrated services. They enable event-driven automation and workflow orchestration. > **⚠️ IMPORTANT:** Always verify integration events exist before using them! Use `adk info --events` to discover real events. Integration events change between versions. > > **⚠️ Dev-mode limitation:** `adk dev` is not a perfect mirror of production for every integration event flow. Some private or custom integration events may not register or route locally even when the event name is valid. If an event-driven flow works in docs but not in local dev, verify the event first, then test against a deployed bot before assuming your trigger code is wrong. ## Basic Concepts ### What are Triggers? - **Event handlers**: React to bot or integration events - **Automated**: Execute automatically when events occur - **Flexible**: Can start workflows, update tables, or perform any action ### File Location - **Location**: `src/triggers/*.ts` - **Auto-registration**: Files automatically subscribe to specified events ## Creating Triggers ### Basic Structure ```typescript import { Trigger } from "@botpress/runtime"; export default new Trigger({ name: "myTrigger", description: "Handles user creation events", events: ["user.created"], // Events to subscribe to handler: async ({ event }) => { console.log(event.type); // "user.created" console.log(event.payload); // Event data // React to the event await handleUserCreated(event.payload); } }); ``` ### Multiple Events Subscribe to multiple events in one trigger: ```typescript export default new Trigger({ name: "userEvents", description: "Handles all user-related events", events: [ "user.created", "user.updated", "user.deleted" ], handler: async ({ event }) => { switch (event.type) { case "user.created": await handleUserCreated(event.payload); break; case "user.updated": await handleUserUpdated(event.payload); break; case "user.deleted": await handleUserDeleted(event.payload); break; } } }); ``` ## Event Types ### Bot Events Standard events emitted by the bot: ```typescript // User events "user.created" // New user created "user.updated" // User information updated "user.deleted" // User deleted // Conversation events "conversation.started" // New conversation started "conversation.ended" // Conversation ended "message.created" // New message in conversation // Workflow events "workflow.started" // Workflow instance started "workflow.completed" // Workflow completed successfully "workflow.failed" // Workflow failed // Bot lifecycle events "bot.started" // Bot started "bot.stopped" // Bot stopped ``` ### Integration Events Events from integrated services use the pattern `integrationName:eventName`. **IMPORTANT:** Always verify events exist using `adk info --events` before using them. ```typescript // Intercom events (real events from vertical-one) "intercom:conversationEvent" // Fires on create/update/delete "intercom:contactEvent" // Contact created/updated "intercom:adminEvent" // Admin changes // Slack events (verified with adk info slack --events) "slack:reactionAdded" "slack:reactionRemoved" "slack:memberJoinedChannel" "slack:memberJoinedWorkspace" "slack:memberLeftChannel" "slack:workflowWebhook" // Linear events (verified with adk info linear --events) "linear:issueCreated" "linear:issueUpdated" "linear:issueDeleted" // GitHub events (verified with adk info github --events) "github:issueOpened" "github:pullRequestOpened" "github:pullRequestMerged" "github:pullRequestReviewSubmitted" // Webchat events (verified with adk info webchat --events) "webchat:conversationStarted" "webchat:trigger" ``` **How to discover events:** ```bash # Search for an integration adk search email # Get all events for an integration adk info slack --events adk info linear --events adk info github --events ``` Checking `adk info --events` confirms the event exists, but it does not guarantee that every event source behaves the same in `adk dev` as it does after deployment. ## Trigger Handlers ### Starting Workflows Common pattern: Start a workflow when an event occurs: ```typescript import { Trigger } from "@botpress/runtime"; import { OnboardingWorkflow } from "../workflows/onboarding"; export default new Trigger({ name: "userOnboarding", description: "Start onboarding when user is created", events: ["user.created"], handler: async ({ event }) => { const { userId, email, name } = event.payload; // Start onboarding workflow const instance = await OnboardingWorkflow.start({ userId, email, name, createdAt: new Date() }); console.log(`Started onboarding for ${userId}: ${instance.id}`); } }); ``` ### Updating Tables Store event data in tables: ```typescript import { Trigger } from "@botpress/runtime"; import { EventLogTable } from "../tables/EventLog"; export default new Trigger({ name: "eventLogger", description: "Log all events to database", events: [ "user.created", "user.updated", "conversation.started", "workflow.completed" ], handler: async ({ event }) => { // Store event in table await EventLogTable.createRows({ rows: [{ type: event.type, payload: event.payload, timestamp: new Date(), metadata: { source: event.source || "bot", version: event.version || "1.0" } }] }); } }); ``` ### Calling Actions Execute bot actions or integration actions in response to events: ```typescript import { Trigger } from "@botpress/runtime"; import { actions } from "@botpress/runtime"; export default new Trigger({ name: "notificationSender", description: "Send email notifications for important events", events: ["workflow.failed", "user.deleted"], handler: async ({ event }) => { // Call integration action (e.g., SendGrid) // First add the integration: adk add sendgrid await actions.sendgrid.sendEmail({ to: "admin@example.com", from: "bot@example.com", subject: `Alert: ${event.type}`, content: `Event occurred: ${JSON.stringify(event.payload)}` }); } }); ``` **Note:** The `actions` object provides access to: - **Bot actions**: `actions.myAction()` - your custom actions from `src/actions/` - **Integration actions**: `actions.integrationName.actionName()` - actions from installed integrations ## Integration Event Examples ### Intercom Integration Real-world example from vertical-one bot: ```typescript import { Trigger } from "@botpress/runtime"; import { syncSingleConversation, deleteConversation, } from "../utils/syncConversation"; export default new Trigger({ name: "onIntercomUpdate", description: "Updates the Intercom tables in realtime when conversations are created or messages are sent", events: ["intercom:conversationEvent"], handler: async ({ event }) => { const { eventType, conversationId } = event.payload; console.log( `Intercom event: ${eventType} for conversation ${conversationId}` ); try { if (eventType === "deleted") { // Delete the conversation and its messages await deleteConversation(conversationId); } else { // Sync the conversation with all its data await syncSingleConversation(conversationId); } } catch (error) { console.error( `Failed to handle ${eventType} event for conversation ${conversationId}:`, error ); throw error; } }, }); ``` ### Linear Integration Real example from ADK source code: ```typescript import { Trigger } from "@botpress/runtime"; import { LinearIssueTable } from "../tables/LinearIssues"; export default new Trigger({ name: "onLinearIssueUpdate", description: "Updates the Linear Issue Table when issues change", events: [ "linear:issueCreated", "linear:issueUpdated", "linear:issueDeleted" ], handler: async ({ event }) => { if (event.type === "linear:issueDeleted") { await LinearIssueTable.deleteRows({ linearId: event.payload.id }); } else if (event.type === "linear:issueCreated") { await LinearIssueTable.createRows({ rows: [{ linearId: event.payload.linearIds?.issueId, content: `# ${event.payload.title}\n\n${event.payload.description}`, issue: event.payload, linearUpdatedAt: event.payload.updatedAt }] }); } else if (event.type === "linear:issueUpdated") { await LinearIssueTable.upsertRows({ rows: [{ linearId: event.payload.linearIds?.issueId, content: `# ${event.payload.title}\n\n${event.payload.description}`, issue: event.payload, linearUpdatedAt: event.payload.updatedAt }], keyColumn: "linearId" }); } } }); ``` ### Slack Integration ```typescript import { Trigger } from "@botpress/runtime"; import { SlackActivityTable } from "../tables/SlackActivity"; export default new Trigger({ name: "slackActivityMonitor", description: "Monitor Slack reactions and member activity", events: [ "slack:reactionAdded", "slack:memberJoinedChannel" ], handler: async ({ event }) => { if (event.type === "slack:reactionAdded") { const { reaction, user, item } = event.payload; // Log important reactions to table if (reaction === "fire" || reaction === "star" || reaction === "eyes") { await SlackActivityTable.createRows({ rows: [{ type: "reaction", reaction: reaction, user: user, messageId: item.ts, channelId: item.channel, timestamp: new Date() }] }); } } if (event.type === "slack:memberJoinedChannel") { const { user, channel } = event.payload; // Log new member joins await SlackActivityTable.createRows({ rows: [{ type: "member_join", user: user, channelId: channel, timestamp: new Date() }] }); } } }); ``` ## Advanced Patterns ### Conditional Processing Process events based on conditions: ```typescript export default new Trigger({ name: "conditionalProcessor", events: ["user.created"], handler: async ({ event }) => { const { email, source, metadata } = event.payload; // Only process certain users if (!email.endsWith("@company.com")) { console.log("Skipping external user"); return; } // Different handling based on source if (source === "api") { await handleApiUser(event.payload); } else if (source === "signup") { await handleSignupUser(event.payload); } // Check metadata flags if (metadata?.requiresApproval) { await startApprovalWorkflow(event.payload); } } }); ``` ### Event Aggregation Collect and batch process events: ```typescript import { Trigger } from "@botpress/runtime"; import { EventBatchTable } from "../tables/EventBatch"; export default new Trigger({ name: "eventAggregator", events: ["metrics.recorded"], handler: async ({ event }) => { // Store event for batching await EventBatchTable.createRows({ rows: [{ type: event.type, data: event.payload, processed: false, timestamp: new Date() }] }); // Check if we should process batch const { rows } = await EventBatchTable.findRows({ where: { processed: false }, limit: 100 }); if (rows.length >= 100) { // Process batch await processBatch(rows); // Mark as processed const ids = rows.map(r => r.id); await EventBatchTable.updateRows({ where: { id: { $in: ids } }, updates: { processed: true } }); } } }); ``` ### Workflow Orchestration Chain workflows based on events: ```typescript export default new Trigger({ name: "workflowOrchestrator", events: ["workflow.completed"], handler: async ({ event }) => { const { workflowName, output } = event.payload; // Chain workflows based on completion switch (workflowName) { case "dataCollection": // Start processing workflow await ProcessingWorkflow.start({ data: output.collectedData }); break; case "processing": // Start validation workflow await ValidationWorkflow.start({ processedData: output.result }); break; case "validation": // Start final reporting workflow if (output.isValid) { await ReportingWorkflow.start({ data: output.validatedData }); } break; } } }); ``` ### Error Recovery Handle failed events: ```typescript export default new Trigger({ name: "errorRecovery", events: ["workflow.failed", "integration:error"], handler: async ({ event }) => { const { error, context, retryCount = 0 } = event.payload; // Log error await ErrorLogTable.createRows({ rows: [{ type: event.type, error: error.message, stack: error.stack, context, timestamp: new Date() }] }); // Attempt recovery if (retryCount < 3) { // Retry with backoff setTimeout(async () => { await RetryWorkflow.start({ originalEvent: event, retryCount: retryCount + 1 }); }, Math.pow(2, retryCount) * 1000); } else { // Send alert after max retries await actions.sendAlert({ severity: "critical", title: "Max retries exceeded", details: { event, error } }); } } }); ``` ## Best Practices ### 1. Use Descriptive Names ```typescript // ✅ Good export default new Trigger({ name: "linearIssueSync", description: "Syncs Linear issues to internal tracking" }); // ❌ Bad export default new Trigger({ name: "trigger1", description: "Does stuff" }); ``` ### 2. Handle Errors Gracefully ```typescript handler: async ({ event }) => { try { await processEvent(event); } catch (error) { console.error(`Failed to process ${event.type}:`, error); // Store error for debugging await ErrorTable.createRows({ rows: [{ trigger: "myTrigger", event: event.type, error: error.message, timestamp: new Date() }] }); // Don't re-throw unless you want to retry } } ``` ### 3. Avoid Heavy Processing ```typescript handler: async ({ event }) => { // ✅ Good: Start async workflow await HeavyProcessingWorkflow.start({ eventData: event.payload }); // ❌ Bad: Long-running synchronous processing // const result = await performHeavyComputation(event.payload); } ``` ### 4. Use Event Patterns ```typescript // Group related events from multiple integrations export default new Trigger({ name: "issueTracker", events: [ "linear:issueCreated", "linear:issueUpdated", "github:issueOpened", "github:pullRequestOpened" ], handler: async ({ event }) => { const [integration, action] = event.type.split(":"); // Common processing for all issue-related events await IssueTrackingTable.createRows({ rows: [{ source: integration, eventType: action, data: event.payload, timestamp: new Date() }] }); } }); ``` ### 5. Document Event Payloads ```typescript /** * Handles user creation events * * Expected payload: * { * userId: string * email: string * name: string * source: "signup" | "api" | "admin" * metadata?: Record * } */ export default new Trigger({ name: "userCreatedHandler", events: ["user.created"], handler: async ({ event }) => { const { userId, email, name, source } = event.payload; // Process with confidence in payload structure } }); ``` ## Common Patterns ### Audit Trail ```typescript export default new Trigger({ name: "auditTrail", events: [ "user.created", "user.updated", "user.deleted", "conversation.started" ], handler: async ({ event }) => { await AuditTable.createRows({ rows: [{ eventType: event.type, payload: event.payload, userId: event.payload.userId || event.userId, timestamp: new Date() }] }); } }); ``` ### Notification System ```typescript export default new Trigger({ name: "notifications", events: [ "workflow.failed", "user.deleted", "integration:error" ], handler: async ({ event }) => { const notifications = { "workflow.failed": { channel: "slack", priority: "high", template: "workflow-failure" }, "user.deleted": { channel: "email", priority: "normal", template: "user-deleted" }, "integration:error": { channel: "pagerduty", priority: "critical", template: "integration-error" } }; const config = notifications[event.type]; if (config) { await actions.sendNotification({ ...config, data: event.payload }); } } }); ```