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.
See what your crontab looks like as systemd timers
No install needed. Pipe your crontab in, get rendered systemd units out:
crontab -l | docker run --network none --rm -i -v "$PWD/output:/output" ghcr.io/ginden/timertab-import
This produces a full review bundle β timertab.yaml, rendered .service and .timer files, and a REPORT.md with migration notes β all without touching your system. Runs sandboxed with --network none.
Why timertab?
π True zero lock-in
Generated units are standard, human-readable systemd files. They work without timertab installed β your timers keep running whether timertab is present or not. When you outgrow YAML and want to manage units directly:
timertab eject <id>
This removes ownership markers and leaves a standalone systemd timer. No proprietary format, no runtime dependency.
πͺ Success and failure hooks
Run a command when a job succeeds or fails β send a notification, dump logs, trigger another script. Hooks are first-class, not an afterthought:
on_success:
command: "echo ok"
on_failure:
command: 'notify-send "Backup failed"'
Hooks receive rich context: TIMERTAB_JOB_ID, TIMERTAB_UNIT, SERVICE_RESULT, EXIT_CODE, EXIT_STATUS.
π‘οΈ Atomic reconcile
If your config is invalid, nothing gets written or pruned. No partial state, no orphaned units. Ever.
π Multiple schedules per job
One job can fire at different times β no need to duplicate entries:
when:
- "0 9 * * *"
- "0 18 * * *"
π¦ Multiple instances per user
Run separate job namespaces (work, personal, project-specific) on the same machine. Each instance manages its own units and never touches the others:
instance_id: work
βοΈ Raw systemd when you need it
Sensible defaults for everything, full systemd control when you want it:
systemd:
service:
Restart: "on-failure"
RestartSec: "30s"
timer:
AccuracySec: "1m"
Directive names must be plain systemd keys like Restart or AccuracySec, and values
must be single-line strings. Raw systemd: directive values are passed through unchanged,
including systemd % specifiers.
π Git auto-commit
Every successful config change (edit, enable, disable, eject, import) is automatically committed to a local git repo. Full audit trail of every change, with no extra effort. Disable with --no-commit or in config.
All features
- One YAML file β all your scheduled jobs in one place, version-control friendly.
- Cron syntax you already know β
@hourly, @daily, or standard 5-field cron expressions.
- Shell shorthand or explicit argv β use a string for
/bin/sh -lc, or a YAML list for direct execution.
- Multiline scripts β these just work in string mode, run multiple commands without
&& abuse.
- Per-user isolation β units are scoped to your UID;
timertab never touches units it didn't create.
- JSON Schema β get autocomplete and validation in editors that support it.
- Shell completions β bash, zsh, and fish.
Quick start
Install
go install github.com/ginden/timertab/cmd/timertab@latest
Or from a local clone:
make install
Tagged GitHub releases publish standalone static Linux binaries, tar.gz,
.pkg.tar.zst, .deb, and .rpm assets for both amd64 and arm64.
If you want a native package instead of go install, download one from the latest release:
# Debian/Ubuntu
sudo dpkg -i timertab_*_amd64.deb
# or
sudo dpkg -i timertab_*_arm64.deb
# Fedora/RHEL/openSUSE
sudo rpm -i timertab-*.x86_64.rpm
# or
sudo rpm -i timertab-*.aarch64.rpm
# Arch Linux and derivatives
sudo pacman -U timertab-*.pkg.tar.zst
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.1.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.1.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 * * *"
tz: "America/New_York"
run:
- /usr/bin/rsync
- -a
- /home/user/Documents/
- /mnt/backup/
cwd: "/home/user"
systemd:
service:
Restart: "on-failure"
RestartSec: "30s"
timer:
AccuracySec: "1m"
on_failure:
command: 'notify-send "Backup failed"'
String run values are shorthand for ["/bin/sh", "-lc", "..."]. Use the list form when you want exact argv execution without an extra shell.
Set tz: "Area/Location" when a job should follow a specific IANA time zone instead of the machine's local time.
In generated timertab-owned directives, literal % characters in commands, environment values, and cwd are escaped for systemd so values such as date +%F run as written.
Raw systemd: directive values are not escaped.
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 apply |
Reconcile systemd units to match the config without opening an editor |
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 trigger <id> |
Run one job immediately by starting its generated service unit |
timertab add --when <schedule> -- <command> |
Add a job without opening an editor |
timertab rm <id> |
Remove a job, then apply pruning of its managed units |
timertab eject <id> |
Stop managing a job β its units stay and keep running |
timertab adopt <id> |
Resume managing previously ejected unit files |
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 doctor |
Show active, orphaned, ejected, and other-instance timertab unit files |
timertab diff |
Preview create/modify/delete reconcile operations |
timertab import |
Convert crontab entries into timertab YAML |
timertab render |
Convert crontab input into a review bundle with timertab.yaml, rendered units, and REPORT.md |
timertab validate / timertab validate --config <path> |
Validate a config file without applying |
timertab print-path (or timertab --print-path) |
Show where the config file lives |
timertab --show-ai-skill |
Print the bundled timertab skill for users and AI agents |
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
The reusable timertab skill is checked into the repository at
skills/timertab/SKILL.md. Print the same skill from
an installed binary with timertab --show-ai-skill.
Import or Review an Existing Crontab
To convert your current crontab into timertab YAML without applying anything:
timertab import --stdout > timertab.yaml
To generate a review bundle with rendered systemd units:
crontab -l | timertab render --stdin --output output
render writes:
output/timertab.yaml
- one
.service and one .timer file per imported job
output/REPORT.md with imported jobs, warnings, and cron-vs-systemd caveats
Unlike edit, render never touches live systemd unit directories, never calls systemctl, and does not require systemd to be installed.
If you want the same review flow without installing timertab locally, use the published container image:
crontab -l | docker run --network none --rm -i -v "$PWD/output:/output" ghcr.io/ginden/timertab-import
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
- For non-root use: a running user session (
systemctl --user must work)
- For root use: access to the system manager (
systemctl without --user)
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 when unit files changed, and timers are enabled/started or disabled/stopped only when needed to match the config. @reboot-only timers are enabled but not started during apply, so they do not fire immediately.
If validation fails at step 2, nothing else happens β no partial writes, no orphaned units.
Successful config-changing runs (edit, enable, disable, eject, import) 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 --no-commit, or persistently in config:
git:
auto_commit: false
Spec and schema
License
MIT Β© MichaΕ Wadas