Documentation Index Fetch the complete documentation index at: https://mintlify.com/platforma-dev/platforma/llms.txt
Use this file to discover all available pages before exploring further.
Platforma provides a cron-based scheduler for running periodic tasks with full cron expression support.
Overview
The scheduler.Scheduler executes an application.Runner according to a cron schedule. It provides:
Standard 5-field cron syntax
Named schedules (@daily, @hourly, etc.)
Interval syntax (@every 30m)
Automatic validation at construction time
Context-aware execution with trace IDs
Creating a Scheduler
Create a scheduler with a cron expression and a runner:
import (
" github.com/platforma-dev/platforma/scheduler "
)
scheduler , err := scheduler . New ( "0 9 * * MON-FRI" , myRunner )
if err != nil {
return fmt . Errorf ( "invalid cron expression: %w " , err )
}
The constructor validates the cron expression and returns an error immediately if it’s invalid:
// This fails at construction time, not when Run() is called
scheduler , err := scheduler . New ( "" , myRunner ) // Error: empty expression
scheduler , err := scheduler . New ( "invalid" , myRunner ) // Error: parse error
Cron Expression Syntax
Standard cron uses 5 fields:
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday)
│ │ │ │ │
* * * * *
Examples
Every 5 minutes
Every 2 hours
9 AM on weekdays
Midnight on 1st of month
Every Monday at 3:30 PM
Named Schedules
Use predefined schedule names:
@yearly or @annually - Run once a year at midnight on January 1st (0 0 1 1 *)
@monthly - Run once a month at midnight on the first day (0 0 1 * *)
@weekly - Run once a week at midnight on Sunday (0 0 * * 0)
@daily or @midnight - Run once a day at midnight (0 0 * * *)
@hourly - Run once an hour at the start of the hour (0 * * * *)
scheduler , err := scheduler . New ( "@daily" , backupRunner )
Interval Syntax
Use @every for simple intervals:
scheduler . New ( "@every 30m" , runner ) // Every 30 minutes
scheduler . New ( "@every 2h" , runner ) // Every 2 hours
scheduler . New ( "@every 1h30m" , runner ) // Every 1.5 hours
scheduler . New ( "@every 5s" , runner ) // Every 5 seconds
Running the Scheduler
Start the scheduler with Run(ctx):
err := scheduler . Run ( ctx )
if err != nil {
return err
}
The scheduler:
Starts the cron scheduler
Blocks until the context is cancelled
Stops the scheduler gracefully
Returns when all tasks complete
Task Execution
Each scheduled execution:
Gets a new context with a unique trace ID
Logs start and completion
Logs errors but continues running
Maintains the original parent context
// Each execution gets a unique trace ID
runCtx := context . WithValue ( ctx , log . TraceIDKey , uuid . NewString ())
log . InfoContext ( runCtx , "scheduler task started" )
err := runner . Run ( runCtx )
if err != nil {
log . ErrorContext ( runCtx , "error in scheduler" , "error" , err )
// Continues to next scheduled run
}
log . InfoContext ( runCtx , "scheduler task finished" )
Implementing a Runner
Your task must implement the application.Runner interface:
type Runner interface {
Run ( context . Context ) error
}
Struct-based Runner
Function-based Runner
type BackupRunner struct {
db * database . Database
s3 * s3 . Client
}
func ( r * BackupRunner ) Run ( ctx context . Context ) error {
log . InfoContext ( ctx , "starting backup" )
// Perform backup
data , err := r . db . Export ( ctx )
if err != nil {
return fmt . Errorf ( "export failed: %w " , err )
}
err = r . s3 . Upload ( ctx , data )
if err != nil {
return fmt . Errorf ( "upload failed: %w " , err )
}
log . InfoContext ( ctx , "backup completed" )
return nil
}
Complete Example
Create a runner
Implement the task logic: type CleanupRunner struct {
db * database . Database
}
func ( r * CleanupRunner ) Run ( ctx context . Context ) error {
log . InfoContext ( ctx , "cleaning up expired sessions" )
result , err := r . db . Connection (). ExecContext ( ctx ,
"DELETE FROM sessions WHERE expires < NOW()" )
if err != nil {
return fmt . Errorf ( "cleanup failed: %w " , err )
}
rows , _ := result . RowsAffected ()
log . InfoContext ( ctx , "cleanup complete" , "deleted" , rows )
return nil
}
Create the scheduler
Configure the schedule: runner := & CleanupRunner { db : db }
// Run daily at 2 AM
scheduler , err := scheduler . New ( "0 2 * * *" , runner )
if err != nil {
return err
}
Register with application
Add to your application’s runners: app := application . New ()
app . AddRunner ( "cleanup-scheduler" , scheduler )
if err := app . Run ( ctx ); err != nil {
log . Error ( "application failed" , "error" , err )
}
Multiple Schedulers
Create multiple schedulers for different tasks:
app := application . New ()
// Daily backup at 2 AM
backupScheduler , _ := scheduler . New ( "0 2 * * *" , backupRunner )
app . AddRunner ( "backup-scheduler" , backupScheduler )
// Cleanup every 6 hours
cleanupScheduler , _ := scheduler . New ( "0 */6 * * *" , cleanupRunner )
app . AddRunner ( "cleanup-scheduler" , cleanupScheduler )
// Health check every 5 minutes
healthScheduler , _ := scheduler . New ( "*/5 * * * *" , healthRunner )
app . AddRunner ( "health-scheduler" , healthScheduler )
app . Run ( ctx )
Time Zone
Schedulers use UTC by default. All cron expressions are evaluated in UTC:
// This runs at 9 AM UTC, not local time
scheduler . New ( "0 9 * * *" , runner )
Error Handling
Errors from the runner are logged but don’t stop the scheduler:
func ( r * MyRunner ) Run ( ctx context . Context ) error {
err := r . doWork ( ctx )
if err != nil {
// This error is logged but the scheduler continues
return fmt . Errorf ( "work failed: %w " , err )
}
return nil
}
If you want to stop the scheduler on error, cancel the context from within the runner:
func ( r * MyRunner ) Run ( ctx context . Context ) error {
err := r . doWork ( ctx )
if errors . Is ( err , ErrCritical ) {
// Cancel the context to stop the scheduler
r . cancel ()
}
return err
}
Best Practices
Validate early The constructor validates cron expressions at creation time, so invalid schedules fail fast during application startup.
Use named schedules Prefer @daily over 0 0 * * * for better readability and intent.
Make tasks idempotent Tasks may run multiple times if the application restarts. Ensure they’re safe to re-run.
Monitor task duration Log start and end times to track how long tasks take and detect slowdowns.
Testing Schedulers
Test your runner independently of the scheduler:
func TestCleanupRunner ( t * testing . T ) {
db := setupTestDB ( t )
runner := & CleanupRunner { db : db }
ctx := context . Background ()
err := runner . Run ( ctx )
if err != nil {
t . Fatalf ( "runner failed: %v " , err )
}
// Verify cleanup worked
count := countSessions ( t , db )
if count != 0 {
t . Errorf ( "expected 0 sessions, got %d " , count )
}
}
For integration tests with actual schedules, use short intervals:
func TestSchedulerIntegration ( t * testing . T ) {
runner := & TestRunner {}
// Use a very short interval for testing
scheduler , _ := scheduler . New ( "@every 1s" , runner )
ctx , cancel := context . WithTimeout ( context . Background (), 5 * time . Second )
defer cancel ()
go scheduler . Run ( ctx )
// Wait and verify runner was called
time . Sleep ( 3 * time . Second )
if runner . callCount < 2 {
t . Errorf ( "expected at least 2 calls, got %d " , runner . callCount )
}
}