# 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 <integration> --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 <integration> --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 <integration> --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<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
      });
    }
  }
});
```
