Scheduled Tasks
Scheduled tasks (cron jobs) let you run code on a regular schedule - daily reports, weekly cleanups, hourly health checks, or any time-based operation. Your functions work the same whether they're triggered by a runtime scheduler (Azure Timer, AWS EventBridge) or by the @pikku/scheduler package.
Your domain functions don't need to know they're being called by a scheduler. They just run on schedule and do their work. Pikku handles all the scheduling details.
Your First Scheduled Task​
Let's create a daily maintenance task:
// maintenance.function.ts
import { pikkuVoidFunc } from '#pikku'
export const runDailyMaintenance = pikkuVoidFunc({
func: async ({ database, logger }) => {
logger.info('Starting daily maintenance')
// Clean up old records
await database.delete('logs', {
where: { createdAt: { lt: Date.now() - 30 * 24 * 60 * 60 * 1000 } }
})
// Update statistics
await database.query('UPDATE stats SET last_updated = NOW()')
logger.info('Daily maintenance completed')
},
auth: false, // Scheduled tasks don't have user sessions
docs: {
summary: 'Run daily maintenance tasks',
tags: ['maintenance']
}
})
// maintenance.schedule.ts
import { wireScheduler } from '#pikku/scheduler'
import { runDailyMaintenance } from './functions/maintenance.function.js'
wireScheduler({
cron: '0 3 * * *', // Run at 3:00 AM UTC every day
func: runDailyMaintenance
})
That's it! Your function will now run every day at 3 AM UTC.
If triggered by a serverless function like Lambda or Azure Timer, the platform's scheduler overrides this cron setting. The cron in wireScheduler is only for in-process scheduling (using @pikku/scheduler).
Scheduled Functions​
Scheduled functions use pikkuVoidFunc (an alias for pikkuSessionlessFunc<void, void>) since they:
- Don't receive any input data
- Don't return any output
- Don't have user sessions
import { pikkuVoidFunc } from '#pikku'
export const generateWeeklyReport = pikkuVoidFunc({
func: async ({ database, emailService, logger }) => {
logger.info('Generating weekly report')
const stats = await database.query('SELECT * FROM weekly_stats')
await emailService.send({
to: 'admin@example.com',
subject: 'Weekly Report',
template: 'weekly-report',
data: stats
})
logger.info('Weekly report sent')
},
auth: false,
docs: {
summary: 'Generate and send weekly report',
tags: ['reports']
}
})
Since scheduled functions are just regular Pikku functions, you can also wire them to HTTP endpoints for manual triggering during testing or emergencies:
wireHTTP({
method: 'post',
route: '/admin/trigger-maintenance',
func: runDailyMaintenance,
permissions: { admin: requireAdmin }
})
Cron Expressions​
Cron expressions use the standard 5-field format:
┌────────────── minute (0-59)
│ ┌───── ─────── hour (0-23)
│ │ ┌────────── day of month (1-31)
│ │ │ ┌──────── month (1-12)
│ │ │ │ ┌────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
Common examples:
// Every 5 minutes
wireScheduler({
cron: '*/5 * * * *',
func: healthCheck
})
// Every hour at minute 30
wireScheduler({
cron: '30 * * * *',
func: hourlyUpdate
})
// Every day at 2:30 AM
wireScheduler({
cron: '30 2 * * *',
func: dailyBackup
})
// Every Monday at 9:00 AM
wireScheduler({
cron: '0 9 * * 1',
func: weeklyReport
})
// First day of every month at midnight
wireScheduler({
cron: '0 0 1 * *',
func: monthlyBilling
})
Cron expressions are interpreted as UTC by default (unless your runtime specifies otherwise). Make sure to account for timezone differences when scheduling tasks.
Configuration​
Basic Wiring​
import { wireScheduler } from '#pikku/scheduler'
import { runMaintenance } from './functions/maintenance.function.js'
wireScheduler({
// Required
cron: '0 3 * * *',
func: runMaintenance,
// Optional
middleware: [loggingMiddleware],
tags: ['maintenance', 'critical']
})
Sourcing Cron from Config​
For different schedules across environments:
import { wireScheduler } from '#pikku/scheduler'
import { config } from './config.js'
import { runBackup } from './functions/backup.function.js'
wireScheduler({
cron: config.backupSchedule, // '0 2 * * *' in prod, '0 */4 * * *' in dev
func: runBackup
})
Middleware​
Middleware is important for scheduled tasks since they don't have automatic observability. Add logging, metrics, or error handling:
import { pikkuMiddleware } from '#pikku'
export const schedulerMetrics = pikkuMiddleware(
async ({ logger }, interaction, next) => {
// Guard for scheduled task interaction
if (!interaction.scheduledTask) {
throw new InvalidMiddlewareInteractionError(
'schedulerMetrics middleware can only be used with scheduled tasks'
)
}
const start = Date.now()
const taskName = interaction.scheduledTask.name
logger.info('Scheduled task started', { task: taskName })
try {
await next()
logger.info('Scheduled task completed', {
task: taskName,
duration: Date.now() - start
})
} catch (error) {
logger.error('Scheduled task failed', {
task: taskName,
error: error.message,
duration: Date.now() - start
})
throw error
}
}
)
Per-Schedule Middleware​
wireScheduler({
cron: '0 3 * * *',
func: runMaintenance,
middleware: [schedulerMetrics, retryMiddleware]
})
Global Middleware​
Apply middleware to all scheduled tasks:
import { addSchedulerMiddleware } from '#pikku/scheduler'
addSchedulerMiddleware([schedulerMetrics, alertMiddleware])
Error Handling​
Scheduled tasks should throw errors for failures. How errors are handled depends on your runtime:
export const syncData = pikkuVoidFunc({
func: async ({ database, externalAPI, logger }) => {
try {
const data = await externalAPI.fetch()
await database.insert('synced_data', data)
logger.info('Data synced successfully')
} catch (error) {
logger.error('Data sync failed', { error: error.message })
// Throw to indicate failure
throw new DataSyncError(error.message)
}
},
auth: false,
docs: {
summary: 'Sync external data',
tags: ['sync'],
errors: ['DataSyncError']
}
})
Some runtimes automatically retry failed scheduled tasks, while others require middleware or external retry logic.
Grouping Multiple Schedules​
You can wire multiple schedules in a single file:
// housekeeping.schedule.ts
import { wireScheduler } from '#pikku/scheduler'
import {
cleanupOldLogs,
updateStatistics,
archiveOldData,
refreshCache
} from './functions/housekeeping.function.js'
// Every hour - cleanup logs
wireScheduler({
cron: '0 * * * *',
func: cleanupOldLogs
})
// Every 6 hours - update stats
wireScheduler({
cron: '0 */6 * * *',
func: updateStatistics
})
// Daily at 2 AM - archive old data
wireScheduler({
cron: '0 2 * * *',
func: archiveOldData
})
// Every 15 minutes - refresh cache
wireScheduler({
cron: '*/15 * * * *',
func: refreshCache
})
Orchestrating Complex Tasks​
For complex workflows, use rpc.invoke() to orchestrate multiple operations:
export const monthlyProcessing = pikkuVoidFunc({
func: async ({ rpc, logger }) => {
logger.info('Starting monthly processing')
// Run multiple tasks in sequence
await rpc.invoke('generateMonthlyReports', {})
await rpc.invoke('processMonthlyBilling', {})
await rpc.invoke('archiveMonthlyData', {})
await rpc.invoke('sendMonthlyNotifications', {})
logger.info('Monthly processing completed')
},
auth: false,
docs: {
summary: 'Run all monthly processing tasks',
tags: ['monthly', 'batch']
}
})
Next Steps​
- Middleware - Adding observability to scheduled tasks
- RPC - Orchestrating complex workflows
- Errors - Error handling patterns