UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

719 lines (587 loc) 18.4 kB
# 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 integrations info <integration> --format json` and inspect the returned event spec. 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. > > **⚠️ To _receive_ a pushed event, subscribe to `chat:custom` not a custom name.** An event pushed into a chat conversation (e.g. an order shipped, a status update relayed in) arrives as `chat:custom`. Subscribe the `Conversation` to `events: ['chat:custom']` and branch on `type === 'event'`; a handler on a made-up name like `orderShipped` never fires. See [Conversations Custom & Proactive Events](./conversations.md#custom--proactive-events). The camelCase rule below applies only to events you _define/emit_, not to receiving. > > **⚠️ Events you define must be camelCase** `orderShipped`, not `order_shipped`; `adk build` rejects snake_case names. ## 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 integrations info <integration> --format json` 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 integrations info slack --format json) 'slack:reactionAdded' 'slack:reactionRemoved' 'slack:memberJoinedChannel' 'slack:memberJoinedWorkspace' 'slack:memberLeftChannel' 'slack:workflowWebhook' // Linear events (verified with adk integrations info linear --format json) 'linear:issueCreated' 'linear:issueUpdated' 'linear:issueDeleted' // GitHub events (verified with adk integrations info github --format json) 'github:issueOpened' 'github:pullRequestOpened' 'github:pullRequestMerged' 'github:pullRequestReviewSubmitted' // Webchat events (verified with adk integrations info webchat --format json) 'webchat:conversationStarted' 'webchat:trigger' ``` **How to discover events:** ```bash # Search for an integration adk integrations search email # Get all events for an integration adk integrations info slack --format json adk integrations info linear --format json adk integrations info github --format json ``` Checking `adk integrations info <integration> --format json` 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 integrations add sendgrid@<version> 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<string, any> * } */ 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, }) } }, }) ```