@OnSchedule

Run tasks on a recurring schedule using cron expressions.

Overview

The @OnSchedule decorator marks methods that run on a recurring schedule. Schedules are defined using standard cron syntax and executed via Google Cloud Scheduler.

import { HyperfoldAgent, OnSchedule } from '@hyperfold/actions-sdk';
@HyperfoldAgent({ name: 'ops-bot', type: 'custom' })
export class OpsBot {
  @OnSchedule('0 9 * * *')  // Every day at 9:00 AM
  async dailyReport() {
    const stats = await this.analytics.getDailyStats();
    await this.notifications.sendSlack({
      channel: '#sales-reports',
      message: `Daily Report: ${stats.conversions} conversions, $${stats.revenue}`,
    });
  }
  @OnSchedule('0 * * * *')  // Every hour
  async syncInventory() {
    await this.integrations.shopify.syncInventory();
  }
  @OnSchedule('0 0 1 * *')  // First day of each month
  async monthlyReconciliation() {
    await this.billing.reconcile();
  }
}

Cron Syntax

Standard 5-field cron expressions:

// minute hour day month weekday
// * * * * *
@OnSchedule('0 9 * * *')     // 9:00 AM every day
@OnSchedule('30 14 * * *')   // 2:30 PM every day
@OnSchedule('0 */4 * * *')   // Every 4 hours
@OnSchedule('*/15 * * * *')  // Every 15 minutes
@OnSchedule('0 9 * * 1')     // 9:00 AM every Monday
@OnSchedule('0 9 * * 1-5')   // 9:00 AM weekdays
@OnSchedule('0 0 1 * *')     // Midnight on 1st of month
@OnSchedule('0 0 * * 0')     // Midnight every Sunday

Common Patterns

ExpressionDescription
*/15 * * * *Every 15 minutes
0 * * * *Every hour
0 9 * * *Daily at 9 AM
0 9 * * 1-5Weekdays at 9 AM
0 0 * * 0Weekly on Sunday
0 0 1 * *Monthly on the 1st

Examples

Common scheduled task patterns:

@HyperfoldAgent({ name: 'maintenance-bot', type: 'custom' })
export class MaintenanceBot {
  @OnSchedule('*/15 9-17 * * 1-5')
  async frequentInventorySync() {
    await this.catalog.syncWithShopify();
  }
  @OnSchedule('0 * * * *')
  async checkCompetitorPrices() {
    const products = await this.catalog.getAll();
    for (const product of products) {
      const competitorPrice = await this.pricing.fetchCompetitorPrice(product.id);
      await this.pricing.updateCompetitorCache(product.id, competitorPrice);
    }
  }
  @OnSchedule('0 9 * * *')
  async dailyPerformanceReport() {
    const yesterday = await this.analytics.getStats('yesterday');
    await this.notifications.sendEmail({
      to: 'team@company.com',
      subject: `Daily Report: ${yesterday.date}`,
      template: 'daily-report',
      data: yesterday,
    });
  }
  @OnSchedule('0 9 * * 1')
  async weeklyDigest() {
    const weekStats = await this.analytics.getStats('last_7_days');
    await this.notifications.sendSlack({
      channel: '#leadership',
      blocks: this.formatWeeklyDigest(weekStats),
    });
  }
  @OnSchedule('0 2 1 * *')
  async monthlyReconciliation() {
    const month = new Date().toISOString().slice(0, 7);
    const usage = await this.billing.calculateMonthlyUsage(month);
    await this.billing.generateInvoices(usage);
    await this.notifications.sendEmail({
      to: 'billing@company.com',
      subject: `Monthly Billing: ${month}`,
      data: usage,
    });
  }
  @OnSchedule('0 3 * * *')
  async cleanupSessions() {
    const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
    const deleted = await this.sessions.deleteOlderThan(cutoff);
    console.log(`Cleaned up ${deleted} expired sessions`);
  }
}

Timezone Handling

Default timezone is UTC. Specify timezone for business-hours tasks:

@OnSchedule('0 9 * * *')  // 9 AM UTC
async task() { }

@OnSchedule('0 9 * * *', { timezone: 'America/New_York' })
async eastCoastMorning() {
  // Runs at 9 AM Eastern Time
}
@OnSchedule('0 9 * * *', { timezone: 'Europe/London' })
async londonMorning() {
  // Runs at 9 AM London time
}

Best Practices

Use locking for long-running tasks, handle failures gracefully, and use idempotent operations. Avoid assuming tasks run at exactly the scheduled time—Cloud Scheduler may have slight delays.

@HyperfoldAgent({ name: 'scheduled-ops', type: 'custom' })
export class ScheduledOps {
  @OnSchedule('0 * * * *')
  async expensiveTask() {
    const lock = await this.locks.acquire('expensive-task', { ttl: 3600 });
    if (!lock) {
      console.log('Task already running, skipping');
      return;
    }
    try {
      await this.doExpensiveWork();
    } finally {
      await lock.release();
    }
  }
  @OnSchedule('0 9 * * *')
  async criticalTask() {
    try {
      await this.doCriticalWork();
    } catch (error) {
      await this.alerts.send({
        severity: 'critical',
        message: `Critical task failed: ${error.message}`,
      });
      throw error;
    }
  }
  @OnSchedule('*/5 * * * *')
  async syncData() {
    const lastSync = await this.state.get('last_sync_timestamp');
    const items = await this.source.getModifiedSince(lastSync);
    for (const item of items) {
      await this.destination.upsert(item);
    }
    await this.state.set('last_sync_timestamp', new Date().toISOString());
  }
}