Cara Balas WA Otomatis dengan Scheduling

Tutorial schedule pesan WhatsApp otomatis. Time-based triggers, recurring messages, campaign scheduling. Panduan lengkap!

Cara Balas WA Otomatis dengan Scheduling
Cara Balas WA Otomatis dengan Scheduling

Scheduling = Pesan tepat waktu, otomatis!

Dengan scheduling, kamu bisa jadwalkan pesan untuk dikirim di waktu tertentu - reminder, promo, atau follow up tanpa harus online.


Jenis Scheduling

📅 TIPE SCHEDULING:

1. ONE-TIME SCHEDULE
   Kirim sekali di waktu tertentu
   Contoh: Promo launch jam 10:00 besok

2. RECURRING SCHEDULE
   Kirim berulang di pattern tertentu
   Contoh: Reminder setiap Senin jam 9

3. TRIGGER-BASED DELAY
   Kirim setelah X waktu dari trigger
   Contoh: Follow up 1 jam setelah order

4. TIME-WINDOW BASED
   Auto reply beda sesuai jam
   Contoh: Greeting pagi vs malam

One-Time Schedule

Database Schema:

javascript

const scheduledMessageSchema = {
    id: String,
    recipient: String,           // Phone number
    message: String,             // Message content
    scheduledAt: Date,           // When to send
    status: String,              // 'pending', 'sent', 'failed'
    createdAt: Date,
    sentAt: Date,
    metadata: Object             // Additional data
};

Schedule Message:

javascript

async function scheduleMessage(recipient, message, sendAt) {
    const scheduled = {
        id: generateId(),
        recipient,
        message,
        scheduledAt: new Date(sendAt),
        status: 'pending',
        createdAt: new Date()
    };
    
    await db.scheduledMessages.insertOne(scheduled);
    
    return scheduled;
}

// Usage
await scheduleMessage(
    '628123456789',
    'Hai! Jangan lupa promo hari ini ya! 🎉',
    '2026-02-18T10:00:00+07:00'
);

Scheduler Worker:

javascript

const cron = require('node-cron');

// Check setiap menit untuk pesan yang perlu dikirim
cron.schedule('* * * * *', async () => {
    const now = new Date();
    
    // Get pesan yang sudah waktunya
    const pendingMessages = await db.scheduledMessages.find({
        status: 'pending',
        scheduledAt: { $lte: now }
    }).toArray();
    
    for (const msg of pendingMessages) {
        try {
            await sendWhatsApp(msg.recipient, msg.message);
            
            await db.scheduledMessages.updateOne(
                { id: msg.id },
                { 
                    $set: { 
                        status: 'sent',
                        sentAt: new Date()
                    }
                }
            );
            
            console.log(`Sent scheduled message ${msg.id}`);
        } catch (error) {
            await db.scheduledMessages.updateOne(
                { id: msg.id },
                { 
                    $set: { 
                        status: 'failed',
                        error: error.message
                    }
                }
            );
            
            console.error(`Failed to send ${msg.id}:`, error);
        }
    }
});

Recurring Schedule

Recurring Schema:

javascript

const recurringScheduleSchema = {
    id: String,
    name: String,
    recipients: [String],        // Array of phone numbers or segment
    message: String,
    schedule: {
        type: String,            // 'daily', 'weekly', 'monthly', 'cron'
        time: String,            // '09:00'
        dayOfWeek: Number,       // 0-6 for weekly
        dayOfMonth: Number,      // 1-31 for monthly
        cronExpression: String   // For complex schedules
    },
    active: Boolean,
    lastRun: Date,
    nextRun: Date
};

Recurring Examples:

javascript

// Daily reminder jam 9 pagi
const dailyReminder = {
    id: 'daily-reminder',
    name: 'Daily Morning Reminder',
    recipients: ['segment:active_customers'],
    message: 'Selamat pagi! ☀️ Jangan lupa cek promo hari ini!',
    schedule: {
        type: 'daily',
        time: '09:00'
    },
    active: true
};

// Weekly report setiap Senin
const weeklyReport = {
    id: 'weekly-report',
    name: 'Weekly Report',
    recipients: ['segment:vip_customers'],
    message: 'Hai! Ini rangkuman belanja kamu minggu ini...',
    schedule: {
        type: 'weekly',
        time: '10:00',
        dayOfWeek: 1 // Senin
    },
    active: true
};

// Monthly birthday promo
const birthdayPromo = {
    id: 'birthday-promo',
    name: 'Birthday Promo',
    recipients: ['segment:birthday_this_month'],
    message: 'Happy birthday! 🎂 Ini voucher spesial untuk kamu!',
    schedule: {
        type: 'monthly',
        time: '08:00',
        dayOfMonth: 1 // Awal bulan
    },
    active: true
};

// Complex: Setiap weekday jam 9 dan 14
const complexSchedule = {
    id: 'complex-reminder',
    name: 'Weekday Reminders',
    message: 'Hai! Ada yang bisa dibantu?',
    schedule: {
        type: 'cron',
        cronExpression: '0 9,14 * * 1-5' // Mon-Fri, 9am & 2pm
    },
    active: true
};

Recurring Processor:

javascript

// Process recurring schedules
cron.schedule('* * * * *', async () => {
    const now = new Date();
    
    const dueSchedules = await db.recurringSchedules.find({
        active: true,
        nextRun: { $lte: now }
    }).toArray();
    
    for (const schedule of dueSchedules) {
        await processRecurringSchedule(schedule);
    }
});

async function processRecurringSchedule(schedule) {
    // Get recipients
    const recipients = await resolveRecipients(schedule.recipients);
    
    // Send to all
    for (const recipient of recipients) {
        const personalizedMessage = personalizeMessage(
            schedule.message,
            recipient
        );
        
        await sendWhatsApp(recipient.phone, personalizedMessage);
        await sleep(100); // Rate limiting
    }
    
    // Update next run
    const nextRun = calculateNextRun(schedule.schedule);
    
    await db.recurringSchedules.updateOne(
        { id: schedule.id },
        { 
            $set: { 
                lastRun: new Date(),
                nextRun
            }
        }
    );
}

function calculateNextRun(scheduleConfig) {
    const now = new Date();
    
    switch (scheduleConfig.type) {
        case 'daily':
            const [hours, minutes] = scheduleConfig.time.split(':');
            const tomorrow = new Date(now);
            tomorrow.setDate(tomorrow.getDate() + 1);
            tomorrow.setHours(parseInt(hours), parseInt(minutes), 0, 0);
            return tomorrow;
            
        case 'weekly':
            const nextWeek = new Date(now);
            const daysUntil = (scheduleConfig.dayOfWeek - now.getDay() + 7) % 7 || 7;
            nextWeek.setDate(nextWeek.getDate() + daysUntil);
            const [h, m] = scheduleConfig.time.split(':');
            nextWeek.setHours(parseInt(h), parseInt(m), 0, 0);
            return nextWeek;
            
        case 'cron':
            const parser = require('cron-parser');
            const interval = parser.parseExpression(scheduleConfig.cronExpression);
            return interval.next().toDate();
            
        default:
            return null;
    }
}

Trigger-Based Delay

Delayed Follow Up:

javascript

// Kirim follow up X waktu setelah event
async function scheduleFollowUp(event, phone, delays) {
    for (const delay of delays) {
        const sendAt = new Date(Date.now() + delay.after);
        
        await scheduleMessage(
            phone,
            delay.message,
            sendAt
        );
    }
}

// Usage: Follow up sequence setelah order
const orderFollowUps = [
    {
        after: 1 * 60 * 60 * 1000, // 1 jam
        message: 'Hai! Order kamu sedang diproses. Ada pertanyaan?'
    },
    {
        after: 24 * 60 * 60 * 1000, // 1 hari
        message: 'Update: Pesanan kamu sudah siap kirim!'
    },
    {
        after: 3 * 24 * 60 * 60 * 1000, // 3 hari
        message: 'Paket harusnya sudah sampai. Gimana produknya? 😊'
    }
];

// Saat ada order baru
async function onNewOrder(order) {
    await scheduleFollowUp('order', order.phone, orderFollowUps);
}

Conditional Scheduling:

javascript

// Schedule dengan kondisi
async function scheduleConditional(recipient, message, sendAt, condition) {
    const scheduled = {
        id: generateId(),
        recipient,
        message,
        scheduledAt: new Date(sendAt),
        status: 'pending',
        condition // { type: 'order_not_paid', orderId: '123' }
    };
    
    await db.scheduledMessages.insertOne(scheduled);
}

// Processor check condition sebelum kirim
async function processScheduledWithCondition(msg) {
    if (msg.condition) {
        const shouldSend = await checkCondition(msg.condition);
        
        if (!shouldSend) {
            await db.scheduledMessages.updateOne(
                { id: msg.id },
                { $set: { status: 'skipped', reason: 'condition_not_met' } }
            );
            return;
        }
    }
    
    // Proceed with sending
    await sendWhatsApp(msg.recipient, msg.message);
}

async function checkCondition(condition) {
    switch (condition.type) {
        case 'order_not_paid':
            const order = await db.orders.findOne({ id: condition.orderId });
            return order.status !== 'paid';
            
        case 'no_response':
            const lastMessage = await getLastMessage(condition.phone);
            return lastMessage.direction === 'outgoing'; // No reply yet
            
        default:
            return true;
    }
}

Time-Window Auto Reply

Office Hours Response:

javascript

function getTimeBasedGreeting() {
    const hour = new Date().getHours();
    
    if (hour >= 5 && hour < 12) {
        return {
            greeting: 'Selamat pagi! ☀️',
            availability: 'Tim kami siap membantu!'
        };
    }
    
    if (hour >= 12 && hour < 15) {
        return {
            greeting: 'Selamat siang! 🌤️',
            availability: 'Kami online sampai jam 21:00'
        };
    }
    
    if (hour >= 15 && hour < 18) {
        return {
            greeting: 'Selamat sore! 🌅',
            availability: 'Masih sempat order hari ini!'
        };
    }
    
    if (hour >= 18 && hour < 21) {
        return {
            greeting: 'Selamat malam! 🌙',
            availability: 'CS online sampai jam 21:00'
        };
    }
    
    // After hours (21:00 - 05:00)
    return {
        greeting: 'Hai! 👋',
        availability: 'CS akan balas besok pagi jam 09:00. Pesanmu sudah tercatat!'
    };
}

// Auto reply dengan time-based greeting
async function handleIncomingMessage(from, message) {
    const timeContext = getTimeBasedGreeting();
    
    const response = `
${timeContext.greeting}

${timeContext.availability}

Ada yang bisa kami bantu?

1️⃣ Lihat Produk
2️⃣ Cek Pesanan
3️⃣ Bantuan
    `;
    
    await sendWhatsApp(from, response);
}

Campaign Scheduling UI

Schedule Campaign:

javascript

// API endpoint untuk schedule campaign
app.post('/api/campaigns/schedule', async (req, res) => {
    const { name, message, recipients, scheduledAt, recurring } = req.body;
    
    const campaign = {
        id: generateId(),
        name,
        message,
        recipients, // Array atau segment ID
        scheduledAt: new Date(scheduledAt),
        recurring: recurring || null,
        status: 'scheduled',
        createdAt: new Date(),
        stats: {
            total: 0,
            sent: 0,
            delivered: 0,
            failed: 0
        }
    };
    
    await db.campaigns.insertOne(campaign);
    
    res.json({ success: true, campaign });
});

// Get upcoming scheduled campaigns
app.get('/api/campaigns/upcoming', async (req, res) => {
    const campaigns = await db.campaigns.find({
        status: 'scheduled',
        scheduledAt: { $gte: new Date() }
    }).sort({ scheduledAt: 1 }).toArray();
    
    res.json(campaigns);
});

// Cancel scheduled campaign
app.post('/api/campaigns/:id/cancel', async (req, res) => {
    await db.campaigns.updateOne(
        { id: req.params.id, status: 'scheduled' },
        { $set: { status: 'cancelled', cancelledAt: new Date() } }
    );
    
    res.json({ success: true });
});

Best Practices

DO ✅

- Schedule di waktu optimal
- Include cancel/reschedule option
- Test timezone handling
- Log semua scheduled messages
- Handle failures gracefully
- Conditional scheduling

DON'T ❌

- Schedule terlalu banyak sekaligus
- Ignore timezone
- No logging
- Skip error handling
- Fixed schedule tanpa kondisi
- Schedule spam

FAQ

Bagaimana handle timezone?

Simpan dalam UTC, convert ke local timezone saat display dan schedule.

Kalau server mati saat scheduled time?

Pesan akan terkirim saat server up karena check by scheduledAt <= now.

Berapa banyak bisa schedule sekaligus?

Tergantung infrastructure. Dengan queue system, ribuan tidak masalah.


Kesimpulan

Scheduling = Set it and forget it!

Manual SendingScheduled
Harus onlineOtomatis
Bisa lupaPasti terkirim
Inconsistent timingTepat waktu
Time-consumingEfisien

Setup Message Scheduling →


Artikel Terkait