Application Triggers
Application triggers initiate workflows based on specific events or conditions. These triggers, such as database updates, user actions, or scheduled timers, enable real-time processing and automation. They are essential for reactive systems and event-driven architectures.
Example: Multi-Modal Trigger System for E-commerce
Consider an e-commerce platform that uses various types of triggers to automate business processes:
Trigger Architecture Overview
Trigger Types and Implementation
// Base trigger interface
interface Trigger {
id: string;
name: string;
description: string;
enabled: boolean;
conditions: TriggerCondition[];
actions: TriggerAction[];
metadata: Record<string, any>;
}
// Different trigger types
interface EventTrigger extends Trigger {
type: 'event';
eventType: string;
eventSource: string;
}
interface ScheduleTrigger extends Trigger {
type: 'schedule';
cronExpression: string;
timezone: string;
}
interface WebhookTrigger extends Trigger {
type: 'webhook';
endpoint: string;
secret: string;
method: 'POST' | 'PUT' | 'PATCH';
}
interface DatabaseTrigger extends Trigger {
type: 'database';
table: string;
operation: 'INSERT' | 'UPDATE' | 'DELETE';
columns?: string[];
}
// Trigger conditions
interface TriggerCondition {
id: string;
field: string;
operator: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'in' | 'custom';
value: any;
customFunction?: string;
}
// Trigger actions
interface TriggerAction {
id: string;
type: 'email' | 'webhook' | 'database' | 'service_call' | 'custom';
config: Record<string, any>;
retryConfig?: {
maxRetries: number;
backoffMs: number;
};
}
Event-Based Triggers
// Order status change trigger
const orderStatusTrigger: EventTrigger = {
id: 'order-status-changed',
name: 'Order Status Changed',
description: 'Triggered when an order status changes',
type: 'event',
eventType: 'order.status.changed',
eventSource: 'order-service',
enabled: true,
conditions: [
{
id: 'status-shipped',
field: 'newStatus',
operator: 'equals',
value: 'shipped'
}
],
actions: [
{
id: 'send-shipping-notification',
type: 'email',
config: {
template: 'shipping-notification',
to: '{{order.customerEmail}}',
subject: 'Your order #{{order.id}} has shipped!'
}
},
{
id: 'update-analytics',
type: 'service_call',
config: {
service: 'analytics-service',
method: 'trackOrderShipped',
payload: {
orderId: '{{order.id}}',
customerId: '{{order.customerId}}',
shippedAt: '{{timestamp}}'
}
}
}
],
metadata: {
category: 'order-management',
priority: 'high'
}
};
// Low inventory trigger
const lowInventoryTrigger: EventTrigger = {
id: 'low-inventory-alert',
name: 'Low Inventory Alert',
description: 'Triggered when product inventory falls below threshold',
type: 'event',
eventType: 'inventory.updated',
eventSource: 'inventory-service',
enabled: true,
conditions: [
{
id: 'below-threshold',
field: 'quantity',
operator: 'less_than',
value: 10
},
{
id: 'active-product',
field: 'status',
operator: 'equals',
value: 'active'
}
],
actions: [
{
id: 'notify-purchasing',
type: 'email',
config: {
template: 'low-inventory-alert',
to: 'purchasing@company.com',
subject: 'Low Inventory Alert: {{product.name}}'
}
},
{
id: 'create-purchase-order',
type: 'service_call',
config: {
service: 'purchasing-service',
method: 'createAutoPurchaseOrder',
payload: {
productId: '{{product.id}}',
suggestedQuantity: '{{product.reorderQuantity}}',
priority: 'medium'
}
},
retryConfig: {
maxRetries: 3,
backoffMs: 5000
}
}
],
metadata: {
category: 'inventory-management',
priority: 'medium'
}
};
Schedule-Based Triggers
// Daily sales report trigger
const dailySalesReportTrigger: ScheduleTrigger = {
id: 'daily-sales-report',
name: 'Daily Sales Report',
description: 'Generate and send daily sales report',
type: 'schedule',
cronExpression: '0 8 * * *', // 8 AM every day
timezone: 'America/New_York',
enabled: true,
conditions: [], // No conditions for scheduled triggers
actions: [
{
id: 'generate-report',
type: 'service_call',
config: {
service: 'reporting-service',
method: 'generateDailySalesReport',
payload: {
date: '{{yesterday}}',
format: 'pdf'
}
}
},
{
id: 'email-report',
type: 'email',
config: {
template: 'daily-sales-report',
to: 'management@company.com',
subject: 'Daily Sales Report - {{date}}',
attachments: ['{{report.filePath}}']
}
}
],
metadata: {
category: 'reporting',
priority: 'low'
}
};
// Abandoned cart reminder trigger
const abandonedCartTrigger: ScheduleTrigger = {
id: 'abandoned-cart-reminder',
name: 'Abandoned Cart Reminder',
description: 'Send reminder emails for abandoned carts',
type: 'schedule',
cronExpression: '0 */2 * * *', // Every 2 hours
timezone: 'UTC',
enabled: true,
conditions: [],
actions: [
{
id: 'find-abandoned-carts',
type: 'service_call',
config: {
service: 'cart-service',
method: 'findAbandonedCarts',
payload: {
abandonedAfterHours: 2,
maxRemindersSent: 3
}
}
},
{
id: 'send-reminder-emails',
type: 'custom',
config: {
handler: 'abandoned-cart-email-handler',
batchSize: 100
}
}
],
metadata: {
category: 'marketing',
priority: 'medium'
}
};
Trigger Manager Implementation
class TriggerManager {
private triggers = new Map<string, Trigger>();
private activeListeners = new Map<string, Function>();
constructor(
private eventBus: EventBus,
private conditionEngine: ConditionEngine,
private actionDispatcher: ActionDispatcher,
private scheduler: SchedulerService,
private logger: Logger
) {}
async registerTrigger(trigger: Trigger): Promise<void> {
this.triggers.set(trigger.id, trigger);
if (!trigger.enabled) {
return;
}
switch (trigger.type) {
case 'event':
await this.registerEventTrigger(trigger as EventTrigger);
break;
case 'schedule':
await this.registerScheduleTrigger(trigger as ScheduleTrigger);
break;
case 'webhook':
await this.registerWebhookTrigger(trigger as WebhookTrigger);
break;
case 'database':
await this.registerDatabaseTrigger(trigger as DatabaseTrigger);
break;
}
this.logger.info(`Registered trigger: ${trigger.name} (${trigger.id})`);
}
private async registerEventTrigger(trigger: EventTrigger): Promise<void> {
const listener = async (event: any) => {
await this.processTrigger(trigger, event);
};
this.eventBus.subscribe(trigger.eventType, listener);
this.activeListeners.set(trigger.id, listener);
}
private async registerScheduleTrigger(trigger: ScheduleTrigger): Promise<void> {
const job = this.scheduler.schedule(
trigger.cronExpression,
async () => {
const context = {
triggerId: trigger.id,
triggerType: 'schedule',
timestamp: new Date(),
timezone: trigger.timezone
};
await this.processTrigger(trigger, context);
},
{ timezone: trigger.timezone }
);
this.activeListeners.set(trigger.id, () => job.cancel());
}
private async processTrigger(trigger: Trigger, eventData: any): Promise<void> {
try {
// Evaluate conditions
const conditionsMet = await this.conditionEngine.evaluate(
trigger.conditions,
eventData
);
if (!conditionsMet) {
this.logger.debug(`Trigger ${trigger.id} conditions not met`);
return;
}
this.logger.info(`Executing trigger: ${trigger.name}`);
// Execute actions
for (const action of trigger.actions) {
await this.actionDispatcher.dispatch(action, eventData, trigger);
}
// Record metrics
await this.recordTriggerExecution(trigger, eventData, 'success');
} catch (error) {
this.logger.error(`Error processing trigger ${trigger.id}:`, error);
await this.recordTriggerExecution(trigger, eventData, 'error', error.message);
}
}
async unregisterTrigger(triggerId: string): Promise<void> {
const cleanup = this.activeListeners.get(triggerId);
if (cleanup) {
cleanup();
this.activeListeners.delete(triggerId);
}
this.triggers.delete(triggerId);
this.logger.info(`Unregistered trigger: ${triggerId}`);
}
async enableTrigger(triggerId: string): Promise<void> {
const trigger = this.triggers.get(triggerId);
if (!trigger) {
throw new Error(`Trigger not found: ${triggerId}`);
}
trigger.enabled = true;
await this.registerTrigger(trigger);
}
async disableTrigger(triggerId: string): Promise<void> {
const trigger = this.triggers.get(triggerId);
if (!trigger) {
throw new Error(`Trigger not found: ${triggerId}`);
}
trigger.enabled = false;
await this.unregisterTrigger(triggerId);
}
private async recordTriggerExecution(
trigger: Trigger,
eventData: any,
status: 'success' | 'error',
errorMessage?: string
): Promise<void> {
// Record execution metrics and logs
const execution = {
triggerId: trigger.id,
triggerName: trigger.name,
status,
timestamp: new Date(),
eventData: JSON.stringify(eventData),
errorMessage,
executionTimeMs: 0 // Would be calculated from start time
};
// Store in database or metrics system
await this.storeTriggerExecution(execution);
}
}
Condition Engine
class ConditionEngine {
private customFunctions = new Map<string, Function>();
async evaluate(conditions: TriggerCondition[], data: any): Promise<boolean> {
if (conditions.length === 0) {
return true; // No conditions means always trigger
}
// All conditions must be true (AND logic)
for (const condition of conditions) {
const result = await this.evaluateCondition(condition, data);
if (!result) {
return false;
}
}
return true;
}
private async evaluateCondition(condition: TriggerCondition, data: any): Promise<boolean> {
const fieldValue = this.getFieldValue(data, condition.field);
switch (condition.operator) {
case 'equals':
return fieldValue === condition.value;
case 'not_equals':
return fieldValue !== condition.value;
case 'greater_than':
return Number(fieldValue) > Number(condition.value);
case 'less_than':
return Number(fieldValue) < Number(condition.value);
case 'contains':
return String(fieldValue).includes(String(condition.value));
case 'in':
return Array.isArray(condition.value) && condition.value.includes(fieldValue);
case 'custom':
const customFn = this.customFunctions.get(condition.customFunction!);
if (!customFn) {
throw new Error(`Custom function not found: ${condition.customFunction}`);
}
return await customFn(fieldValue, condition.value, data);
default:
throw new Error(`Unknown operator: ${condition.operator}`);
}
}
private getFieldValue(data: any, fieldPath: string): any {
return fieldPath.split('.').reduce((obj, key) => obj?.[key], data);
}
registerCustomFunction(name: string, fn: Function): void {
this.customFunctions.set(name, fn);
}
}
Action Dispatcher
class ActionDispatcher {
private actionHandlers = new Map<string, ActionHandler>();
constructor(
private templateEngine: TemplateEngine,
private retryService: RetryService,
private logger: Logger
) {
this.registerDefaultHandlers();
}
async dispatch(action: TriggerAction, eventData: any, trigger: Trigger): Promise<void> {
const handler = this.actionHandlers.get(action.type);
if (!handler) {
throw new Error(`No handler found for action type: ${action.type}`);
}
// Process templates in action config
const processedConfig = await this.templateEngine.process(action.config, {
...eventData,
trigger: trigger.metadata
});
const executeAction = async () => {
return handler.execute(processedConfig, eventData);
};
if (action.retryConfig) {
await this.retryService.executeWithRetry(
executeAction,
action.retryConfig.maxRetries,
action.retryConfig.backoffMs
);
} else {
await executeAction();
}
}
registerActionHandler(type: string, handler: ActionHandler): void {
this.actionHandlers.set(type, handler);
}
private registerDefaultHandlers(): void {
this.registerActionHandler('email', new EmailActionHandler());
this.registerActionHandler('webhook', new WebhookActionHandler());
this.registerActionHandler('service_call', new ServiceCallActionHandler());
this.registerActionHandler('database', new DatabaseActionHandler());
}
}
// Action handler implementations
interface ActionHandler {
execute(config: any, eventData: any): Promise<void>;
}
class EmailActionHandler implements ActionHandler {
constructor(private emailService: EmailService) {}
async execute(config: any, eventData: any): Promise<void> {
await this.emailService.send({
to: config.to,
subject: config.subject,
template: config.template,
data: eventData,
attachments: config.attachments
});
}
}
class ServiceCallActionHandler implements ActionHandler {
constructor(private serviceRegistry: ServiceRegistry) {}
async execute(config: any, eventData: any): Promise<void> {
const service = this.serviceRegistry.get(config.service);
await service[config.method](config.payload);
}
}
Benefits of Application Triggers
Automation: Reduce manual processes and human error Real-time Response: React immediately to events and conditions Scalability: Handle increasing event volumes without manual intervention Flexibility: Easily modify conditions and actions without code changes Audit Trail: Track all trigger executions for compliance and debugging Decoupling: Separate trigger logic from business logic
When to Use Application Triggers
Good fit when:
- You need to automate repetitive business processes
- Real-time responses to events are required
- Complex conditional logic drives actions
- You want to decouple reactive behavior from core business logic
- Audit trails and compliance are important
- Non-technical users need to configure workflows
Avoid when:
- Simple, linear workflows are sufficient
- Real-time performance is critical (triggers add latency)
- Trigger logic is extremely complex and changes frequently
- The system has very few events or automation needs
- Debugging complex trigger chains would be difficult
Implementation Guidelines
- Keep Actions Idempotent: Ensure actions can be safely retried
- Implement Circuit Breakers: Prevent cascade failures from trigger storms
- Add Comprehensive Logging: Track trigger evaluations and action executions
- Design for Testability: Make triggers and conditions easily testable
- Monitor Performance: Track trigger execution times and failure rates
- Implement Backpressure: Handle high-volume events gracefully
- Version Trigger Configurations: Track changes to trigger definitions over time