sdtab
Manage systemd user timers and services like crontab.
# Add a timer (cron syntax)
# Add a long-running service
# List all managed units
NAME TYPE SCHEDULE COMMAND STATUS
report timer 0 9 * * * uv run ./report.py Tue 2026-03-03 09:00:00 JST
web service @service node server.js ● active
Web Server
Features
- cron syntax — familiar
* * * * *schedule format, automatically converted to systemd OnCalendar - Long-running services — use
@serviceto create always-on daemons with restart policies - Resource limits — set memory, CPU, and I/O constraints per unit
- Export/Import —
sdtab exportdumps config to TOML,sdtab applyrestores it on another machine - Failure notifications — Slack webhook alerts when a unit fails (
OnFailure=mechanism) - Colored status —
● active(green),● failed(red),○ inactive(yellow) at a glance; descriptions shown as gray subtitles; auto-disabled when piped - Zero dependencies beyond systemd — no database, no daemon, just unit files
Install
# Pre-built binary (no Rust required)
&&
# Or via Cargo
# Or build from source
Requirements
- Linux with systemd 244+ (user session only — system-wide units are not supported)
~/.local/binin your$PATH
Some options require newer systemd versions:
| Option | Requires | Notes |
|---|---|---|
--memory-max |
systemd 244+ and cgroups v2 | MemoryMax= directive |
--cpu-quota |
systemd 244+ and cgroups v2 | CPUQuota= directive |
--io-weight |
systemd 247+ and cgroups v2 | IOWeight= directive |
--log-level-max |
systemd 239+ | LogLevelMax= directive |
cgroups v2 is a kernel feature, not just a systemd version. Check with:
test -f /sys/fs/cgroup/cgroup.controllers && echo v2 || echo v1
Amazon Linux 2 (systemd 219) is not supported — it lacks cgroups v2 and several required directives. Use Amazon Linux 2023 (systemd 252) or newer.
Note: sdtab manages user-level units only (
systemctl --user). It cannot create or manage system-wide services that require root privileges. Ifloginctl enable-lingerfails, ask your system administrator to enable it for your user.
Quick Start
# Initialize sdtab (enables linger, creates config directory)
# Optional: set up Slack failure notifications
# Add a daily task at 9:00 AM
# Add a service that runs continuously
# Check status
# View logs
# Export config to file
# Apply config from file (on another machine)
Commands
| Command | Description |
|---|---|
sdtab init [--slack-webhook URL] [--slack-mention USER_ID] |
Enable linger, create directories, set up notifications |
sdtab add "<schedule>" "<command>" [--dry-run] |
Add a timer |
sdtab add "@service" "<command>" [--dry-run] |
Add a long-running service |
sdtab list [--json] [--sort time|name] |
List all managed timers and services (default: sorted by next run time) |
sdtab status <name> |
Show detailed status with next 5 run times |
sdtab edit <name> |
Edit unit file with $EDITOR (see caveat below) |
sdtab logs <name> [-f] [-n N] |
View logs (journalctl) |
sdtab restart <name> |
Restart a service |
sdtab enable <name> |
Enable a timer or service |
sdtab disable <name> |
Disable (keep files) |
sdtab remove <name> |
Stop, disable, and remove unit files |
sdtab export [-o <file>] |
Export config as TOML |
sdtab apply <file> [--prune] [--dry-run] |
Apply config from TOML |
sdtab removestops and disables the unit before deleting files.sdtab apply --pruneonly removes units with thesdtab-prefix — manually created systemd units are never touched.
sdtab editcaveat: sdtab stores metadata (original cron expression, command, etc.) as comments at the top of the.servicefile (# sdtab:cron=,# sdtab:command=). If you delete these comments during editing,sdtab listandsdtab exportwill show?for the missing fields. The unit itself will still work — only sdtab's bookkeeping is affected.
sdtab applyupdates changed units in-place (overwrite → daemon-reload) without stopping them first. Only units that actually need a restart are restarted: for services, description-only changes skip restart; for timers, service-side changes (command, env, etc.) take effect on the next trigger without restarting the timer.
sdtab initalso installs a Claude Code skill file to~/.claude/commands/sdtab.md, enabling/sdtabcommands in any project.
Schedule Syntax
Standard cron expressions and convenient shortcuts:
| Expression | Meaning |
|---|---|
*/5 * * * * |
Every 5 minutes |
0 9 * * * |
Daily at 9:00 |
0 9 * * Mon-Fri |
Weekdays at 9:00 |
@daily |
Once a day (midnight) |
@daily/9 |
Daily at 9:00 |
@daily/9:30 |
Daily at 9:30 |
@hourly |
Once an hour |
@reboot |
On system boot |
@mon/13 |
Every Monday at 13:00 |
@tue/18 |
Every Tuesday at 18:00 |
@weekly/Mon,Wed |
Every Monday and Wednesday |
@1st/8 |
1st of every month at 8:00 |
@20th/8 |
20th of every month at 8:00 |
@26th/11:30 |
26th of every month at 11:30 |
@service |
Long-running service (not a timer) |
Weekdays use English abbreviations (@mon, @tue, ..., @sun). Dates use English ordinals (@1st, @2nd, @20th, @26th). The / separator always means "at this time".
Add Options
| Option | Description |
|---|---|
--name <name> |
Unit name (auto-generated from command if omitted) |
--workdir <path> |
Working directory (defaults to current) |
--description <text> |
Description |
--env-file <path> |
Environment file |
--restart <policy> |
always / on-failure / no (services only, default: always) |
--memory-max <size> |
Memory limit (e.g. 512M, 1G) |
--cpu-quota <percent> |
CPU limit (e.g. 50%, 200%) |
--io-weight <N> |
I/O priority: 1-10000 (default: 100) |
--timeout-stop <duration> |
Stop timeout (e.g. 30s) |
--exec-start-pre <cmd> |
Command to run before ExecStart |
--exec-stop-post <cmd> |
Command to run after process stops |
--log-level-max <level> |
Max log level to store (e.g. warning, err) |
--random-delay <duration> |
Random delay for timer firing (e.g. 5m) |
--env <KEY=VALUE> |
Environment variable (repeatable) |
--no-notify |
Disable failure notification for this unit |
--dry-run |
Preview generated unit files without creating them |
Failure Notifications
Set up Slack notifications for when any unit fails:
# With user mention
This creates a template unit sdtab-notify@.service and adds OnFailure=sdtab-notify@%n.service to all subsequently created units. When a timer or service fails, systemd triggers the notification unit, which sends a message to Slack via curl.
To opt out of notifications for a specific unit:
In Sdtabfile.toml, use no_notify = true:
[]
= "0 9 * * *"
= "./quiet-task.sh"
= "/home/user"
= true
Export Format
sdtab export produces a TOML file:
[]
= "0 3 * * *"
= "./backup.sh"
= "/home/user/project"
= "512M"
[]
= "node dist/index.js"
= "/home/user/app"
= "Web Server"
= "on-failure"
= "/home/user/.env"
Use sdtab apply Sdtabfile.toml to recreate all units from this file. Add --prune to remove sdtab-managed units not in the file.
How It Works
sdtab generates standard systemd unit files under ~/.config/systemd/user/ with a sdtab- prefix. No custom daemon or database — everything is plain systemd.
~/.config/systemd/user/
├── sdtab-backup.service # [Service] definition
├── sdtab-backup.timer # [Timer] with OnCalendar
├── sdtab-web.service # Long-running service
├── sdtab-notify@.service # Failure notification template (if webhook configured)
Metadata is stored as comments in the service file (# sdtab:type=, # sdtab:cron=, etc.), so sdtab can reconstruct the original configuration without an external database.
Comparison with Alternatives
| sdtab | crontab | systemd-cron | fcron | jobber | |
|---|---|---|---|---|---|
| One command to create timer | Yes | Yes | No (uses crontab files) | Yes | Yes |
| Long-running services | Yes (@service) |
No | No (oneshot only) | No | No |
| Resource limits (memory/CPU) | --memory-max, --cpu-quota |
No | Manual (edit unit files) | No | No |
| Export/import config | Yes (TOML) | crontab -l (text) |
No | No | No |
| Machine-readable output | --json |
No | No | No | No |
| Backend | systemd native | crond | systemd (generated) | own daemon | own daemon |
| User-level without root | Yes | Yes | System-level | Needs root | Needs root |
Testing
The cron parser, unit file generation, and TOML serialization are covered by 93 unit tests:
AI Agent Support
sdtab init installs a Claude Code skill file to ~/.claude/commands/sdtab.md. After that, you can manage systemd timers from any project with natural language:
You> /sdtab run report.py every day at 9am
Claude Code interprets the intent, runs sdtab add "0 9 * * *" "uv run ./report.py" --dry-run for confirmation, then creates the timer.
The skill works globally — not just inside the sdtab repository. Once installed, any Claude Code session can create, list, and manage timers and services through /sdtab.
Also included:
CLAUDE.md— project instructions with full command reference (auto-loaded by AI agents)--dry-run— preview generated unit files before committing--json— machine-readable output for programmatic use
License
MIT