Scheduling¶
Istota supports cron-based scheduling through CRON.md files and natural-language reminders.
CRON.md¶
Each user can define scheduled jobs in /Users/{user_id}/{bot_dir}/config/CRON.md. The file uses markdown with an embedded TOML block:
# Scheduled jobs
```toml
[[jobs]]
name = "daily-check"
cron = "0 9 * * *"
prompt = "Check my calendar for today and summarize what's coming up"
conversation_token = "room123"
[[jobs]]
name = "backup-reminder"
cron = "0 10 * * 1"
prompt = "Remind me to check the backup logs"
output_target = "talk"
silent_unless_action = true
[[jobs]]
name = "disk-check"
cron = "0 */6 * * *"
type = "command"
command = "df -h / | tail -1"
```
CRON.md is the source of truth. cron_loader.py reads it and syncs job definitions to the scheduled_jobs DB table.
Job types¶
Prompt jobs (default): Run through Claude Code like any other task. The prompt is sent to the executor with full skill and context support.
Prompt file jobs: Like prompt jobs, but the prompt is loaded from an external file. Paths are relative to the Nextcloud mount root.
Command jobs: Run shell commands in a subprocess (via _run_capture, which kills the whole process group on timeout). No Claude invocation. Output captured and optionally posted to Talk.
All job types go through the same task queue with retry logic, !stop support, failure tracking, and auto-disable.
Configuration options¶
| Field | Required | Description |
|---|---|---|
name |
yes | Job identifier |
cron |
yes | Cron expression (evaluated in user's timezone) |
prompt |
for prompt jobs | The prompt to send to Claude |
prompt_file |
for prompt_file jobs | Path to prompt file (relative to mount root) |
command |
for command jobs | Shell command to execute |
type |
no | "prompt" (default), "prompt_file", or "command" |
conversation_token |
no | Talk room for output |
output_target |
no | talk, email, ntfy, both, all, or a surface:channel / comma-list descriptor |
once |
no | Auto-delete after successful execution |
silent_unless_action |
no | Suppress output unless response has ACTION: prefix |
skip_log_channel |
no | Suppress log channel output for frequent jobs |
model |
no | Claude model override for this job (e.g. "claude-sonnet-4-6") |
effort |
no | Effort override: low, medium, high, xhigh, or max |
Failure handling¶
Jobs auto-disable after 5 consecutive failures (scheduled_job_max_consecutive_failures). Failures reset on success. Disabled jobs can be re-enabled via !cron enable <name> in Talk.
Catch-up suppression after outages¶
When the daemon returns from a long outage, it does not fire every missed cron instance at once. If a job or briefing's computed next fire time is more than cron_max_staleness_minutes (default 60) behind the current time, the fire is skipped and last_run_at is bumped so the schedule resumes from the next future fire. This prevents a thundering herd of stale tasks from flooding the queue after a restart.
Set cron_max_staleness_minutes = 0 in [scheduler] to restore the prior behavior (unconditional catch-up of all missed fires).
Context isolation¶
Scheduled job results are excluded from interactive conversation context. This prevents cron output from cluttering a user's chat history.
Reminders¶
Natural-language reminders are implemented as one-shot cron entries (once = true). When a user asks "remind me to do X tomorrow at 9am", the bot creates a CRON.md entry that fires once and auto-deletes.
Managing jobs¶
In Talk, use !cron to list, enable, or disable scheduled jobs: