Light Dark

Schedules

The Hot Scheduler runs functions on a schedule using cron expressions or natural language.

Scheduled Runs

Schedule functions to run at specific times using the schedule metadata:

::myapp::jobs ns

// Run every hour
hourly-cleanup
meta {schedule: "0 * * * *"}
fn () {
  cleanup-expired-sessions()
  prune-old-logs()
}

// Run daily at midnight UTC
daily-report
meta {schedule: "0 0 * * *"}
fn () {
  generate-daily-report()
  send-to-slack()
}

// Run every 5 minutes
health-check
meta {schedule: "*/5 * * * *"}
fn () {
  check-external-services()
}

Cron Expression Format

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *

Common patterns:

PatternDescription
* * * * *Every minute
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 0 * * *Daily at midnight
0 0 * * 0Weekly on Sunday
0 0 1 * *Monthly on the 1st

Natural Language Schedules

You can also use plain English to define schedules:

::myapp::jobs ns

// Natural language schedules
daily-digest
meta {schedule: "every day at 9:00 am"}
fn () {
  send-daily-digest()
}

weekly-review
meta {schedule: "on Sunday at 12:00"}
fn () {
  generate-weekly-review()
}

payroll
meta {schedule: "run at midnight on the 1st and 15th of the month"}
fn () {
  process-payroll()
}

Supported English patterns:

English PhraseEquivalent Cron
every minute* * * * *
every 15 seconds*/15 * * * * *
every day at 4:00 pm0 16 * * *
at 10:00 am0 10 * * *
run at midnight on the 1st and 15th of the month0 0 1,15 * *
on Sunday at 12:000 12 * * SUN
7pm every Thursday0 19 * * THU
midnight on Tuesdays0 0 * * TUE

Natural language schedules are converted to cron expressions at startup. Both formats are fully supported and can be mixed within the same project.

Dynamic Schedules

In addition to metadata-driven schedules (defined at build time), you can create schedules dynamically at runtime using events. This is useful for:

  • One-time scheduled calls - Execute a function once at a specific time
  • User-triggered scheduling - Let users schedule tasks for later
  • Dynamic recurring jobs - Create cron schedules based on runtime conditions

Creating Schedules

Use the hot:schedule:new event to create a schedule:

// Schedule for a specific datetime
send("hot:schedule:new", {
  fn: "::myapp::orders/process-order",
  args: [{order_id: "12345"}],
  schedule: "2024-01-15T10:30:00Z"
})

// Schedule for 10 minutes from now
send("hot:schedule:new", {
  fn: "::myapp::reminders/send-reminder",
  args: [{user_id: user.id, message: "Time to check in!"}],
  schedule: "in 10 minutes"
})

// Schedule with natural language duration
send("hot:schedule:new", {
  fn: "::myapp::notifications/send-followup",
  args: [{email: customer.email}],
  schedule: "2 hours from now"
})

// Create a recurring schedule dynamically
send("hot:schedule:new", {
  fn: "::myapp::reports/generate",
  args: [{report_type: "daily"}],
  schedule: "every day at 9am"
})

Schedule Formats

The schedule field supports multiple formats:

FormatExampleDescription
ISO 8601 datetime"2024-01-15T10:30:00Z"Execute at exact time
Duration"10 minutes", "2h", "1 day 3 hours"Execute after duration
Natural language"in 10 minutes", "2 hours from now"Human-friendly durations
Cron expression"0 30 9 * * MON"Recurring schedule
English cron"every day at 9am", "every Monday at 2 PM"Natural language recurring

Cancelling Schedules

Cancel pending schedules using the hot:schedule:cancel event:

// Cancel by schedule ID (returned from hot:schedule:new)
send("hot:schedule:cancel", {
  schedule-id: "01916d8a-9c12-7f00-8000-123456789abc"
})

// Cancel all schedules for a specific function
send("hot:schedule:cancel", {
  fn: "::myapp::jobs/heavy-process"
})

Cancelling schedules is useful for:

  • Removing pending one-time schedules that are no longer needed
  • Disabling recurring schedules without redeploying
  • Cleaning up after a user cancels an action

Dynamic vs Metadata Schedules

FeatureMetadata ScheduleDynamic Schedule
Defined inHot code (meta {schedule: ...})Runtime via hot:schedule:new
LifecycleTied to build/deploymentCan be created/cancelled anytime
One-time supportNoYes
Visible in UIAlwaysWhen active
Use caseRegular jobs (daily reports, cleanup)User-triggered, conditional scheduling

Example: Scheduled Reminders

::myapp::reminders ns

// Schedule a reminder for later
schedule-reminder fn (user-id: Str, message: Str, delay: Str): Str {
  // Create a one-time schedule
  schedule-id send("hot:schedule:new", {
    fn: "::myapp::reminders/send-reminder",
    args: [{user-id: user-id, message: message}],
    schedule: delay
  })

  // Return the schedule ID so it can be cancelled if needed
  schedule-id
}

// Cancel a pending reminder
cancel-reminder fn (schedule-id: Str): Bool {
  send("hot:schedule:cancel", {schedule-id: schedule-id})
}

// The actual reminder function (called by scheduler)
send-reminder fn (data: Map) {
  user get-user(data.user-id)
  send-push-notification(user.device-token, data.message)
}

Usage:

// Schedule a reminder for 30 minutes from now
reminder-id schedule-reminder("user-123", "Time for your meeting!", "in 30 minutes")

// Later, cancel if user dismisses early
cancel-reminder(reminder-id)

Retries

Scheduled functions can automatically retry on failure using the retry metadata:

::myapp::jobs ns

// Simple: Just specify attempts (uses default 1 second delay)
import-data meta {
  schedule: "0 2 * * *",
  retry: 5
}
fn () {
  import-from-sftp()
}

// Full config: Specify attempts and custom delay
sync-external-api meta {
  schedule: "every 15 minutes",
  retry: {
    attempts: 3,
    delay: 5000
  }
}
fn () {
  fetch-and-sync-data()
}

Retry Metadata

The retry metadata supports two formats:

Simple format - just the number of attempts:

retry: 3

Full format - object with attempts and delay:

retry: {
  attempts: 3,
  delay: 5000
}
FieldDescriptionDefault
attempts (or simple number)Maximum retry attempts (1-10)0 (no retries)
delayDelay between retries in milliseconds (max 1 hour)1000 (1 second)

How Retries Work

  1. When a scheduled function fails (returns a Failure), the system checks for retry configuration
  2. If retries remain, the run is marked as pending_retry with a scheduled retry time
  3. The scheduler picks up pending retries and creates a new run for each retry attempt
  4. Each retry is a separate run linked to the original via origin_run_id
  5. Retries continue until the function succeeds or max retries are exhausted

Retry Limits

The platform enforces maximum limits on retry configuration:

SettingEnvironment VariableDefault
Max retry attemptsHOT_RETRY_MAX_ATTEMPTS10
Max retry delayHOT_RETRY_MAX_DELAY_MS3600000 (1 hour)
Default retry delayHOT_RETRY_DEFAULT_DELAY_MS1000 (1 second)

Values exceeding these limits are clamped to the maximum.

Viewing Retries in Hot App

Retry runs are clearly marked in the Hot App:

  • A retry badge shows the attempt number (e.g., "↻1", "↻2")
  • The run detail page shows the original run that triggered the retry
  • The stream graph displays retry runs with their attempt number

How It Works

When a scheduled function's time arrives, the scheduler sends a hot:schedule event with the function details. A worker picks up the event and executes the function.

Scheduled runs are tracked and visible in the Hot App alongside event-triggered and API-triggered runs.