timertab

module
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: MIT

README ΒΆ

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:

  1. Your editor opens the YAML config.
  2. On save, timertab validates the config against the schema and semantic rules.
  3. Missing job id fields are auto-generated and persisted back to the file.
  4. For each job, a .service and .timer unit is rendered.
  5. Stale units (from removed jobs) are stopped, disabled, and deleted.
  6. 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

Directories ΒΆ

Path Synopsis
cmd
timertab command
internal
cli
skills
timertab
Package timertabskill exposes the repository's timertab skill to the CLI.
Package timertabskill exposes the repository's timertab skill to the CLI.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL