You can watch every cron job and macOS LaunchAgent in a single dashboard without migrating either by globbing their on-disk state — crontab -l, the ~/Library/LaunchAgents/*.plist files, and launchctl print exit codes — and normalizing all of it into one schema. You only read existing registrations, so nothing moves and nothing needs to be instrumented; you add a pane of glass on top of schedulers that keep running exactly as before.
If you run a personal fleet of automation on one machine — a handful of cron jobs, several macOS LaunchAgents, a few long-running Python agents, and some MCP servers — you hit a specific failure mode that has nothing to do with any single job. The schedulers don't know about each other. cron and launchd are separate worlds; the Python agents are a third; the MCP servers a fourth. There is no single place that answers "what is scheduled, did it run, and what did it produce?" So things rot quietly: a daily report generator stops and you don't notice for a week, an ingest job's output piles up unread, a calibration job silently never fires after a reboot.
The instinct is to reach for a scheduler or a monitoring SaaS. I evaluated 25 of them and they all failed for the same two reasons.
The candidates split into two camps, each with a disqualifying assumption:
| Tool category (examples) | Why it doesn't fit |
|---|---|
| launchd GUIs — Lingon X, LaunchControl | launchd only; can't see cron. No link from a job to its output files. |
| Schedulers — Cronicle, Rundeck, Airflow | Can't read your existing registrations. They want you to re-create every job inside their system — invasive migration. |
| Cron monitoring SaaS — heartbeat / dead-man-switch services | Every job must curl a ping endpoint on success. That's editing every crontab line (migration + instrumentation), and it can't see a job that never started. |
| Dashboards — Homepage, Dashy | Link collections. No run state, no exit codes, no outputs. |
| Agent observability OSS (e.g. 1.4k-star session monitors) | Built to watch one runtime's sessions — not a heterogeneous cron + launchd + agent + MCP mix. |
The pattern across all 25: they are push systems that require migration. You move your jobs into their model, or you instrument each job to report into them. In a high-churn setup where you add new programs and MCP servers constantly, that tax compounds — every new thing has to be migrated or it's invisible.
Instead of asking jobs to report in, read the state the OS already keeps. A deterministic collector unifies four read-only sources into one schema:
# cron
crontab -l # schedule + command per line
# launchd
~/Library/LaunchAgents/*.plist # parse with plutil -convert json
launchctl print gui/$UID/<label> # load state, last exit code
# resident services
ps # is the long-running process alive?
Normalize every entry into a single record — call it a ScheduledJob: a label, the source (cron or launchd), the schedule, the command, and the log path. Now cron lines and launchd plists live in one list, sorted and searchable, with no change to either scheduler.
Two things are still missing from a bare job list, and they are the difference between a dashboard you ignore and one you use.
A job that "ran successfully" is not interesting; the file it produced is. To connect the two without instrumenting anything, lean on a convention you can enforce yourself: every job writes its artifacts under <project>/report/. Store one glob per job in a small catalog, and the dashboard lists each job's newest matching files by modification time (plus anything at the plist StandardOutPath). Clicking opens the file. This output shelf is the payload — you see what each job made, not just that it exited 0.
The worst failure in scheduled work is silent: the job that should have run and didn't. Push monitoring is blind to it — a missing heartbeat looks the same as a paused job, and a job that never started sends nothing at all. A read-only aggregator can derive the signal directly: compare the schedule to the freshness of the log. If a daily job's StandardOutPath was last written outside its expected window, raise an "expected run, no output" flag on the card. No instrumentation — just the gap between when it should have run and when its log last changed.
A dashboard with a run button is a foot-gun unless you bound it hard. Four invariants:
launchctl kickstart <label> or a script whose absolute path is registered in the catalog. There is no input box that accepts an arbitrary command.EnvironmentVariables key names, never values.The whole thing is a small FastAPI + htmx app — a few hundred lines, two dependencies. The leverage isn't the framework; it's the decision to read existing state instead of demanding that state report to you.
If you control the convention (here, a per-project report/ directory) and the OS already records the state you need (schedules, exit codes, log mtimes), you don't need a heavier system — you need a read-only view that joins what's already on disk. Migration and instrumentation are costs you pay forever; a glob is a cost you pay once. For a single-operator machine that keeps growing, "aggregate without migrating" scales where "move everything into one scheduler" quietly does not.
Q. How do I see all my cron jobs and launchd agents in one place without moving them?
Build a read-only aggregator that globs their on-disk state — parse crontab -l, parse ~/Library/LaunchAgents/*.plist, read launchctl print for exit codes — and normalize into one schema. The jobs keep running unchanged; you add a pane of glass with nothing to migrate.
Q. Why do cron monitoring tools require migration or instrumentation?
They are heartbeat / dead-man-switch designs: each job must ping their endpoint on success, so you edit every crontab line to add the ping. That can't see a job that never started, and can't link a job to its output files.
Q. How do I link a scheduled job to the output files it produces?
Adopt a per-project output convention (e.g. <project>/report/), store a glob per job in a catalog, and list each job's newest matching files by mtime. Clicking opens the artifact.
Q. How do I detect a job that should have run but didn't?
Compare the schedule to the log's modification time. If a daily job's log was last written outside the expected window, flag it — the silent failure heartbeat tools miss.