Skip to main content

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

TypeDescriptionExample
IntervalRun every N secondsEvery 5 minutes (300s)
CronStandard cron expression (5, 6, or 7 fields)0 9 * * 1-5 (weekdays at 9 AM)

Payload Types

PayloadDescriptionUse Case
HeartbeatRun a heartbeat checklistSystem health monitoring
Agent TurnExecute the AI agent with a promptPeriodic summaries, reports
NotifyPublish a notification eventReminders, alerts
Send via ChannelSend a message through a named channelTelegram/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:

  1. Click "New Job" to expand the creation form
  2. Enter a name (e.g., "Morning Briefing")
  3. Select a schedule type:
    • Interval: enter seconds (e.g., 3600 for hourly)
    • Cron: enter a cron expression (e.g., 0 9 * * * for daily at 9 AM)
  4. Select a payload type and fill in the required fields:
    • Notify: enter a message
    • Agent Turn: enter a prompt
    • Heartbeat: no extra fields
  5. Optionally set session target (Main or Isolated)
  6. Optionally check One-shot for single-run jobs
  7. Optionally enable Active hours and set the time window
  8. 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 create command 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_secs to 5 or 10 to reduce CPU wake-ups
  • Increase scheduler_stuck_threshold_secs for jobs that call slow external APIs
  • Adjust scheduler_error_backoff_secs to 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)
│ │ │ │ │ │
* * * * * *
ExpressionMeaning
* * * * *Every minute
0 * * * *Every hour
0 9 * * *Daily at 9:00 AM
0 9 * * 1-5Weekdays 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 * * 0Every 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_jobs SQLite table (migration v4). On daemon start, all jobs are loaded into a lock-free DashMap for fast in-memory access.
  • Tick loop: A background tokio::spawn task wakes every tick_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.