Skip to content

Repository files navigation

summond

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.

Install

brew install --cask joshgummersall/summond/summond

Or install from source (requires Go 1.24+):

go install github.com/joshgummersall/summond/cmd/summond@latest

How it works

Define 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 apply

That'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"

Or skip the config file entirely

# 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
EOF

Schedules

login, 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.

File watch trigger

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 2

The 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"
done

Interactive TUI

summond tui

Opens 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.

Logs, status, and execution history

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 JSON

Stdout and stderr go to ~/Library/Application Support/summond/logs/ and are rotated by macOS's native newsyslog. No third-party log management needed.

Agents and daemons

Jobs run as LaunchAgents (per-user) by default. Set target = "daemon" for system-wide LaunchDaemons — summond will prompt for sudo when needed.

Retry and backoff

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/sync

Delay 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-work

Retry annotations ([summond] retry attempt N/M after Xs) are written to the job's stderr log between attempts.

Shared environment

summond env set API_KEY=secret   # injected into every job at runtime

Per-job [jobs.name.env] values are merged on top.

About

Schedule macOS background jobs without writing plists — a friendly CLI for launchd LaunchAgents and LaunchDaemons

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages