# sdtab
[日本語](README_ja.md)
Manage systemd user timers and services like crontab.
```bash
# Add a timer (cron syntax)
sdtab add "0 9 * * *" "uv run ./report.py"
# Add a long-running service
sdtab add "@service" "node server.js" --restart on-failure
# List all managed units
sdtab list
```
```
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 `@service` to create always-on daemons with restart policies
- **Resource limits** — set memory, CPU, and I/O constraints per unit
- **Export/Import** — `sdtab export` dumps config to TOML, `sdtab apply` restores 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
```bash
cargo install --git https://github.com/kok1eee/systemdtab
```
Or build from source:
```bash
git clone https://github.com/kok1eee/systemdtab
cd systemdtab
cargo build --release
cp target/release/sdtab ~/.local/bin/
```
### Requirements
- Linux with systemd (user session only — system-wide units are not supported)
- Rust 1.70+
> **Note**: sdtab manages **user-level** units only (`systemctl --user`). It cannot create or manage system-wide services that require root privileges. If `loginctl enable-linger` fails, ask your system administrator to enable it for your user.
## Quick Start
```bash
# Initialize sdtab (enables linger, creates config directory)
sdtab init
# Optional: set up Slack failure notifications
sdtab init --slack-webhook "https://hooks.slack.com/services/T.../B.../xxx"
# Add a daily task at 9:00 AM
sdtab add "0 9 * * *" "./backup.sh" --name backup --memory-max 512M
# Add a service that runs continuously
sdtab add "@service" "node dist/index.js" --name web --restart on-failure --env-file .env
# Check status
sdtab list
sdtab status backup
# View logs
sdtab logs web -f
# Export config to file
sdtab export -o Sdtabfile.toml
# Apply config from file (on another machine)
sdtab apply Sdtabfile.toml --dry-run
sdtab apply Sdtabfile.toml
```
## Commands
| `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]` | List all managed timers and services (long commands are truncated) |
| `sdtab status <name>` | Show detailed status with next 5 run times |
| `sdtab edit <name>` | Edit unit file with $EDITOR |
| `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 remove` stops and disables the unit before deleting files. `sdtab apply --prune` only removes units with the `sdtab-` prefix — manually created systemd units are never touched.
> `sdtab apply` updates 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 init` also installs a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill file to `~/.claude/commands/sdtab.md`, enabling `/sdtab` commands in any project.
## Schedule Syntax
Standard cron expressions and convenient shortcuts:
| `*/5 * * * *` | Every 5 minutes |
| `0 9 * * *` | Daily at 9:00 |
| `0 9 * * Mon-Fri` | Weekdays at 9:00 |
| `@daily` | Once a day (midnight) |
| `@hourly` | Once an hour |
| `@reboot` | On system boot |
| `@daily/3` | Daily at 3:00 |
| `@weekly/Mon,Wed` | Every Monday and Wednesday |
| `@service` | Long-running service (not a timer) |
## Add Options
| `--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:
```bash
sdtab init --slack-webhook "https://hooks.slack.com/services/T.../B.../xxx"
# With user mention
sdtab init --slack-webhook "https://hooks.slack.com/services/T.../B.../xxx" --slack-mention "U0700J8MN3W"
```
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:
```bash
sdtab add "0 9 * * *" "./quiet-task.sh" --no-notify
```
In `Sdtabfile.toml`, use `no_notify = true`:
```toml
[timers.quiet-task]
schedule = "0 9 * * *"
command = "./quiet-task.sh"
workdir = "/home/user"
no_notify = true
```
## Export Format
`sdtab export` produces a TOML file:
```toml
[timers.backup]
schedule = "0 3 * * *"
command = "./backup.sh"
workdir = "/home/user/project"
memory_max = "512M"
[services.web]
command = "node dist/index.js"
workdir = "/home/user/app"
description = "Web Server"
restart = "on-failure"
env_file = "/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
| 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:
```bash
cargo test
```
## AI Agent Support
`sdtab init` installs a [Claude Code](https://docs.anthropic.com/en/docs/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
```bash
sdtab list --json
```
```json
[
{"name":"backup","type":"timer","schedule":"0 3 * * *","command":"./backup.sh","status":"Mon 2026-03-02 03:00:00 JST"},
{"name":"web","type":"service","schedule":"@service","command":"node index.js","status":"active"}
]
```
## License
MIT