timertab
Cron jobs done right — one YAML file, real systemd timers under the hood.
If you've ever wished crontab -e gave you systemd timers instead of 1970s cron, this is that tool. You write a simple YAML config, timertab turns it into proper .service and .timer units. No daemon, no lock-in — just plain systemd.
Why not just use crontab?
Systemd timers are better than cron in almost every way: structured logging through journald, resource controls, dependency ordering, per-user isolation. But managing them by hand means writing two unit files per job and juggling systemctl enable, daemon-reload, and cleanup yourself.
timertab takes care of all of that. You get the simplicity of crontab with the power of systemd timers.
Why not just write unit files?
You absolutely can — and timertab won't stop you. In fact, that's the point: the generated units are standard systemd, human-readable, and work without timertab installed. If you ever want to stop using this tool, run timertab eject <id> and your timer keeps running on its own.
Features
- One YAML file — all your scheduled jobs in one place, version-control friendly.
- Success and failure hooks — run a command when a job succeeds or fails (send a notification, dump logs, trigger another script).
- Cron syntax you already know —
@hourly, @daily, or standard 5-field cron expressions.
- Multiline scripts — these just work, run multiple commands without
&& abuse.
- Multiple schedules per job —
when accepts a list, so one job can fire at different times.
- Zero lock-in — eject any job and it keeps running as a standalone systemd timer.
- Per-user isolation — units are scoped to your UID;
timertab never touches units it didn't create.
- Raw systemd overrides when needed — set extra
[Service] / [Timer] directives per job.
- JSON Schema — get autocomplete and validation in editors that support it.
- Safe reconcile — if your config is invalid, nothing gets written or pruned. No partial state.
Quick start
Install
go install github.com/ginden/timertab/cmd/timertab@latest
Or from a local clone:
make install
Create your first job
timertab -e
This opens your $EDITOR with the config file. Add a job:
$schema: "https://raw.githubusercontent.com/ginden/timertab/v1.0.0/schema/v1.json"
version: 1
instance_id: work
jobs:
- name: clean temp files
when: "@daily"
run: "find /tmp -user $USER -mtime +7 -delete"
Save and close — timertab validates the config, generates the systemd units, and starts the timer. That's it.
A more complete example
$schema: "https://raw.githubusercontent.com/ginden/timertab/v1.0.0/schema/v1.json"
version: 1
jobs:
- name: NPM cache verify
when: "@hourly"
run: "npm --global cache verify"
env:
NPM_CONFIG_PREFIX: "/home/user/.npm-global"
on_success:
command: "echo ok"
on_failure:
command: 'journalctl -u "$TIMERTAB_UNIT" -n 100 --no-pager'
- name: backup documents
when:
- "0 9 * * *"
- "0 18 * * *"
run: "rsync -a ~/Documents /mnt/backup/"
cwd: "/home/user"
systemd:
service:
Restart: "on-failure"
RestartSec: "30s"
timer:
AccuracySec: "1m"
on_failure:
command: 'notify-send "Backup failed"'
Usage
| Command |
What it does |
timertab edit (or timertab -e) |
Edit config, validate, and apply (generate and start timers) |
timertab edit --no-apply (or timertab -e --no-apply) |
Edit and validate only, don't touch systemd |
timertab edit --no-commit |
Apply without creating/updating the git history entry for that edit run |
timertab list / timertab print-config (or timertab -l) |
Print current config |
timertab status / timertab status --json |
Show last run, next trigger, and result for each job |
timertab status <id> |
Show detailed runtime state, generated unit definitions, file locations, and diagnostic commands |
timertab eject <id> |
Stop managing a job — its units stay and keep running |
timertab enable <id> / timertab disable <id> |
Toggle one job on/off without removing it |
timertab logs <id> |
Tail/query journald logs for one job |
timertab diff |
Preview create/modify/delete reconcile operations |
timertab import |
Convert crontab entries into timertab YAML |
timertab validate --config <path> |
Validate a config file without applying |
timertab print-path (or timertab --print-path) |
Show where the config file lives |
Config file location:
--config <path> if provided
${TIMERTAB_CONFIG_DIR}/timertab.yaml if TIMERTAB_CONFIG_DIR is set
- otherwise
${XDG_CONFIG_HOME:-$HOME/.config}/timertab/timertab.yaml
Shell Completions
Generate completions with timertab completion <shell>.
bash
timertab completion bash > ~/.local/share/bash-completion/completions/timertab
zsh
timertab completion zsh > ~/.zfunc/_timertab
Then make sure ~/.zfunc is on your fpath.
fish
timertab completion fish > ~/.config/fish/completions/timertab.fish
Requirements
- Linux with systemd ≥ 247
- A running user session (
systemctl --user must work)
If you need timers to fire while you're logged out:
loginctl enable-linger "$USER"
timertab will automatically detect this and print instructions if it's not set up.
How it works
When you run timertab edit (or timertab -e), here's what happens:
- Your editor opens the YAML config.
- On save,
timertab validates the config against the schema and semantic rules.
- Missing job
id fields are auto-generated and persisted back to the file.
- For each job, a
.service and .timer unit is rendered.
- Stale units (from removed jobs) are stopped, disabled, and deleted.
- New/changed units are written,
daemon-reload is called, and timers are started.
If validation fails at step 2, nothing else happens — no partial writes, no orphaned units.
Successful timertab edit apply runs also auto-commit the config file by default. If the config directory is not already inside a git work tree, timertab initializes one first, then stages and commits the config change. Disable that once with timertab edit --no-commit, or persistently in config:
git:
auto_commit: false
Spec and schema
License
MIT © Michał Wadas