cron-burgundy is a macOS cron manager focused on simple local automation and missed-job recovery.

Full documentation, synced from the source README:

A classy macOS cron manager that never* misses a job. Uses native launchd for scheduling and catches up on anything missed while your Mac was asleep.

"99% of the time, it runs every time." It won't run if your computer is off

Features

  • Human-readable schedules - 'every 5 minutes', 'daily at 9am', 'on weekdays at 8:30am'
  • Native launchd integration - no daemon, each job gets its own plist
  • Missed job recovery - catches up on jobs missed during sleep
  • Per-job logging - separate log files for each job
  • Pause/unpause - temporarily disable jobs without removing them
  • Built-in macOS utils - notifications, text-to-speech, system sounds
  • Lock files - prevents concurrent runs of the same job
  • Interactive CLI - autocomplete job selection, multi-select for batch operations

How It Works

  1. Each job gets its own launchd plist with its schedule
  2. sleepwatcher detects wake from sleep and runs missed jobs
  3. No daemon, no background process - jobs fire on their own schedules

If your Mac was asleep when a job should have run, it fires on wake.

Quick Start

# Install globally
npm install -g cron-burgundy

# Create a jobs file
cat > jobs.js << 'EOF'
export const jobs = [
  {
    id: 'hello',
    schedule: 'every 5 minutes',
    run: ({ utils }) => utils.notify('Hello', 'Job ran!')
  }
]
EOF

# Register and sync to launchd
cronb sync jobs.js

# Done! Jobs run automatically on schedule

Wake Detection Setup

To catch up on missed jobs when your Mac wakes from sleep:

# 1. Install sleepwatcher
brew install sleepwatcher

# 2. Create ~/.wakeup script
cat > ~/.wakeup << 'EOF'
#!/bin/bash
cron-burgundy check-missed
EOF
chmod +x ~/.wakeup

# 3. Start sleepwatcher service
brew services start sleepwatcher

Note: sleepwatcher may request "Input Monitoring" permission - this is NOT required for ~/.wakeup to work. You can safely deny it.

Focus Mode

If using utils.notify() and macOS Focus Mode is enabled, add Script Editor to allowed apps:

System Settings → Focus → [Your Focus] → Allowed Apps → Add → Script Editor

(playSound() and speak() work regardless of Focus Mode)

CLI Commands

Available as cron-burgundy or cronb:

Usage: cronb [options] [command]

Commands:
  list                   List all registered jobs with status
  run [options] [jobId]  Run a job manually (autocomplete if no arg)
  logs                   View, list, or clear logs
  pause [name]           Pause a job or all jobs (interactive if no arg)
  unpause [name]         Unpause a job or all jobs (interactive if no arg)
  sync [path]            Register and sync a job file, or sync all registered files
  clear [target]         Unregister job files and remove from launchd (interactive if no arg)
  status                 Check installed launchd plists
  check-missed           Check and run any missed jobs (called on wake)

Options:
  -V, --version          output the version number
  -h, --help             display help for command

Logs subcommands

cronb logs                      # View runner log
cronb logs view <jobId>         # View specific job log
cronb logs view -t              # Tail runner log (follow)
cronb logs view <jobId> -t      # Tail job log
cronb logs list                 # List all log file paths
cronb logs clear                # Clear runner log
cronb logs clear <jobId>        # Clear specific job log
cronb logs clear all            # Clear runner + all job logs

Defining Jobs

Edit jobs.js in project root:

export const jobs = [
  {
    id: 'my-job',
    schedule: 'every 5 minutes',  // Human-readable or cron syntax
    enabled: true,                 // Optional, default: true
    run: async ({ logger, utils, lastRun }) => {
      logger.log('Starting...')
      // Your job logic here
      utils.notify('Job done', 'Completed successfully')
      logger.log('Done!')
    }
  }
]

Available Utils

Jobs receive a utils object with macOS helpers:

utils.notify(title, message, { sound: true })  // macOS notification
utils.speak('Hello world')                      // Text-to-speech
utils.playSound('Ping')                         // System sound (Ping, Pop, Glass, Frog, etc.)

Job Context

The run function receives:

  • logger - Job-specific logger (logger.log(), logger.error())
  • utils - macOS utilities (notify, speak, playSound)
  • lastRun - Date of last successful run (or null)

Schedule Syntax

// Basic intervals
schedule: 'every minute'
schedule: 'every hour'
schedule: 'every day'
schedule: 'hourly'
schedule: 'daily'
schedule: 'weekly'
schedule: 'monthly'
schedule: 'yearly'

// Custom intervals
schedule: 'every 5 minutes'
schedule: 'every 2 hours'
schedule: 'every 3 days'
schedule: 'every 2 weeks'
schedule: 'every 6 months'

// Shorthand intervals (without "every")
schedule: '5 minutes'
schedule: '2 hours'
schedule: '1 week'

// Specific times
schedule: 'at 9:30'
schedule: 'at 2:00 pm'
schedule: 'at 14:15'
schedule: 'noon'
schedule: 'midnight'
schedule: 'morning'       // 9am
schedule: 'evening'       // 6pm

// Days of week
schedule: 'monday'
schedule: 'friday'
schedule: 'weekdays'
schedule: 'weekends'
schedule: 'on monday at 9:00'
schedule: 'on friday at 5:30 pm'
schedule: 'on monday,wednesday,friday at 8:00 am'
schedule: 'on tuesday,thursday at 2:30 pm'
schedule: 'on weekdays at 8:30 am'
schedule: 'on weekends at 10:00 am'

// Day of month
schedule: 'on 1st of month at 9:00'
schedule: 'on 15th of month at 12:00'
schedule: 'on 31st of month at 2:00 pm'
schedule: 'first day of month'
schedule: 'middle of month'

// Business patterns
schedule: 'business hours'   // 9am-5pm weekdays

// Special
schedule: 'never'            // Feb 30th (never runs)
schedule: 'reboot'           // @reboot
schedule: 'startup'          // @reboot

Standard Cron

schedule: '0 9 * * *'      // 9am daily
schedule: '*/5 * * * *'    // Every 5 minutes
schedule: '0 0 * * 0'      // Midnight on Sundays
schedule: '30 14 1 * *'    // 2:30pm on 1st of month
schedule: '0 9-17 * * 1-5' // Every hour 9am-5pm weekdays
schedule: '15 2,14 * * *'  // 2:15am and 2:15pm daily

Interval (milliseconds)

The minimum interval is 10 seconds, per macOS launchd requirements.

interval: 60 * 1000           // Every minute
interval: 60 * 60 * 1000      // Every hour
interval: 24 * 60 * 60 * 1000 // Every day

Disabling Jobs

Set enabled: false to disable a job, then run sync:

{
  id: 'my-job',
  enabled: false,  // Job won't run
  schedule: 'daily',
  run: async () => { ... }
}
cronb sync  # Removes disabled job from launchd

Files

PathDescription
~/.cron-burgundy/state.jsonLast run times for each job
~/.cron-burgundy/runner.logMain execution log
~/.cron-burgundy/jobs/*.logPer-job logs
~/.cron-burgundy/locks/Job lock files (prevent concurrent runs)
~/Library/LaunchAgents/com.cron-burgundy.job.*.plistPer-job launchd configs
~/.wakeupsleepwatcher script for wake detection

Architecture

┌─────────────────┐     ┌─────────────────┐
│    launchd      │     │  sleepwatcher   │
│ (per-job plist) │     │  (~/.wakeup)    │
└────────┬────────┘     └────────┬────────┘
         │                       │
         │ fires on schedule     │ fires on wake
         │                       │
         ▼                       ▼
┌─────────────────────────────────────────┐
│           cron-burgundy                 │
│  - Acquires job lock                    │
│  - Runs job function                    │
│  - Updates state.json                   │
│  - Releases lock                        │
└─────────────────────────────────────────┘

DarkWake Behavior

Jobs can run even when your Mac appears asleep. macOS uses DarkWake - brief wake periods where background tasks run while the display stays off.

What triggers DarkWake

  • Do Not Disturb schedule - DND end time wakes the system
  • Power Nap - periodic maintenance wakes (if enabled)
  • Scheduled alarms - Calendar events, reminders
  • Push notifications - iCloud, Messages, etc.
  • Network activity - Wake on LAN, Find My Mac

How it affects your jobs

If a StartCalendarInterval job is scheduled during a DarkWake, launchd runs it. This means:

  • Jobs may run at their exact scheduled time even while "asleep"
  • Audio (utils.speak(), utils.playSound()) will play
  • Logs show the actual scheduled time, not wake time

Checking wake events

pmset -g log | grep -E "(Wake|Sleep)" | tail -20

Look for DarkWake entries to see when your Mac briefly woke.