a crontab-ish interface for systemd user timers (and services) https://code.opennomad.com/opennomad/systab/
Find a file
Matthias Johnson 2b75547726
Some checks failed
CI / shellcheck (push) Failing after 16s
a note on thhe crontab format
2026-02-17 14:02:48 -07:00
.forgejo/workflows purge github mirror badge 2026-02-16 20:36:13 -07:00
.githooks Fix test cleanup destroying user jobs, consolidate code 2026-02-15 21:55:09 -07:00
badges adding tests 2026-02-15 12:10:09 -07:00
demo adding demo gifs 2026-02-15 12:32:16 -07:00
CLAUDE.md adding tests 2026-02-15 12:10:09 -07:00
LICENSE adding license 2026-02-15 10:39:38 -07:00
README.md a note on thhe crontab format 2026-02-17 14:02:48 -07:00
systab Fix test cleanup destroying user jobs, consolidate code 2026-02-15 21:55:09 -07:00
test.sh Fix test cleanup destroying user jobs, consolidate code 2026-02-15 21:55:09 -07:00

systab

ShellCheck Tests License: AGPL-3.0

A cron/at-like interface for systemd user timers. Create, manage, and monitor scheduled jobs without writing unit files by hand.

Because you want to use systemd, but miss the ease of crontabsystab -e!

  • 🚀 create one-time or recurring jobs with one command
  • ✏️ use your $EDITOR to manage systab timers in a single line format
  • 📊 quickly see the status of your timers
  • 📋 access the logs of timers
  • 💪 enable and disable timers
Quick start demo Edit mode demo Notifications demo
Quick start Edit mode Notifications

Install

Copy the systab script somewhere on your $PATH:

cp systab ~/.local/bin/

Requires bash, systemctl, and optionally notify-send (for -i) and sendmail/msmtp (for -m).

Quick start

# Run a command every 5 minutes (with a name for easy reference)
systab -t "every 5 minutes" -n healthcheck -c "curl -s https://example.com/health"

# Run a backup script every day at 2am
systab -t "every day at 2am" -n backup -f ~/backup.sh

# Run a one-time command in 30 minutes
systab -t "in 30 minutes" -c "echo reminder"

# Check status of all jobs
systab -S

# View logs
systab -L

Time formats

systab accepts several time formats:

Format Example Type
Natural recurring every 5 minutes Recurring
Natural recurring every 2 hours Recurring
Natural recurring every 30 seconds Recurring
Natural recurring every day at 2am Recurring
Natural recurring every monday at 9am Recurring
Natural recurring every month Recurring
Relative in 5 minutes One-time
Relative tomorrow One-time
Absolute 2025-06-15 14:30 One-time
Absolute next tuesday at 9am One-time
Systemd keyword hourly, daily, weekly, monthly Recurring
Systemd OnCalendar *:0/15 (every 15 min) Recurring
Systemd OnCalendar *-*-* 02:00:00 (daily at 2am) Recurring
Systemd OnCalendar Mon *-*-* 09:00 (Mondays at 9am) Recurring

Relative and absolute formats are parsed by date -d. Systemd OnCalendar values are passed through directly.

Note: date -d does not technically like "in 5 minutes" or "at" between day and time. systab strips "in" and "at" before passing to date -d.

Usage

Creating jobs

# Command string (with optional name)
systab -t "every 5 minutes" -n ping -c "echo hello"

# Script file
systab -t "every day at 2am" -n backup -f ~/backup.sh

# From stdin
echo "ls -la /tmp" | systab -t daily

# With desktop notification (success/failure with status icon)
systab -t "in 1 hour" -c "make build" -i

# With email notification (via sendmail)
systab -t "every day at 6am" -c "df -h" -m user@example.com

# Include last 10 lines of output in notification
systab -t "every day at 6am" -c "df -h" -i -o

Managing jobs

# Edit all jobs in your $EDITOR (crontab-style)
systab -e

# Show status of all jobs
systab -S

# Show status of a specific job (by ID or name)
systab -S a1b2c3
systab -S backup

# View logs (all jobs)
systab -L

# View logs for a specific job (by ID or name)
systab -L a1b2c3
systab -L backup

# View logs (filtered)
systab -L error

# Disable/enable a job (by ID or name)
systab -D backup
systab -E backup

# Clean up completed one-time jobs
systab -C

Edit mode

systab -e opens your editor with a pipe-delimited job list:

a1b2c3:n=backup | daily | /home/user/backup.sh
d4e5f6:i | *:0/15 | curl -s https://example.com
g7h8i9:n=weekly-backup,e=user@host | weekly | ~/backup.sh
# aabbcc | hourly | echo "this job is disabled"
  • Edit the schedule or command to update a job
  • Delete a line to remove a job
  • Add a line with new as the ID to create a job: new | every 5 minutes | echo hello
  • Comment out a line (#) to disable, uncomment to enable
  • Append flags after the ID with :n=name for naming, i for desktop notification, e=addr for email, o for output (default 10 lines), o=N for custom count, comma-separated (e.g., a1b2c3:n=backup,i,o,e=user@host)

Job IDs and names

Each job gets a 6-character hex ID (e.g., a1b2c3) displayed on creation and in status output. You can also assign a human-readable name with -n at creation time. Names can be used interchangeably with hex IDs in -D, -E, -S, and -L. Names must be unique and cannot contain whitespace, pipes, or colons.

How it works

systab creates systemd .service and .timer unit file pairs in ~/.config/systemd/user/. Each managed unit is tagged with a # SYSTAB_MANAGED marker comment. One-time jobs auto-unload after firing. Job output (stdout/stderr) is captured in the systemd journal and viewable via systab -L.

Notifications use ExecStopPost so they fire after the service completes regardless of success or failure. Desktop notifications show dialog-information or dialog-error icons based on $SERVICE_RESULT. Notification flags are persisted as # SYSTAB_FLAGS= comments in service files, so they survive across edit sessions.

Options

Job Creation:
  -t <time>         Time specification (required for job creation)
  -c <command>      Command string to execute
  -f <script>       Script file to execute (reads stdin if neither -c nor -f)
  -n <name>         Give the job a human-readable name (usable in place of hex ID)
  -i                Send desktop notification on completion (success/failure)
  -m <email>        Send email notification to address (via sendmail)
  -o [lines]        Include job output in notifications (default: 10 lines)

Management (accept hex ID or name):
  -D <id|name>      Disable a job
  -E <id|name>      Enable a disabled job
  -e                Edit jobs in crontab-like format
  -L [id|name] [filter]  List job logs (optionally for a specific job and/or filtered)
  -S [id|name]      Show status of all managed jobs (or a specific job)
  -C                Clean up completed one-time jobs
  -h                Show help

License

AGPL-3.0-or-later. See LICENSE.

Contributing

The primary repository is hosted on Forgejo with a public mirror on GitHub.

Contributions (issues and pull requests) are welcome on GitHub.

After cloning, enable the pre-commit hook (runs ShellCheck + tests):

git config core.hooksPath .githooks

FAQ

Why this wrapper?

I was missing the simplicity of at and crontab commands. systemd has many features and benefits that those tools do not have, but convenience for the user to set a quick timer is not one of them.

What's the difference between -c and -f?

-f validates that the file exists and is executable at creation time, catching typos and permission issues early. With -c, errors only surface when systemd runs the job later (visible via systab -L). Under the hood, both produce the same ExecStart line.

Why did you mess with the crontab format?

Originally I meant to keep it the same, but there isn't a one-to-one mapping of times supported by cron and systemd. systemd has more options. I also want the human readble strings like "in 5 minutes". Trying to force that into the crontab format with it's space delimited format meant quotes, etc. in the end I used the | and limit the number of fields so that the commands can also be piped.