Skip to main content

Calendar Sync Plugin

A comprehensive example demonstrating calendar synchronization with external systems using the MuluTime Plugin SDK's Action System.

Features

  • 🗓️ Bidirectional calendar sync with Google Calendar
  • Real-time event handling for instant synchronization
  • 🔄 Scheduled sync tasks for data consistency
  • 🌐 REST API endpoints for external integrations
  • 🛡️ Error handling and retry logic
  • 📊 Sync statistics and monitoring

Plugin Overview

This plugin demonstrates all four action types in the MuluTime SDK:

  1. Event Actions - React to booking changes
  2. Scheduled Actions - Periodic sync operations
  3. API Actions - External webhook endpoints
  4. Lifecycle Actions - Setup and cleanup

Complete Implementation

import {
BasePlugin,
PluginManifest,
PluginCategory,
IntegrationType,
PluginPermission,
SystemEventType,
PluginScheduleType,
OnEvent,
Scheduled,
Get,
Post,
OnInstall,
OnUninstall,
SystemEventPayload,
APIRequest,
APIResponse,
PluginContext,
ValidationResult
} from '@mulutime/plugin-sdk';

export class CalendarSyncPlugin extends BasePlugin {
manifest: PluginManifest = {
id: 'com.mulutime.calendar-sync',
name: 'Calendar Sync Plugin',
version: '1.0.0',
description: 'Synchronizes bookings with external calendar systems',
author: {
name: 'MuluTime Team',
email: 'team@mulutime.com'
},
category: PluginCategory.CALENDAR,
type: [IntegrationType.CALENDAR, IntegrationType.AUTOMATION],
permissions: [
PluginPermission.READ_USER_DATA,
PluginPermission.WRITE_USER_DATA,
PluginPermission.EXTERNAL_API_ACCESS,
PluginPermission.SEND_NOTIFICATIONS
],
apiVersion: '1.0.0',
minSystemVersion: '1.0.0',
main: 'index.js'
};

// =================================================================
// LIFECYCLE ACTIONS - Plugin installation and configuration
// =================================================================

@OnInstall()
async handleInstall(context: PluginContext): Promise<void> {
const sdk = this.getSDK();

// Initialize sync database
await sdk.storage.set('sync-mappings', {});
await sdk.storage.set('sync-stats', {
totalSyncs: 0,
lastSyncTime: null,
errors: 0,
success: 0
});

// Validate Google Calendar connection
await this.validateGoogleCalendarConnection();

sdk.logger.info('Calendar sync plugin installed successfully');
}

@OnUninstall()
async handleUninstall(context: PluginContext): Promise<void> {
const sdk = this.getSDK();

// Clean up external calendar events
const mappings = await sdk.storage.get('sync-mappings') || {};
for (const [bookingId, externalId] of Object.entries(mappings)) {
await this.deleteExternalCalendarEvent(externalId as string);
}

// Clear all stored data
const keys = await sdk.storage.list();
for (const key of keys) {
await sdk.storage.delete(key);
}

sdk.logger.info('Calendar sync plugin uninstalled and cleaned up');
}

// =================================================================
// EVENT ACTIONS - Real-time booking synchronization
// =================================================================

@OnEvent(SystemEventType.BOOKING_CREATED, {
priority: 1,
retry: {
attempts: 3,
delay: 1000,
backoff: 'exponential'
}
})
async onBookingCreated(event: SystemEventPayload, context: PluginContext): Promise<void> {
const sdk = this.getSDK();

sdk.logger.info('Processing new booking for calendar sync', {
bookingId: event.data.id,
userId: event.userId
});

try {
// Create external calendar event
const externalEventId = await this.createExternalCalendarEvent({
title: event.data.service.name,
description: event.data.notes || '',
startTime: event.data.startTime,
endTime: event.data.endTime,
attendees: [
{ email: event.data.customer.email, name: event.data.customer.name },
{ email: event.data.provider.email, name: event.data.provider.name }
]
});

// Store sync mapping
const mappings = await sdk.storage.get('sync-mappings') || {};
mappings[event.data.id] = externalEventId;
await sdk.storage.set('sync-mappings', mappings);

// Update stats
await this.updateSyncStats('success');

sdk.logger.info('Booking synced to external calendar', {
bookingId: event.data.id,
externalEventId
});

} catch (error) {
await this.updateSyncStats('error');
sdk.logger.error('Failed to sync booking to calendar', error, {
bookingId: event.data.id
});
throw error; // This will trigger retry logic
}
}

@OnEvent(SystemEventType.BOOKING_UPDATED, {
priority: 1,
timeout: 30000 // 30 second timeout
})
async onBookingUpdated(event: SystemEventPayload, context: PluginContext): Promise<void> {
const sdk = this.getSDK();

const mappings = await sdk.storage.get('sync-mappings') || {};
const externalEventId = mappings[event.data.id];

if (!externalEventId) {
sdk.logger.warn('No external calendar event found for booking', {
bookingId: event.data.id
});
return;
}

try {
// Update external calendar event
await this.updateExternalCalendarEvent(externalEventId, {
title: event.data.service.name,
description: event.data.notes || '',
startTime: event.data.startTime,
endTime: event.data.endTime
});

await this.updateSyncStats('success');

sdk.logger.info('Booking update synced to external calendar', {
bookingId: event.data.id,
externalEventId
});

} catch (error) {
await this.updateSyncStats('error');
sdk.logger.error('Failed to sync booking update to calendar', error);
throw error;
}
}

@OnEvent(SystemEventType.BOOKING_CANCELLED, {
priority: 1
})
async onBookingCancelled(event: SystemEventPayload, context: PluginContext): Promise<void> {
const sdk = this.getSDK();

const mappings = await sdk.storage.get('sync-mappings') || {};
const externalEventId = mappings[event.data.id];

if (!externalEventId) {
return;
}

try {
// Delete external calendar event
await this.deleteExternalCalendarEvent(externalEventId);

// Remove mapping
delete mappings[event.data.id];
await sdk.storage.set('sync-mappings', mappings);

await this.updateSyncStats('success');

sdk.logger.info('Cancelled booking removed from external calendar', {
bookingId: event.data.id,
externalEventId
});

} catch (error) {
await this.updateSyncStats('error');
sdk.logger.error('Failed to remove cancelled booking from calendar', error);
}
}

// =================================================================
// SCHEDULED ACTIONS - Periodic maintenance and sync
// =================================================================

@Scheduled(PluginScheduleType.HOURLY)
async performHourlySync(context: PluginContext): Promise<void> {
const sdk = this.getSDK();

sdk.logger.info('Starting hourly calendar sync');

try {
// Sync external calendar events back to MuluTime
const externalEvents = await this.getExternalCalendarEvents();

for (const externalEvent of externalEvents) {
await this.syncExternalEventToMuluTime(externalEvent);
}

sdk.logger.info(`Hourly sync completed: ${externalEvents.length} events processed`);

} catch (error) {
sdk.logger.error('Hourly sync failed', error);
}
}

@Scheduled(PluginScheduleType.DAILY, { time: '02:00' })
async performDailyMaintenance(context: PluginContext): Promise<void> {
const sdk = this.getSDK();

sdk.logger.info('Starting daily maintenance');

try {
// Clean up orphaned sync mappings
await this.cleanupOrphanedMappings();

// Generate daily sync report
const stats = await sdk.storage.get('sync-stats');
sdk.logger.info('Daily sync statistics', stats);

// Reset daily counters
await sdk.storage.set('sync-stats', {
...stats,
dailySuccess: 0,
dailyErrors: 0,
lastMaintenanceRun: new Date().toISOString()
});

} catch (error) {
sdk.logger.error('Daily maintenance failed', error);
}
}

// =================================================================
// API ACTIONS - External webhook endpoints
// =================================================================

@Get('/sync-status')
async getSyncStatus(request: APIRequest, context: PluginContext): Promise<APIResponse> {
const sdk = this.getSDK();

const stats = await sdk.storage.get('sync-stats') || {};
const mappings = await sdk.storage.get('sync-mappings') || {};

return {
status: 200,
data: {
isConnected: await this.isGoogleCalendarConnected(),
syncStats: stats,
activeMappings: Object.keys(mappings).length,
lastSyncTime: stats.lastSyncTime
}
};
}

@Post('/manual-sync', {
validation: {
body: {
type: 'object',
properties: {
bookingId: { type: 'string' },
force: { type: 'boolean', default: false }
},
required: ['bookingId']
}
},
requiredPermissions: [PluginPermission.WRITE_USER_DATA]
})
async manualSync(request: APIRequest, context: PluginContext): Promise<APIResponse> {
const sdk = this.getSDK();
const { bookingId, force } = request.body;

try {
// Get booking data from MuluTime API
const booking = await sdk.api.get(`/bookings/${bookingId}`);

if (!booking) {
return {
status: 404,
error: 'Booking not found'
};
}

// Check if already synced
const mappings = await sdk.storage.get('sync-mappings') || {};
if (mappings[bookingId] && !force) {
return {
status: 400,
error: 'Booking already synced. Use force=true to re-sync.'
};
}

// Perform sync
const externalEventId = await this.createExternalCalendarEvent({
title: booking.service.name,
description: booking.notes || '',
startTime: booking.startTime,
endTime: booking.endTime,
attendees: [
{ email: booking.customer.email, name: booking.customer.name }
]
});

// Update mapping
mappings[bookingId] = externalEventId;
await sdk.storage.set('sync-mappings', mappings);

await this.updateSyncStats('success');

return {
status: 200,
data: {
bookingId,
externalEventId,
message: 'Booking synced successfully'
}
};

} catch (error) {
await this.updateSyncStats('error');

return {
status: 500,
error: 'Sync failed: ' + error.message
};
}
}

@Post('/webhook/google-calendar')
async handleGoogleCalendarWebhook(request: APIRequest, context: PluginContext): Promise<APIResponse> {
const sdk = this.getSDK();

sdk.logger.info('Received Google Calendar webhook', {
headers: request.headers,
body: request.body
});

try {
// Process Google Calendar webhook
const { eventId, eventType, eventData } = request.body;

switch (eventType) {
case 'event.created':
await this.handleExternalEventCreated(eventData);
break;
case 'event.updated':
await this.handleExternalEventUpdated(eventData);
break;
case 'event.deleted':
await this.handleExternalEventDeleted(eventId);
break;
}

return { status: 200, data: { processed: true } };

} catch (error) {
sdk.logger.error('Failed to process Google Calendar webhook', error);
return { status: 500, error: 'Webhook processing failed' };
}
}

// =================================================================
// HELPER METHODS
// =================================================================

private async validateGoogleCalendarConnection(): Promise<boolean> {
const sdk = this.getSDK();

try {
// Test Google Calendar API connection
const response = await sdk.http.get('https://www.googleapis.com/calendar/v3/calendars/primary', {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`
}
});

return response.status === 200;
} catch (error) {
sdk.logger.error('Google Calendar connection validation failed', error);
return false;
}
}

private async createExternalCalendarEvent(eventData: any): Promise<string> {
const sdk = this.getSDK();

const response = await sdk.http.post('https://www.googleapis.com/calendar/v3/calendars/primary/events', {
summary: eventData.title,
description: eventData.description,
start: {
dateTime: eventData.startTime,
timeZone: 'UTC'
},
end: {
dateTime: eventData.endTime,
timeZone: 'UTC'
},
attendees: eventData.attendees
}, {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`,
'Content-Type': 'application/json'
}
});

return response.data.id;
}

private async updateExternalCalendarEvent(eventId: string, eventData: any): Promise<void> {
const sdk = this.getSDK();

await sdk.http.put(`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`, {
summary: eventData.title,
description: eventData.description,
start: {
dateTime: eventData.startTime,
timeZone: 'UTC'
},
end: {
dateTime: eventData.endTime,
timeZone: 'UTC'
}
}, {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`,
'Content-Type': 'application/json'
}
});
}

private async deleteExternalCalendarEvent(eventId: string): Promise<void> {
const sdk = this.getSDK();

await sdk.http.delete(`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`, {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`
}
});
}

private async updateSyncStats(type: 'success' | 'error'): Promise<void> {
const sdk = this.getSDK();

const stats = await sdk.storage.get('sync-stats') || {
totalSyncs: 0,
errors: 0,
success: 0
};

stats.totalSyncs++;
stats.lastSyncTime = new Date().toISOString();

if (type === 'success') {
stats.success++;
} else {
stats.errors++;
}

await sdk.storage.set('sync-stats', stats);
}

// Additional helper methods...
private async isGoogleCalendarConnected(): Promise<boolean> {
return await this.validateGoogleCalendarConnection();
}

private async getExternalCalendarEvents(): Promise<any[]> {
// Implementation for fetching external events
return [];
}

private async syncExternalEventToMuluTime(externalEvent: any): Promise<void> {
// Implementation for syncing external events back
}

private async cleanupOrphanedMappings(): Promise<void> {
// Implementation for cleaning up orphaned sync mappings
}

private async handleExternalEventCreated(eventData: any): Promise<void> {
// Implementation for handling external event creation
}

private async handleExternalEventUpdated(eventData: any): Promise<void> {
// Implementation for handling external event updates
}

private async handleExternalEventDeleted(eventId: string): Promise<void> {
// Implementation for handling external event deletion
}
}

// Export the plugin instance
export default new CalendarSyncPlugin();

Configuration

The plugin requires Google Calendar API credentials:

{
"googleCalendarApiKey": "your-google-api-key-here",
"syncDirection": "bidirectional",
"autoSync": true,
"syncInterval": 60
}

Key Features Demonstrated

🎯 Action System Usage

  • Event Actions: Real-time booking synchronization
  • Scheduled Actions: Hourly sync and daily maintenance
  • API Actions: REST endpoints for manual sync and webhooks
  • Lifecycle Actions: Installation setup and cleanup

🛡️ Error Handling

  • Retry logic with exponential backoff
  • Timeout protection for long operations
  • Comprehensive error logging
  • Graceful degradation

📊 Monitoring

  • Sync statistics tracking
  • Connection health monitoring
  • Performance metrics
  • Detailed logging

🔧 External Integration

  • Google Calendar API integration
  • Webhook handling for bidirectional sync
  • Authentication management
  • Rate limiting compliance

Installation

  1. Install the plugin through the MuluTime admin panel
  2. Configure Google Calendar API credentials
  3. Set sync preferences (direction, interval, etc.)
  4. Test the connection using the /sync-status endpoint

Usage Examples

Manual Sync via API

curl -X POST "https://your-mulutime-instance.com/plugins/calendar-sync/manual-sync" \
-H "Content-Type: application/json" \
-d '{"bookingId": "booking-123", "force": false}'

Check Sync Status

curl "https://your-mulutime-instance.com/plugins/calendar-sync/sync-status"

This example demonstrates the full power of the MuluTime Action System, showing how to build a production-ready plugin with comprehensive event handling, scheduling, API endpoints, and external integrations.