Scheduled Tasks (Cron)
Sometimes a document needs to do things on its own — clean up old data, send a daily digest, recalculate stats. I didn't want people setting up external cron jobs or Lambda functions just to poke a document on a schedule. So I built @cron directly into the language.
The @cron Annotation
The @cron annotation defines a named block of code that runs on a schedule:
@cron job_name daily 12:00 {
// Code to execute on schedule
}
The components are:
- job_name: A unique identifier for this scheduled task
- schedule_type: One of
daily,hourly, ormonthly - schedule_value: The time or interval specification (depends on schedule type)
Daily Tasks
Daily tasks execute once per day at a specified time. Specify the time in 24-hour format as HH:MM:
procedure generateDailyReport() {}
procedure cleanupExpiredSessions() {}
procedure sendMorningReminders() {}
@cron daily_report daily 14:30 {
// Runs every day at 2:30 PM
generateDailyReport();
}
@cron midnight_cleanup daily 00:00 {
// Runs at midnight each day
cleanupExpiredSessions();
}
@cron morning_notification daily 08:00 {
// Runs at 8:00 AM
sendMorningReminders();
}
Dynamic Daily Schedules
You can use a time field instead of a literal to make the schedule configurable at runtime:
procedure generateReport() {}
message SetTimeMessage {
time new_time;
}
private time report_time;
@cron dynamic_report daily report_time {
// Runs at whatever time is stored in report_time
generateReport();
}
channel set_report_time(SetTimeMessage msg) {
report_time = msg.new_time;
}
This lets administrators (or the document itself) adjust when tasks run without redeploying code. I find this useful for letting users pick their own notification times.
Hourly Tasks
Hourly tasks execute once per hour at a specified minute (0-59):
procedure syncExternalData() {}
procedure hourlyMetricsSnapshot() {}
procedure checkPendingTasks() {}
@cron hourly_sync hourly 30 {
// Runs at 30 minutes past every hour (1:30, 2:30, 3:30, etc.)
syncExternalData();
}
@cron top_of_hour hourly 0 {
// Runs at the top of every hour (1:00, 2:00, 3:00, etc.)
hourlyMetricsSnapshot();
}
@cron quarter_past hourly 15 {
// Runs at 15 minutes past every hour
checkPendingTasks();
}
Dynamic Hourly Schedules
Use an int field for configurable hourly schedules:
procedure performSync() {}
private int sync_minute;
@cron configurable_sync hourly sync_minute {
// Runs at the minute specified by sync_minute
performSync();
}
Monthly Tasks
Monthly tasks execute once per month on a specified day (1-31):
procedure processMonthlyBilling() {}
procedure generateMidMonthReport() {}
procedure closeMonthlyAccounts() {}
@cron monthly_billing monthly 1 {
// Runs on the 1st of each month
processMonthlyBilling();
}
@cron mid_month_report monthly 15 {
// Runs on the 15th of each month
generateMidMonthReport();
}
@cron end_of_month monthly 28 {
// Runs on the 28th (safe for all months including February)
closeMonthlyAccounts();
}
If you specify a day that doesn't exist in a given month (like 31 for February), the behavior depends on the platform implementation. Stick to day 28 or earlier if you want it to run every month reliably.
Dynamic Monthly Schedules
Use an int field for configurable monthly schedules:
procedure processBilling() {}
private int billing_day;
@cron configurable_billing monthly billing_day {
// Runs on the day specified by billing_day
processBilling();
}
Multiple Cron Jobs
A document can have as many cron jobs as you need, each with a unique name:
procedure performDailyMaintenance() {}
procedure sendDailySummary() {}
procedure checkSystemHealth() {}
procedure archiveOldRecords() {}
int daily_count;
int hourly_count;
int monthly_count;
@cron morning_task daily 8:00 {
daily_count++;
performDailyMaintenance();
}
@cron afternoon_task daily 17:00 {
daily_count++;
sendDailySummary();
}
@cron hourly_check hourly 30 {
hourly_count++;
checkSystemHealth();
}
@cron monthly_archive monthly 1 {
monthly_count++;
archiveOldRecords();
}
Time Zone Considerations
Cron schedules operate in UTC. If your users are in different time zones, you need to account for that when setting schedule times. I know — time zones are awful. But UTC is the least-bad option for a shared system.
Handling Time Zones
For applications that need local time scheduling, here are a couple of approaches:
Store the offset and calculate:
procedure sendLocalizedNotification() {}
private int timezone_offset_hours; // e.g., -5 for EST
@cron daily_notification daily 13:00 {
// This runs at 1 PM UTC
// For EST users (UTC-5), this is 8 AM local time
sendLocalizedNotification();
}
Use dynamic scheduling based on user timezone:
procedure sendNotification() {}
private time notification_time;
procedure setNotificationForLocalTime(int local_hour, int timezone_offset) {
// Convert local time to UTC
// notification_time = calculated UTC time
}
@cron user_notification daily notification_time {
sendNotification();
}
Best Practices for Time Zones
- Document your assumptions: Comment whether times are UTC or local
- Use UTC for shared resources: When multiple users share a document, UTC is the safest choice
- Store timezone preferences: Keep user timezone preferences in the document state
- Consider DST: Daylight Saving Time will bite you if you forget about it
Idempotency
Cron jobs should be idempotent — running the same job multiple times should produce the same result as running it once. This matters because network issues might cause retries, document recovery might replay cron executions, and you might need to manually trigger a job for testing.
Making Jobs Idempotent
Track when the job last ran:
procedure performCleanup() {}
private datetime last_cleanup;
@cron daily_cleanup daily 02:00 {
datetime now = Time.datetime();
// Only run if we have not run today
if (last_cleanup.date() != now.date()) {
performCleanup();
last_cleanup = now;
}
}
Use upsert patterns instead of insert:
record DailyStat {
public int id;
public date stat_date;
public int count;
}
table<DailyStat> _stats;
function calculateDailyCount() -> int {
return 0;
}
@cron daily_stats daily 00:00 {
// Instead of inserting a new record each time
// Update or create based on a unique key
let today = Time.today();
list<DailyStat> existing = iterate _stats where stat_date == today;
if (existing.size() == 0) {
_stats <- {stat_date: today, count: calculateDailyCount()};
} else {
existing.count = calculateDailyCount();
}
}
Use transactions that can be safely repeated:
record Invoice {
public int id;
public bool billed;
public double amount;
}
table<Invoice> _invoices;
procedure processInvoice(Invoice inv) {}
@cron monthly_billing monthly 1 {
// Process only unbilled items
list<Invoice> unbilled = iterate _invoices where billed == false;
foreach (invoice in unbilled) {
processInvoice(invoice);
invoice.billed = true; // Mark as processed
}
}
Common Patterns
Cleanup and Data Retention
Remove old data to keep document size manageable:
record LogEntry {
public int id;
public date created_at;
public string message;
}
table<LogEntry> _logs;
@cron cleanup_old_logs daily 03:00 {
date cutoff = Time.today().offsetDay(-30);
// Delete logs older than 30 days
(iterate _logs where created_at < cutoff).delete();
}
Aggregation and Statistics
Calculate summary statistics periodically:
record Event {
public int id;
public datetime timestamp;
public double value;
}
table<Event> _events;
record HourlyStat {
public int id;
public datetime hour;
public int event_count;
public double avg_value;
}
table<HourlyStat> _hourly_stats;
function calculateAverage(list<Event> events) -> double {
return 0.0;
}
@cron hourly_aggregation hourly 0 {
// Calculate stats for the previous hour
datetime now = Time.datetime();
list<Event> events = iterate _events;
int count = events.size();
double avg = (count > 0) ? calculateAverage(events) : 0.0;
_hourly_stats <- {
hour: now,
event_count: count,
avg_value: avg
};
}
Notifications and Reminders
Send scheduled notifications:
procedure sendNotification(principal user, string message) {}
record Reminder {
public int id;
public principal user;
public string message;
public datetime remind_at;
public bool sent;
}
table<Reminder> _reminders;
@cron check_reminders hourly 0 {
datetime now = Time.datetime();
// Find due reminders that have not been sent
list<Reminder> due = iterate _reminders
where remind_at <= now && sent == false;
foreach (reminder in due) {
sendNotification(reminder.user, reminder.message);
reminder.sent = true;
}
}
Expiration and Timeout Handling
Handle time-based expirations:
record Session {
public int id;
public principal user;
public date last_activity;
public bool active;
}
table<Session> _sessions;
@cron expire_sessions hourly 15 {
date cutoff = Time.today().offsetDay(-1);
// Expire sessions inactive for more than 24 hours
(iterate _sessions where active == true && last_activity < cutoff).active = false;
}
State Machine Integration
Trigger state machine transitions on schedule:
bool auction_active;
datetime auction_end_time;
procedure determinWinner() {}
@cron auction_check hourly 0 {
// Check if auction should end
if (auction_active && Time.datetime() >= auction_end_time) {
transition #auction_closing;
}
}
#auction_closing {
// Process auction results
determinWinner();
auction_active = false;
transition #auction_complete;
}
#auction_complete {
// Auction finished
}
Cron Job Execution Context
Within a cron job, you have full access to the document state, but with some things to keep in mind:
- No @who: Cron jobs don't have a connected user, so
@whois@no_one - Full state access: You can read and modify all document fields
- Transitions allowed: You can trigger state machine transitions
- Durability: Changes made in cron jobs are persisted like any other mutation
record SystemLog {
public int id;
public datetime timestamp;
public principal actor;
public string action;
public string details;
}
table<SystemLog> _system_logs;
procedure performMaintenance() {}
@cron system_maintenance daily 04:00 {
// Log that system maintenance ran
_system_logs <- {
timestamp: Time.datetime(),
actor: @no_one, // Cron job has no user context
action: "system_maintenance",
details: "Automated maintenance completed"
};
performMaintenance();
}
The beauty of @cron is that it keeps everything inside the document. No external schedulers, no separate infrastructure, no coordination problems. The document just does its own housekeeping on its own schedule.