Schedule and manage macOS background jobs without writing a single plist.
launchd is powerful but painful to use directly. Summond wraps it with a CLI that handles plist generation, log management, and job lifecycle — so you can focus on what the job actually does.
brew install --cask joshgummersall/summond/summondOr install from source (requires Go 1.24+):
go install github.com/joshgummersall/summond/cmd/summond@latestDefine jobs in a TOML file and apply them:
[jobs.cleanup]
command = "/usr/local/bin/my-script"
schedule = "daily"
hour = 3
minute = 0
[jobs.cleanup.env]
MODE = "nightly"summond applyThat's it. Summond generates and loads the launchd plist, creates log files, configures newsyslog rotation, and tracks execution history. When you delete a job from the config, summond apply --prune removes it cleanly.
command, args, shell_command, working_dir, watch_paths, and env values all support $VAR/${VAR} expansion, resolved against summond's own environment when you run apply. Referencing an unset variable is an error rather than silently resolving to an empty string:
[jobs.cleanup]
command = "$HOME/bin/my-script"
working_dir = "$HOME/projects/foo"# Run a binary on a schedule
summond add agent cleanup --schedule daily --hour 3 --minute 0 -- /usr/local/bin/my-script
# Run a shell snippet
summond add agent rotate-logs --schedule daily --hour 3 --minute 30 <<'EOF'
find /tmp -type f -mtime +7 -delete
EOFlogin, boot, hourly, daily, weekly, interval, and calendar — with --hour, --minute, --weekday, and --interval-minutes flags to tune them. Omit the timing flags and summond deterministically seeds them from the job name to spread load.
Run a job when files change instead of on a schedule:
[jobs.on-config-change]
command = "/usr/local/bin/reload"
trigger = "on_change"
[jobs.on-config-change.watch]
paths = ["./config.json"]
throttle_seconds = 2 # debounce delay; defaults to 2The flat keys watch_paths and throttle_interval_seconds are also accepted for backwards compatibility.
The job receives a SUMMOND_CHANGED_PATHS environment variable — a colon-separated list of entries from watch_paths that changed since the last successful run. Note that these are the paths you listed in watch_paths, so a watched directory appears as the directory itself, not the individual file that changed within it. On the first execution (no prior baseline), all watch_paths are included.
# shell_command example
for path in ${SUMMOND_CHANGED_PATHS//:/ }; do
reload "$path"
donesummond tuiOpens a two-pane terminal UI. The left pane lists all jobs; the right pane shows details for the selected job across three tabs:
- State — schedule, command, last run status, execution counts, and file paths
- Plist — the generated launchd plist XML
- Logs — last 500 lines of stdout and stderr
Keybindings:
| Key | Action |
|---|---|
↑ / ↓ or j / k |
Navigate job list |
tab / shift+tab |
Cycle tabs forward / backward |
| scroll wheel | Scroll right pane |
X |
Run selected job immediately in the foreground |
r |
Force refresh |
q / ctrl+c |
Quit |
Shell commands and plist XML are syntax-highlighted if bat is installed, with graceful fallback to plain text.
summond list # all jobs with last-run status
summond logs cleanup # output from the last execution
summond logs cleanup -f # stream new output live
summond exec cleanup # run immediately in the foreground
summond kill cleanup # stop a running job and reconcile its state
summond state cleanup # full execution history as JSONStdout and stderr go to ~/Library/Application Support/summond/logs/ and are rotated by macOS's native newsyslog. No third-party log management needed.
Jobs run as LaunchAgents (per-user) by default. Set target = "daemon" for system-wide LaunchDaemons — summond will prompt for sudo when needed.
Jobs that exit non-zero can be automatically retried with exponential backoff:
[jobs.flaky-api-sync]
command = "/usr/local/bin/sync"
schedule = "hourly"
[jobs.flaky-api-sync.retry]
attempts = 3 # retries after the initial attempt
delay_seconds = 5 # wait before first retry (default 1)
max_delay_seconds = 60 # cap the doubling delay (0 = no cap)Or via summond add:
summond add agent flaky-api-sync --schedule hourly \
--retry-attempts 3 \
--retry-delay-seconds 5 \
--retry-max-delay-seconds 60 \
-- /usr/local/bin/syncDelay doubles between each retry (5 s → 10 s → 20 s … up to the cap). Only exit-code failures are retried; if the binary can't be launched at all, summond gives up immediately.
Each job receives a SUMMOND_ATTEMPT environment variable (0-indexed) so scripts can adapt their behavior on retries:
# shell_command example — skip expensive setup on retries
if [ "$SUMMOND_ATTEMPT" -eq 0 ]; then
do-expensive-preflight
fi
do-the-actual-workRetry annotations ([summond] retry attempt N/M after Xs) are written to the job's stderr log between attempts.
summond env set API_KEY=secret # injected into every job at runtimePer-job [jobs.name.env] values are merged on top.