Cron Scheduler¶
Qanot AI includes an APScheduler-based cron system for running scheduled tasks. The agent can create, update, and delete cron jobs through natural conversation.
How Cron Jobs Work¶
Cron jobs are defined in {cron_dir}/jobs.json. Each job has a name, cron schedule, execution mode, and a prompt that tells the agent what to do.
[
{
"name": "daily-summary",
"schedule": "0 20 * * *",
"mode": "isolated",
"prompt": "Summarize today's conversations and update MEMORY.md",
"enabled": true
}
]
Cron Expression Format¶
Standard 5-field cron: minute hour day month day_of_week
| Expression | Meaning |
|---|---|
0 */4 * * * |
Every 4 hours |
0 20 * * * |
Daily at 20:00 |
30 9 * * 1-5 |
Weekdays at 9:30 |
0 0 1 * * |
First day of each month |
*/15 * * * * |
Every 15 minutes |
The timezone from config is used for scheduling (default: Asia/Tashkent).
Execution Modes¶
isolated¶
Spawns an independent agent with its own conversation history, context tracker, and session writer.
How it works:
- A fresh
Agentinstance is created withprompt_mode="minimal"(only SOUL.md + TOOLS.md + session info) - The prompt is sent as a user message
- The agent runs its full tool loop (up to 25 iterations)
- Results are logged to a dedicated session file (
cron-{name}-{timestamp}.jsonl) - If the agent writes to
proactive-outbox.md, the content is sent to all allowed users
Use for: Background tasks that should not interfere with ongoing user conversations. Examples: memory cleanup, periodic web checks, data processing.
systemEvent¶
Injects the prompt into the main agent's message queue as a system event.
How it works:
- The prompt is put into the scheduler's message queue
- The Telegram adapter's proactive loop picks it up
- The main agent processes it as a regular turn (with full conversation context)
Use for: Tasks that need the current conversation context or should appear as part of the ongoing conversation. Examples: reminders, time-based follow-ups, scheduled check-ins.
Default Heartbeat Job¶
A heartbeat job is automatically created if it does not exist in jobs.json:
{
"name": "heartbeat",
"schedule": "0 */4 * * *",
"mode": "isolated",
"prompt": "HEARTBEAT: Read HEARTBEAT.md and perform self-improvement checks:\n1. Check proactive-tracker.md -- overdue behaviors?\n2. Pattern check -- repeated requests to automate?\n3. Outcome check -- decisions >7 days old to follow up?\n4. Memory -- context %, update MEMORY.md with distilled learnings\n5. Proactive surprise -- anything to delight human?\nIf you have a message for the human, write it to /data/workspace/proactive-outbox.md",
"enabled": true
}
This runs every 4 hours, prompting the agent to review its own state, clean up memory, and optionally send a proactive message to the user.
Proactive Messaging¶
Cron jobs can send messages to users through the proactive-outbox.md mechanism:
- An isolated cron job writes content to
{workspace_dir}/proactive-outbox.md - After the job completes, the scheduler checks the outbox
- If content exists, it is sent to all
allowed_usersvia Telegram - The outbox is cleared
This is the only way for isolated cron jobs to communicate with users. System event jobs communicate directly through the conversation.
Scheduling Skills¶
Skills created via the create_skill tool can be scheduled to run periodically using cron jobs. The cron prompt can reference the run_skill_script tool:
{
"name": "weekly-seo-check",
"schedule": "0 9 * * 1",
"mode": "isolated",
"prompt": "Run the seo-check skill for all tracked URLs and summarize results"
}
The isolated agent will use the run_skill_script tool to execute the skill and process the results.
Managing Cron Jobs¶
Via Tools (in conversation)¶
The agent can manage cron jobs through natural conversation:
User: Set up a daily summary at 8pm
Agent: [calls cron_create with name="daily-summary", schedule="0 20 * * *", ...]
Available tools: cron_create, cron_list, cron_update, cron_delete. See Tools for parameters.
cron_create Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Unique job name |
prompt |
string | Yes | Reminder text or task prompt |
schedule |
string | No* | Cron expression (e.g. 0 9 * * *) for recurring jobs |
at |
string | No* | ISO 8601 timestamp (e.g. 2026-03-12T17:00:00+05:00) for one-shot reminders |
mode |
string | No | "systemEvent" (text delivery) or "isolated" (full agent). Default: "systemEvent" |
delete_after_run |
boolean | No | Auto-delete job after execution. Default: true for at reminders, false for recurring |
timezone |
string | No | IANA timezone override for this job (e.g. "Asia/Tashkent", "Europe/London") |
* Either schedule or at must be provided. Use schedule for recurring jobs and at for one-shot reminders.
One-shot reminders: When using the at parameter, the job runs once at the specified ISO 8601 timestamp and is automatically deleted afterward (delete_after_run is forced to true).
Per-job timezone: By default, jobs use the global timezone from config. The timezone parameter overrides this for a specific job, useful when the user needs reminders in a different timezone.
Via jobs.json (manual)¶
Edit {cron_dir}/jobs.json directly. Changes take effect after restarting the bot, or the agent can call cron_update to trigger a reload.
Job Reloading¶
When a cron tool modifies jobs.json, it calls scheduler.reload_jobs() which:
- Removes all existing
cron_*jobs from the APScheduler - Re-reads
jobs.jsonfrom disk - Ensures the heartbeat job exists
- Re-adds all enabled jobs
This means changes take effect immediately without restarting the bot.
Error Handling¶
- Failed isolated jobs: Errors are logged but do not affect the main bot
- Failed system events: Errors are logged and retried on the next proactive loop iteration
- Invalid cron expressions: Jobs with expressions that are not 5 fields are skipped with a warning
Architecture Notes¶
- The scheduler uses
AsyncIOSchedulerfrom APScheduler 3.x - Each isolated job gets a fresh
Agentwithprompt_mode="minimal"to keep system prompts small - The scheduler shares the same
LLMProviderandToolRegistryas the main agent - The message queue is an
asyncio.Queuebridging the scheduler and the Telegram adapter