Skip to main content

Overview

Message scheduling in GoBlue allows you to send messages at optimal times for maximum engagement, create automated follow-up sequences, and respect recipient preferences. This guide covers advanced scheduling strategies, timezone handling, and automation techniques for sophisticated messaging campaigns.
Message scheduling works in conjunction with iOS Shortcuts, which handles the actual message delivery at the scheduled time.

Scheduling Fundamentals

How Scheduling Works

GoBlue’s scheduling system operates through a multi-layer approach:
1

Queue Management

Messages are queued with scheduled delivery times
2

iOS Shortcuts Processing

iOS Shortcuts polls the queue and sends messages at scheduled times
3

Status Updates

Delivery confirmation updates message status in real-time
4

Analytics Tracking

Performance metrics track optimal sending times

Scheduling Types

Immediate

Use Case: Urgent responses, confirmations Delay: 0-5 minutes Priority: High

Delayed

Use Case: Follow-ups, nurture sequences Delay: Hours to days Priority: Normal

Optimal Time

Use Case: Marketing campaigns, announcements Delay: Based on recipient behavior Priority: Variable

Sequence-Based

Use Case: Drip campaigns, onboarding Delay: Progressive intervals Priority: Low to normal

Timezone Management

Global Timezone Support

Handle recipients across different timezones effectively:
class TimezoneScheduler {
  constructor() {
    this.defaultTimezone = 'UTC';
    this.businessHours = { start: 9, end: 17 }; // 9 AM - 5 PM
  }
  
  scheduleForOptimalTime(contact, baseTime, options = {}) {
    const contactTimezone = this.getContactTimezone(contact);
    const localTime = moment.tz(baseTime, contactTimezone);
    
    // Adjust for business hours
    const adjustedTime = this.adjustForBusinessHours(localTime, options);
    
    // Avoid weekends if specified
    if (options.avoidWeekends) {
      adjustedTime = this.adjustForWeekends(adjustedTime);
    }
    
    return adjustedTime.toISOString();
  }
  
  getContactTimezone(contact) {
    // Priority order for timezone detection
    if (contact.customFields?.timezone) {
      return contact.customFields.timezone;
    }
    
    if (contact.phoneNumber) {
      return this.inferTimezoneFromPhone(contact.phoneNumber);
    }
    
    if (contact.customFields?.location) {
      return this.inferTimezoneFromLocation(contact.customFields.location);
    }
    
    return this.defaultTimezone;
  }
  
  adjustForBusinessHours(momentTime, options) {
    const hour = momentTime.hour();
    
    if (hour < this.businessHours.start) {
      // Too early - move to start of business hours
      return momentTime.hour(this.businessHours.start).minute(0).second(0);
    }
    
    if (hour >= this.businessHours.end) {
      // Too late - move to next business day
      return momentTime
        .add(1, 'day')
        .hour(this.businessHours.start)
        .minute(0)
        .second(0);
    }
    
    return momentTime;
  }
}

Timezone Detection Methods

inferTimezoneFromPhone(phoneNumber) {
  const phoneUtil = require('google-libphonenumber').PhoneNumberUtil.getInstance();
  
  try {
    const parsed = phoneUtil.parse(phoneNumber, null);
    const region = phoneUtil.getRegionCodeForNumber(parsed);
    
    const timezoneMap = {
      'US': this.getUSTimezoneFromNumber(phoneNumber),
      'CA': this.getCanadaTimezoneFromNumber(phoneNumber),
      'GB': 'Europe/London',
      'AU': this.getAustraliaTimezoneFromNumber(phoneNumber),
      'DE': 'Europe/Berlin',
      'FR': 'Europe/Paris'
    };
    
    return timezoneMap[region] || 'UTC';
  } catch (error) {
    return 'UTC';
  }
}

getUSTimezoneFromNumber(phoneNumber) {
  const areaCode = phoneNumber.substring(2, 5); // Extract area code
  
  const timezoneMapping = {
    // Eastern Time
    '212': 'America/New_York', '646': 'America/New_York',
    // Central Time  
    '214': 'America/Chicago', '469': 'America/Chicago',
    // Mountain Time
    '303': 'America/Denver', '720': 'America/Denver',
    // Pacific Time
    '213': 'America/Los_Angeles', '310': 'America/Los_Angeles'
  };
  
  return timezoneMapping[areaCode] || 'America/New_York'; // Default to Eastern
}
async inferTimezoneFromIP(ipAddress) {
  try {
    const response = await fetch(`https://ipapi.co/${ipAddress}/json/`);
    const data = await response.json();
    
    return data.timezone || 'UTC';
  } catch (error) {
    console.warn('IP geolocation failed:', error);
    return 'UTC';
  }
}
// Store timezone preferences in contact custom fields
{
  "firstName": "John",
  "phoneNumber": "+1234567890",
  "customFields": {
    "timezone": "America/Los_Angeles",
    "preferredContactHours": "10:00-16:00",
    "avoidWeekends": true,
    "preferredDays": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
  }
}

Optimal Timing Strategies

Engagement-Based Scheduling

Use historical data to determine the best times to message each contact:
class EngagementAnalyzer {
  async analyzeBestSendTimes(contactId) {
    const messageHistory = await this.getMessageHistory(contactId);
    const responses = messageHistory.filter(m => m.responseTime);
    
    if (responses.length < 3) {
      return this.getDefaultOptimalTimes();
    }
    
    // Analyze response patterns
    const timeAnalysis = this.analyzeResponseTimes(responses);
    
    return {
      bestHour: timeAnalysis.peakHour,
      bestDayOfWeek: timeAnalysis.peakDay,
      avgResponseTime: timeAnalysis.avgResponseTime,
      confidence: timeAnalysis.confidence
    };
  }
  
  analyzeResponseTimes(responses) {
    const hourCounts = {};
    const dayCounts = {};
    let totalResponseTime = 0;
    
    responses.forEach(response => {
      const sentTime = new Date(response.sentAt);
      const responseTime = new Date(response.respondedAt);
      
      const hour = sentTime.getHours();
      const day = sentTime.getDay();
      
      hourCounts[hour] = (hourCounts[hour] || 0) + 1;
      dayCounts[day] = (dayCounts[day] || 0) + 1;
      
      totalResponseTime += (responseTime - sentTime);
    });
    
    const peakHour = Object.keys(hourCounts)
      .reduce((a, b) => hourCounts[a] > hourCounts[b] ? a : b);
    
    const peakDay = Object.keys(dayCounts)
      .reduce((a, b) => dayCounts[a] > dayCounts[b] ? a : b);
    
    return {
      peakHour: parseInt(peakHour),
      peakDay: parseInt(peakDay),
      avgResponseTime: totalResponseTime / responses.length,
      confidence: responses.length / 10 // Confidence increases with more data
    };
  }
}

Industry-Specific Timing

Tailor message timing based on industry patterns:
  • B2B Industries
  • B2C Industries
  • Global Markets
Optimal Times:
  • Technology: Tuesday-Thursday, 10 AM - 3 PM
  • Healthcare: Monday-Friday, 9 AM - 11 AM, 2 PM - 4 PM
  • Finance: Tuesday-Thursday, 8 AM - 10 AM, 1 PM - 3 PM
  • Consulting: Monday-Thursday, 10 AM - 4 PM
const b2bTiming = {
  'technology': {
    days: [2, 3, 4], // Tue-Thu
    hours: [10, 11, 12, 13, 14, 15], // 10 AM - 3 PM
    avoid: ['holidays', 'friday-afternoon']
  },
  'healthcare': {
    days: [1, 2, 3, 4, 5], // Mon-Fri
    hours: [9, 10, 11, 14, 15, 16], // 9-11 AM, 2-4 PM
    avoid: ['lunch-hours', 'shift-changes']
  }
};

Automated Sequences

Drip Campaign Scheduling

Create automated follow-up sequences with progressive delays:
class SequenceScheduler {
  createDripSequence(contactData, sequenceType) {
    const sequences = {
      'lead-nurture': [
        { delay: 0, template: 'welcome', priority: 'high' },
        { delay: '24 hours', template: 'value-proposition', priority: 'normal' },
        { delay: '3 days', template: 'case-study', priority: 'normal' },
        { delay: '1 week', template: 'limited-offer', priority: 'high' },
        { delay: '2 weeks', template: 'final-follow-up', priority: 'low' }
      ],
      'customer-onboarding': [
        { delay: 0, template: 'welcome-customer', priority: 'high' },
        { delay: '2 hours', template: 'setup-guide', priority: 'high' },
        { delay: '1 day', template: 'check-progress', priority: 'normal' },
        { delay: '3 days', template: 'advanced-features', priority: 'normal' },
        { delay: '1 week', template: 'satisfaction-check', priority: 'low' }
      ],
      're-engagement': [
        { delay: 0, template: 'we-miss-you', priority: 'normal' },
        { delay: '5 days', template: 'special-offer', priority: 'high' },
        { delay: '2 weeks', template: 'final-attempt', priority: 'low' }
      ]
    };
    
    const sequence = sequences[sequenceType];
    if (!sequence) {
      throw new Error(`Unknown sequence type: ${sequenceType}`);
    }
    
    return this.scheduleSequence(contactData, sequence);
  }
  
  scheduleSequence(contactData, sequence) {
    const scheduledMessages = [];
    const baseTime = new Date();
    
    sequence.forEach((step, index) => {
      const sendTime = this.calculateSendTime(baseTime, step.delay, contactData);
      
      const message = {
        contactId: contactData.id,
        templateId: step.template,
        scheduledFor: sendTime.toISOString(),
        priority: step.priority,
        sequenceStep: index + 1,
        sequenceTotal: sequence.length,
        metadata: {
          sequenceType: sequenceType,
          stepDelay: step.delay
        }
      };
      
      scheduledMessages.push(message);
    });
    
    return scheduledMessages;
  }
  
  calculateSendTime(baseTime, delay, contactData) {
    let sendTime = moment(baseTime);
    
    if (typeof delay === 'string') {
      const [amount, unit] = delay.split(' ');
      sendTime = sendTime.add(parseInt(amount), unit);
    }
    
    // Adjust for optimal timing
    const optimalTime = this.getOptimalSendTime(contactData, sendTime);
    
    return optimalTime;
  }
}

Conditional Sequences

Create sequences that adapt based on recipient behavior:
class ConditionalSequence {
  createAdaptiveSequence(contactData) {
    const baseSequence = [
      {
        step: 1,
        delay: 0,
        template: 'initial-outreach',
        conditions: {
          responseReceived: 'end-sequence',
          noResponse: 'continue'
        }
      },
      {
        step: 2,
        delay: '48 hours',
        template: 'follow-up-value',
        conditions: {
          responseReceived: 'move-to-sales-sequence',
          emailOpened: 'continue-with-interest',
          noEngagement: 'continue'
        }
      },
      {
        step: 3,
        delay: '1 week',
        template: 'social-proof',
        conditions: {
          responseReceived: 'move-to-sales-sequence',
          previousCustomer: 'use-loyalty-template',
          noEngagement: 'continue'
        }
      }
    ];
    
    return this.processConditionalSequence(contactData, baseSequence);
  }
  
  async evaluateConditions(contact, step) {
    const conditions = step.conditions;
    const evaluation = {};
    
    // Check for responses
    const recentResponses = await this.getRecentResponses(contact.id, '48 hours');
    evaluation.responseReceived = recentResponses.length > 0;
    
    // Check engagement metrics
    const engagement = await this.getEngagementMetrics(contact.id);
    evaluation.emailOpened = engagement.emailOpens > 0;
    evaluation.linkClicked = engagement.linkClicks > 0;
    
    // Check contact history
    evaluation.previousCustomer = contact.customFields?.customerStatus === 'previous';
    
    return evaluation;
  }
}

Advanced Scheduling Features

Smart Retry Logic

Implement intelligent retry mechanisms for failed deliveries:
class SmartRetryScheduler {
  scheduleRetry(failedMessage, attempt = 1) {
    const maxRetries = 3;
    const baseDelay = 15; // minutes
    
    if (attempt > maxRetries) {
      return this.markMessageFailed(failedMessage);
    }
    
    // Exponential backoff with jitter
    const delay = baseDelay * Math.pow(2, attempt - 1);
    const jitter = Math.random() * 0.1 * delay; // 10% jitter
    const totalDelay = delay + jitter;
    
    const retryTime = moment().add(totalDelay, 'minutes');
    
    // Adjust for business hours
    const adjustedTime = this.adjustForBusinessHours(retryTime, failedMessage.contact);
    
    return {
      ...failedMessage,
      scheduledFor: adjustedTime.toISOString(),
      retryAttempt: attempt,
      priority: this.calculateRetryPriority(failedMessage, attempt)
    };
  }
  
  calculateRetryPriority(message, attempt) {
    const basePriority = message.priority;
    const priorityMap = {
      'urgent': 'urgent', // Keep urgent messages urgent
      'high': attempt === 1 ? 'high' : 'normal',
      'normal': attempt <= 2 ? 'normal' : 'low',
      'low': 'low'
    };
    
    return priorityMap[basePriority] || 'normal';
  }
}

Load Balancing

Distribute message load across time to avoid overwhelming iOS Shortcuts:
class LoadBalancer {
  distributeMessageLoad(messages, timeWindow = '1 hour') {
    const windowMs = moment.duration(timeWindow).asMilliseconds();
    const maxMessagesPerMinute = 30; // iOS Shortcuts capacity
    
    // Sort messages by priority and original schedule time
    const sortedMessages = this.sortMessagesByPriority(messages);
    
    const distributedMessages = [];
    let currentTime = moment();
    let messagesInCurrentMinute = 0;
    
    sortedMessages.forEach(message => {
      if (messagesInCurrentMinute >= maxMessagesPerMinute) {
        currentTime = currentTime.add(1, 'minute');
        messagesInCurrentMinute = 0;
      }
      
      // Ensure we don't send before the original scheduled time
      const originalTime = moment(message.scheduledFor);
      if (currentTime.isBefore(originalTime)) {
        currentTime = originalTime;
      }
      
      distributedMessages.push({
        ...message,
        scheduledFor: currentTime.toISOString(),
        distributionApplied: true
      });
      
      messagesInCurrentMinute++;
      currentTime = currentTime.add(2, 'seconds'); // Small gap between messages
    });
    
    return distributedMessages;
  }
}

Seasonal Adjustments

Automatically adjust scheduling for holidays and special events:
class SeasonalScheduler {
  constructor() {
    this.holidays = this.loadHolidayCalendar();
    this.blackouts = this.loadBlackoutPeriods();
  }
  
  adjustForSeasons(scheduledTime, contact) {
    const timezone = this.getContactTimezone(contact);
    const localTime = moment.tz(scheduledTime, timezone);
    
    // Check for holidays
    if (this.isHoliday(localTime, contact.country)) {
      return this.rescheduleAroundHoliday(localTime, contact);
    }
    
    // Check for industry-specific blackouts
    if (this.isBlackoutPeriod(localTime, contact.industry)) {
      return this.rescheduleAroundBlackout(localTime, contact);
    }
    
    // Seasonal optimizations
    return this.applySeasonalOptimizations(localTime, contact);
  }
  
  loadHolidayCalendar() {
    return {
      'US': [
        { date: '2024-01-01', name: 'New Year\'s Day' },
        { date: '2024-07-04', name: 'Independence Day' },
        { date: '2024-11-28', name: 'Thanksgiving' },
        { date: '2024-12-25', name: 'Christmas Day' }
      ],
      'UK': [
        { date: '2024-01-01', name: 'New Year\'s Day' },
        { date: '2024-12-25', name: 'Christmas Day' },
        { date: '2024-12-26', name: 'Boxing Day' }
      ]
    };
  }
  
  rescheduleAroundHoliday(holidayTime, contact) {
    // Move to next business day
    let newTime = holidayTime.clone();
    
    do {
      newTime = newTime.add(1, 'day');
    } while (this.isWeekend(newTime) || this.isHoliday(newTime, contact.country));
    
    // Maintain the same time of day
    return newTime.hour(holidayTime.hour()).minute(holidayTime.minute());
  }
}

Monitoring and Analytics

Scheduling Performance Metrics

Delivery Timing

  • On-Time Delivery Rate: % of messages sent at scheduled time
  • Average Delay: Difference between scheduled and actual send time
  • Queue Processing Time: Time spent in processing queue
  • Peak Load Handling: Performance during high-volume periods

Engagement Impact

  • Response Rate by Send Time: Correlation between timing and responses
  • Optimal Time Accuracy: How well predictions match actual performance
  • Timezone Effectiveness: Response rates across different timezones
  • Sequence Completion Rates: How many recipients complete sequences

Real-Time Monitoring

class SchedulingMonitor {
  async generateSchedulingReport() {
    const now = moment();
    const timeWindows = {
      next1Hour: now.clone().add(1, 'hour'),
      next24Hours: now.clone().add(24, 'hours'),
      next7Days: now.clone().add(7, 'days')
    };
    
    const report = {};
    
    for (const [window, endTime] of Object.entries(timeWindows)) {
      const queuedMessages = await this.getQueuedMessages(now, endTime);
      
      report[window] = {
        totalMessages: queuedMessages.length,
        priorityBreakdown: this.analyzePriority(queuedMessages),
        timezoneDistribution: this.analyzeTimezones(queuedMessages),
        loadDistribution: this.analyzeLoadDistribution(queuedMessages),
        potentialIssues: this.identifyPotentialIssues(queuedMessages)
      };
    }
    
    return report;
  }
  
  identifyPotentialIssues(messages) {
    const issues = [];
    
    // Check for load spikes
    const loadByMinute = this.groupMessagesByMinute(messages);
    Object.entries(loadByMinute).forEach(([minute, count]) => {
      if (count > 50) { // Threshold for concern
        issues.push({
          type: 'load_spike',
          time: minute,
          messageCount: count,
          severity: count > 100 ? 'high' : 'medium'
        });
      }
    });
    
    // Check for timezone mismatches
    const timezoneIssues = this.checkTimezoneIssues(messages);
    issues.push(...timezoneIssues);
    
    return issues;
  }
}

Best Practices

Scheduling Strategy

Respect Preferences

  • Honor recipient timezone preferences
  • Avoid known blackout periods
  • Implement frequency capping
  • Provide opt-out options for scheduling

Test and Optimize

  • A/B test different send times
  • Monitor engagement patterns
  • Adjust based on performance data
  • Track seasonal variations

Technical Reliability

  • Implement robust retry logic
  • Monitor queue health
  • Handle timezone edge cases
  • Plan for system maintenance

User Experience

  • Provide clear expectations
  • Allow schedule modifications
  • Show delivery status
  • Enable emergency overrides

Common Pitfalls to Avoid

Problem: Messages sent at wrong local times Solution: Always store and display times in recipient’s timezone
// Wrong: Using sender's timezone
const sendTime = moment().add(1, 'day').hour(10);

// Right: Using recipient's timezone
const sendTime = moment.tz(contact.timezone).add(1, 'day').hour(10);
Problem: Sequences continue after recipient responds Solution: Implement response monitoring and sequence halting
async function checkSequenceContinuation(messageId) {
  const message = await this.getMessage(messageId);
  const responses = await this.getResponsesSince(message.contactId, message.sentAt);
  
  if (responses.length > 0) {
    await this.cancelRemainingSequence(message.contactId, message.sequenceId);
  }
}
Problem: Too many messages scheduled simultaneously Solution: Implement load balancing and rate limiting
const rateLimiter = new RateLimiter({
  messagesPerMinute: 30,
  messagesPerHour: 1000,
  queueWarningThreshold: 500
});

Next Steps

Start with simple scheduling strategies and gradually add complexity as you learn your audience’s preferences and behavior patterns.