Schedule local Pi agent prompts with systemd user timers.
pi-task is a local command-line tool for recurring and on-demand Pi agent work on a single Linux machine. Each run creates an isolated, resumable Pi session. Task configuration stays in readable TOML; run history lives in SQLite; durable scheduling and process supervision are delegated to the systemd user manager.
| Supported | Not supported (first version) |
|---|---|
| Linux with a systemd user manager | macOS, Windows, non-systemd schedulers |
| Python 3.14 | Older Python versions |
| Single-user local operation | Multi-user / shared-host orchestration |
| Calendar and interval schedules | Per-task time zones |
| Manual runs, pause/resume, cancel | Scheduler-level automatic retries |
| Project trust: inherit / approve / deny | Custom permission systems or tool allowlists |
| Journald operational logs; optional ntfy push Notifications | GUI / TUI, desktop notification daemons |
| Persistent Pi sessions per run | Shared or auto-continued sessions across runs |
Also out of scope: prompt templating, automatic history/session pruning, automatic provider credential management, parallel runs inside one working directory, and recording activations that systemd suppresses before the wrapper starts.
Arch Linux is the primary development platform; behavior is built on portable systemd user interfaces rather than distribution-specific tooling.
- Linux + systemd user manager that can run
systemctl --user. - Python 3.14.
- uv for installation (recommended).
- Pi on
PATH, already authenticated for the models you intend to use. - User lingering when tasks must run without an active login session (recommended; see Lingering).
Provider credentials stay with Pi (or your user-manager environment). pi-task never copies secrets into task storage.
Install the command into uv's user-level tool directory from this repository:
uv tool install .
pi-task --version
pi-task doctorFrom a built wheel (release verification path):
uv build
uv tool install dist/pi_task-*.whl
pi-task --versiondoctor reports required failures with a nonzero exit status. Fix FAIL items before relying on scheduled work. WARN items are actionable but not fatal.
Typer provides shell completion for bash, zsh, and fish:
pi-task --install-completion
# or print the script to review / install manually:
pi-task --show-completionRestart the shell (or source your rc file) after installing.
Scheduled tasks run under the systemd user manager. When you log out, that manager may stop unless lingering is enabled.
loginctl enable-linger "$USER"
pi-task doctorDisabled lingering is an actionable warning: tasks can still run while you are logged in, but unattended runs after logout may not fire. Enable lingering when automation must survive logout.
Guided creation:
pi-task addScripted calendar task (weekdays at 09:00 local time):
pi-task add daily-review \
--name "Daily review" \
--working-directory ~/src/project \
--prompt-file ~/prompts/review.md \
--calendar "Mon..Fri 09:00" \
--model openai-codex/gpt-5.4 \
--thinking high \
--timeout 30m \
--trust inherit \
--yesInterval task (first fire one interval after activation; minimum interval is one minute):
pi-task add heartbeat \
-C ~/src/project \
--prompt "Summarize git status in one sentence." \
--interval 15m \
--model openai-codex/gpt-5.4 \
--thinking low \
--yesCreate configuration without enabling the timer (validate first):
pi-task add trial -C ~/src/project --prompt "ping" --calendar "daily" \
--model openai-codex/gpt-5.4 --paused --yes
pi-task show trial
pi-task resume trialCommon day-to-day commands:
pi-task list
pi-task show daily-review
pi-task edit daily-review --thinking low --interval 1h
pi-task pause daily-review
pi-task resume daily-review
pi-task sync
pi-task run daily-review
pi-task runs
pi-task runs daily-review
pi-task logs RUN_ID
pi-task cancel RUN_ID
pi-task resume-session daily-review
pi-task remove daily-review --yes
pi-task notify
pi-task notify --showEvery task needs:
| Field | Notes |
|---|---|
| Task ID | Immutable lowercase slug (daily-review). Used in units, locks, and filenames. |
| Working directory | Explicit project path; Pi runs there. |
| Prompt | Exactly one of --prompt or --prompt-file. No templating. --result-reporting appends the result contract described below. |
| Schedule | Exactly one of --calendar or --interval. |
| Model | Available provider/model as seen by the systemd user-manager environment. |
| Thinking level | Pi thinking level (off … max). Default medium. |
| Timeout | Default 30m. Wrapper enforces it; systemd has a slightly longer hard backstop. |
| Trust | inherit (default), approve, or deny. |
Structured Result reporting is opt-in per Task:
pi-task add daily-review ... --result-reporting
pi-task edit daily-review --result-reporting
pi-task edit daily-review --no-result-reportingWhen enabled, pi-task asks Pi to end its final response with a small JSON Result containing an outcome and summary. Outcomes are succeeded, partial, blocked, failed, or unknown. A missing or malformed Result becomes unknown. This is the agent's own assessment, not independent verification.
Optional: --name for a human-readable label, --catch-up / --no-catch-up for calendar persistence, --paused to create without activating the timer, --yes to skip confirmation.
add validates the model in the user-manager environment, validates the schedule, previews upcoming calendar occurrences, and writes the TOML definition before reconciling generated units. Inherited project trust emits a warning when Pi has no saved decision for the working directory or a parent.
Calendar — systemd calendar syntax in the machine's local time zone (for example Mon..Fri 09:00, daily, *-*-* 12:00:00). Catch-up after machine downtime is on by default and uses systemd persistence; multiple missed occurrences coalesce into one.
Interval — friendly durations such as 15m or 2h. First fire is one interval after activation. Missed interval occurrences are never replayed. Intervals faster than one minute are rejected.
| Policy | Behavior |
|---|---|
inherit |
Use Pi's existing saved trust decision for the path (default). |
approve |
Explicitly allow project resources for unattended runs. |
deny |
Explicitly refuse project resources. |
Directories are never silently approved. Prefer deny or an already-trusted path for unattended automation.
pi-task does not store provider API keys. Use Pi's normal login state, or supply credentials through the systemd user-manager environment if that is how you already run Pi. doctor helps confirm the Pi executable and models are visible where scheduled services will run.
| Command | Effect |
|---|---|
list / show |
Inspect configuration and scheduling state. |
edit |
Validate the full resulting config and apply atomically. Active runs keep their original snapshot. |
pause |
Suppress future scheduled runs and clear calendar persistence. Does not cancel an active run. Occurrences missed while paused are skipped. |
resume |
Schedule only future work. Interval schedules restart their interval from resume time. |
sync |
Regenerate units from definitions; remove generated orphans. |
remove |
End future scheduling; delete the definition and generated units. Preserves run history and Pi sessions. |
notify |
Configure global ntfy Notifications for terminal Runs (see below). |
Task definitions live under $XDG_CONFIG_HOME/pi-task/tasks (default ~/.config/pi-task/tasks). Generated units under $XDG_CONFIG_HOME/systemd/user are marked as generated — do not edit them by hand; change the TOML and run sync or edit.
When a timer fires, the generated service runs pi-task _run-scheduled TASK_ID --source scheduled. The wrapper snapshots the task, takes exclusive same-task and same-working-directory locks under $XDG_RUNTIME_DIR/pi-task/locks, hashes the resolved user prompt, invokes Pi once in --mode json, and records the Run plus session path in $XDG_STATE_HOME/pi-task/runs.db. For Tasks with Result reporting enabled, it appends a short reporting contract before invoking Pi.
| Concern | Behavior |
|---|---|
| Overlap | Same task cannot overlap; tasks sharing a normalized working directory are serialized; different directories may run concurrently. |
| Scheduled lock conflict | Recorded as skipped when the wrapper starts. Activations systemd suppresses before the wrapper starts are not recorded. |
| Manual run | pi-task run TASK_ID starts a uniquely named transient user service (--collect) and returns immediately with its Run ID. Inspect the eventual result with pi-task runs TASK_ID or pi-task logs RUN_ID. Never starts, stops, or modifies the recurring timer. |
| Timeout | Wrapper records timed_out. Units also set RuntimeMaxSec slightly above the task timeout as a hung-wrapper backstop. After upgrades, run pi-task sync. |
| Cancel | pi-task cancel RUN_ID stops the supervising unit. Distinct from pause. Status cancelled; partial sessions remain. |
| Run success | Normal Pi process exit with a final assistant stop reason indicating completion. Recovered tool errors do not override operational success. A reported partial, blocked, failed, or unknown Result does not change the wrapper exit code. |
| Retries | pi-task does not automatically replay failed prompts. |
pi-task runs
pi-task runs daily-review --limit 50
pi-task logs RUN_ID
pi-task resume-session daily-reviewruns shows source, status (succeeded / failed / skipped / cancelled / timed_out), optional Result and summary, start time, duration, model, thinking, prompt hash, tokens, cost when available, session id/path, and supervising unit / invocation when known.
logs RUN_ID reads journald for that run. With a stored INVOCATION_ID, selection is exact; otherwise it falls back to unit name plus a tight time window. Missing or expired journal lines are explained without changing SQLite history. Concise lifecycle lines go to the journal; the full Pi JSON stream does not — the Pi session remains the canonical detailed history.
resume-session TASK_ID opens the newest run for that task with pi --session PATH, whether the run succeeded or ended unsuccessfully. Active runs cannot be opened interactively. Failed, cancelled, and timed-out sessions remain available once the run is no longer active.
Optional machine-global push Notifications when a Run finishes. Delivery is best-effort and never changes Run status.
pi-task notify
pi-task notify --url https://ntfy.example --topic pi-task --on attention --no-test
pi-task notify --show| Piece | Behavior |
|---|---|
| Config | $XDG_CONFIG_HOME/pi-task/notify.toml — base URL, topic, optional bearer token, triggers |
| Triggers | success, attention, both, or none (soft off). fail remains an alias for attention. |
| Scope | Every terminal Run (scheduled and manual), including lock-conflict terminals |
| Success | Operationally successful Run with reporting disabled, or a succeeded Result |
| Attention | Any operational non-success, or a partial, blocked, failed, or unknown Result |
| Title | Completed without reporting; semantic Result title when reported; otherwise the operational status |
| Body | Status, source, duration, bounded Result summary, and error when present |
| Tags | white_check_mark / x |
Absent config means no publish attempts. Re-run notify to edit; saved values are defaults. After save, the wizard can send a test Notification (skippable; failure warns but keeps the config).
| Location | Content |
|---|---|
$XDG_CONFIG_HOME/pi-task/tasks |
Human-readable task TOML (source of truth) |
$XDG_CONFIG_HOME/pi-task/notify.toml |
Optional global ntfy Notification config |
$XDG_CONFIG_HOME/systemd/user |
Generated pi-task-*.service / .timer units |
$XDG_STATE_HOME/pi-task/runs.db |
Run and session metadata |
$XDG_RUNTIME_DIR/pi-task/locks |
Same-task and working-directory locks |
| Pi's normal session storage | Conversation transcripts |
For isolated testing, dependency executables can be selected through PATH or:
PI_TASK_PI_EXECUTABLE, PI_TASK_SYSTEMCTL_EXECUTABLE, PI_TASK_SYSTEMD_ANALYZE_EXECUTABLE, PI_TASK_SYSTEMD_RUN_EXECUTABLE, PI_TASK_EXECUTABLE, PI_TASK_JOURNALCTL_EXECUTABLE, PI_TASK_LOGINCTL_EXECUTABLE.
A Pi override must also be present in the systemd user manager environment for scheduled runs. Point XDG_CONFIG_HOME, XDG_STATE_HOME, XDG_DATA_HOME, and XDG_RUNTIME_DIR at temporary directories to keep diagnostics and automated tests out of real user locations.
No automated test or release command silently creates real scheduled work. The default test suite uses fakes and isolated XDG directories. The optional smoke test creates only a paused disposable task and always removes it (see below).
uv sync --locked
uv run ruff format --check .
uv run ruff check .
uv run ty check
uv run pytest
uv buildThe automated suite uses isolated XDG directories and fake Pi / systemd-family executables. It does not call real Pi or modify the developer's systemd state. Smoke tests are excluded by default (-m 'not smoke').
Exercise a disposable paused task against the real user manager and Pi (never part of normal CI):
PI_TASK_SMOKE=1 uv run pytest -m smoke
# optional model pin:
PI_TASK_SMOKE=1 PI_TASK_SMOKE_MODEL=provider/model uv run pytest -m smokeRequires a working Pi login, models visible to Pi, and a running systemd user manager. The test runs one manual invocation and removes the task afterward. It will not leave an enabled timer.
GitHub Actions runs lockfile sync, Ruff, ty, pytest (without smoke), packaging build, and installation verification on Python 3.14 only.