Scheduling Jobs in Zenii
Zenii includes a built-in job scheduler for recurring tasks like periodic agent prompts, notifications, heartbeat checks, and channel messages. The scheduler is feature-gated — enable it with the scheduler feature flag.
Note: This document was generated with AI assistance and may contain inaccuracies. If you find errors, please report an issue.
Quick Start
Enable the scheduler
# Build daemon with scheduler support
cargo run -p zenii-daemon --features scheduler
# Or build with all features
cargo run -p zenii-daemon --all-features
The scheduler starts automatically on boot, loading any previously saved jobs from the database.
Concepts
Schedule Types
| Type | Description | Example |
|---|---|---|
| Interval | Run every N seconds | Every 5 minutes (300s) |
| Cron | Standard cron expression (5, 6, or 7 fields) | 0 9 * * 1-5 (weekdays at 9 AM) |
Payload Types
| Payload | Description | Use Case |
|---|---|---|
| Heartbeat | Run a heartbeat checklist | System health monitoring |
| Agent Turn | Execute the AI agent with a prompt | Periodic summaries, reports |
| Notify | Publish a notification event | Reminders, alerts |
| Send via Channel | Send a message through a named channel | Telegram/Slack/Discord updates |
Key Features
- Active hours — Restrict jobs to a time window (e.g., 9 AM – 5 PM). Jobs outside the window are skipped.
- One-shot jobs — Run once and auto-delete on success. Useful for deferred tasks.
- Error backoff — Failed jobs retry with exponential backoff: 30s → 60s → 5m → 15m → 1h.
- Execution history — Each job keeps the last 100 execution records (configurable).
- Persistence — Jobs survive daemon restarts (stored in SQLite).
Using the Scheduler
Desktop App (Tauri)
Open the Schedule page from the sidebar navigation.
Creating a job:
- Click "New Job" to expand the creation form
- Enter a name (e.g., "Morning Briefing")
- Select a schedule type:
- Interval: enter seconds (e.g.,
3600for hourly) - Cron: enter a cron expression (e.g.,
0 9 * * *for daily at 9 AM)
- Interval: enter seconds (e.g.,
- Select a payload type and fill in the required fields:
- Notify: enter a message
- Agent Turn: enter a prompt
- Heartbeat: no extra fields
- Optionally set session target (Main or Isolated)
- Optionally check One-shot for single-run jobs
- Optionally enable Active hours and set the time window
- Click Create
Managing jobs:
- Toggle (play/pause icon) — Enable or disable a job without deleting it
- History (clock icon) — View past executions with status badges (green = success, red = failed, yellow = stuck, gray = skipped)
- Delete (trash icon) — Permanently remove a job
The scheduler status bar at the top shows whether the scheduler is running and how many jobs are registered.
CLI
All scheduler commands go through the daemon's HTTP API, so ensure the daemon is running with the scheduler feature enabled.
# Check scheduler status
zenii-cli schedule status
# Output: Scheduler: Running (3 jobs)
List all jobs:
zenii-cli schedule list
Output:
ID | Name | Status | Schedule | Next Run | Errors
-------------------------------------+-------------------+----------+------------------+-----------------------+-------
a1b2c3d4-... | Morning Briefing | enabled | Cron: 0 9 * * * | 2026-03-09 09:00:00 | 0
e5f6g7h8-... | Health Check | enabled | Every 5m | 2026-03-08 15:35:00 | 0
i9j0k1l2-... | Weekly Report | disabled | Cron: 0 17 * * 5 | — | 2
Create a notification job (every 30 minutes):
zenii-cli schedule create \
--name "Hydration Reminder" \
--schedule-type interval \
--interval-secs 1800 \
--payload-type notify \
--message "Time to drink some water!"
Create a daily agent prompt (weekdays at 9 AM):
zenii-cli schedule create \
--name "Morning Briefing" \
--schedule-type cron \
--cron-expr "0 9 * * 1-5" \
--payload-type agent_turn \
--prompt "Summarize my unread messages and today's calendar. Keep it brief."
Create a one-shot reminder (run once in 1 hour, then delete):
zenii-cli schedule create \
--name "Deploy Reminder" \
--schedule-type interval \
--interval-secs 3600 \
--payload-type notify \
--message "Don't forget to deploy the hotfix!" \
--one-shot
Create a heartbeat job (every 2 minutes, only during work hours):
zenii-cli schedule create \
--name "System Heartbeat" \
--schedule-type interval \
--interval-secs 120 \
--payload-type heartbeat
Active hours are set via the API or Desktop UI. The CLI
createcommand creates jobs with no active hours restriction by default.
Toggle a job on/off:
zenii-cli schedule toggle a1b2c3d4-e5f6-7890-abcd-ef1234567890
# Output: Job a1b2c3d4-...: disabled
View execution history:
zenii-cli schedule history a1b2c3d4-e5f6-7890-abcd-ef1234567890
Output:
[success] 2026-03-08 09:00:01
[success] 2026-03-07 09:00:02
[failed] 2026-03-06 09:00:01 — connection timeout
[skipped] 2026-03-05 09:00:00
Delete a job:
zenii-cli schedule delete a1b2c3d4-e5f6-7890-abcd-ef1234567890
# Output: Job a1b2c3d4-... deleted.
TUI
The TUI (zenii-tui) communicates with the same daemon HTTP API. Scheduler management is available through the schedule view panel. Navigation and controls mirror the CLI semantics — list, create, toggle, delete, and view history — rendered in a terminal UI.
HTTP API (Direct)
For scripting or custom integrations, call the gateway endpoints directly. The daemon listens on localhost:18981 by default.
Get scheduler status:
curl http://localhost:18981/scheduler/status
# {"running":true,"job_count":3}
List all jobs:
curl http://localhost:18981/scheduler/jobs
Create a job:
curl -X POST http://localhost:18981/scheduler/jobs \
-H "Content-Type: application/json" \
-d '{
"name": "Evening Summary",
"schedule": { "Cron": { "expr": "0 18 * * *" } },
"session_target": "Main",
"payload": { "AgentTurn": { "prompt": "Give me a summary of what I accomplished today." } },
"enabled": true,
"delete_after_run": false
}'
# {"id":"generated-uuid-here"}
Create a channel notification (requires channels feature):
curl -X POST http://localhost:18981/scheduler/jobs \
-H "Content-Type: application/json" \
-d '{
"name": "Telegram Daily Update",
"schedule": { "Cron": { "expr": "0 8 * * *" } },
"session_target": "Isolated",
"payload": { "SendViaChannel": { "channel": "telegram", "message": "Good morning! Here is your daily update." } },
"enabled": true,
"active_hours": { "start_hour": 7, "end_hour": 22 },
"delete_after_run": false
}'
Toggle a job:
curl -X PUT http://localhost:18981/scheduler/jobs/{job_id}/toggle
# {"id":"...","enabled":false}
View job history:
curl http://localhost:18981/scheduler/jobs/{job_id}/history
Delete a job:
curl -X DELETE http://localhost:18981/scheduler/jobs/{job_id}
# 204 No Content
Real-World Examples
1. Daily standup prep (weekday mornings)
Have the agent prepare a standup summary every weekday at 8:45 AM:
zenii-cli schedule create \
--name "Standup Prep" \
--schedule-type cron \
--cron-expr "45 8 * * 1-5" \
--payload-type agent_turn \
--prompt "Review my git commits from yesterday, open PRs, and any blocked tasks. Format as a standup update: what I did, what I'm doing, blockers."
2. Hourly system health check
Run a heartbeat every hour during business hours:
curl -X POST http://localhost:18981/scheduler/jobs \
-H "Content-Type: application/json" \
-d '{
"name": "Hourly Health Check",
"schedule": { "Interval": { "secs": 3600 } },
"payload": { "Heartbeat": {} },
"enabled": true,
"active_hours": { "start_hour": 9, "end_hour": 18 }
}'
3. Weekly project report (Friday at 5 PM)
zenii-cli schedule create \
--name "Weekly Report" \
--schedule-type cron \
--cron-expr "0 17 * * 5" \
--payload-type agent_turn \
--prompt "Generate a weekly project report: summarize completed tasks, highlight achievements, list outstanding items, and suggest priorities for next week."
4. One-shot deploy reminder
Remind yourself in 2 hours to check the deployment:
zenii-cli schedule create \
--name "Check Deploy" \
--schedule-type interval \
--interval-secs 7200 \
--payload-type notify \
--message "Check the production deployment status and verify monitoring dashboards." \
--one-shot
5. Telegram channel daily digest
Send a daily digest to your Telegram bot at 7 PM:
curl -X POST http://localhost:18981/scheduler/jobs \
-H "Content-Type: application/json" \
-d '{
"name": "Telegram Daily Digest",
"schedule": { "Cron": { "expr": "0 19 * * *" } },
"session_target": "Isolated",
"payload": { "SendViaChannel": { "channel": "telegram", "message": "Daily digest: tasks completed, pending items, tomorrow priorities." } },
"enabled": true
}'
6. Pomodoro break reminders (every 25 minutes during work)
curl -X POST http://localhost:18981/scheduler/jobs \
-H "Content-Type: application/json" \
-d '{
"name": "Pomodoro Break",
"schedule": { "Interval": { "secs": 1500 } },
"payload": { "Notify": { "message": "Pomodoro complete! Take a 5-minute break." } },
"enabled": true,
"active_hours": { "start_hour": 9, "end_hour": 17 }
}'
Configuration
All scheduler tunables live in ~/.config/zenii/config.toml:
# How often the scheduler checks for due jobs (seconds)
scheduler_tick_interval_secs = 1
# Time before a running job is marked "stuck" (seconds)
scheduler_stuck_threshold_secs = 120
# Exponential backoff delays for failed jobs (seconds)
scheduler_error_backoff_secs = [30, 60, 300, 900, 3600]
# Maximum execution history entries kept per job
scheduler_max_history_per_job = 100
Tuning tips:
- For low-resource machines, increase
scheduler_tick_interval_secsto5or10to reduce CPU wake-ups - Increase
scheduler_stuck_threshold_secsfor jobs that call slow external APIs - Adjust
scheduler_error_backoff_secsto match your retry tolerance — the array caps at the last value (1 hour by default)
Cron Expression Reference
Zenii supports standard 5-field, 6-field (with seconds), and 7-field cron expressions:
┌───────────── second (0–59, optional)
│ ┌───────────── minute (0–59)
│ │ ┌───────────── hour (0–23)
│ │ │ ┌───────────── day of month (1–31)
│ │ │ │ ┌───────────── month (1–12)
│ │ │ │ │ ┌───────────── day of week (0–6, Sun=0)
│ │ │ │ │ │
* * * * * *
| Expression | Meaning |
|---|---|
* * * * * | Every minute |
0 * * * * | Every hour |
0 9 * * * | Daily at 9:00 AM |
0 9 * * 1-5 | Weekdays at 9:00 AM |
0 0 1 * * | First of every month at midnight |
*/15 * * * * | Every 15 minutes |
0 9,17 * * * | At 9:00 AM and 5:00 PM |
0 0 * * 0 | Every Sunday at midnight |
5-field expressions are normalized by prepending 0 for the seconds field automatically.
Architecture Notes
- Persistence: Jobs are stored in the
scheduled_jobsSQLite table (migration v4). On daemon start, all jobs are loaded into a lock-freeDashMapfor fast in-memory access. - Tick loop: A background
tokio::spawntask wakes everytick_interval_secs, evaluates due jobs, executes payloads, and records results. - Event bus: The scheduler publishes events (
SchedulerStarted,SchedulerStopped,HeartbeatTick,CronFired) for other system components to react to. - Feature gate: All scheduler code is behind
#[cfg(feature = "scheduler")]. When disabled, the scheduler routes return empty and no background task runs.