Skip to content

Latest commit

 

History

History
235 lines (185 loc) · 9.99 KB

File metadata and controls

235 lines (185 loc) · 9.99 KB

Scheduled Tasks (native cron)

odek can run agent tasks on a cron schedule and deliver each result somewhere — a Telegram chat, stdout, or a log file. The scheduler is native and in-process: it runs inside a long-lived odek process that has already resolved its configuration (API key, model, bot token, default chat) into memory. A scheduled task therefore sees exactly what an interactive odek run does — no environment-inheritance problems, no external cron daemon, no container-only behaviour.

# A weekday stand-up nudge delivered to Telegram
odek schedule add --cron "0 9 * * 1-5" --deliver telegram "Remind me: stand-up in 15 minutes"

# A weekly proactive-nudge digest from Extended Memory (opt in with
# memory.extended.proactive_nudges_enabled: true first)
odek schedule add --cron "0 9 * * 1" --deliver telegram "Review my proactive nudges (open questions, stale goals) and summarize what deserves attention this week"

# Run the scheduler (headless), or just start `odek telegram` — it hosts one too
odek schedule daemon

Where it runs

The same engine runs in two places; pick whichever fits your deployment:

Use when
Inside odek telegram You already run the bot. The scheduler starts automatically as part of the bot process — one process for chat + reminders.
odek schedule daemon You don't run the bot (headless server, CI box). A dedicated foreground process that only schedules.

A shared lock (~/.odek/schedule.pid) coordinates the two so jobs never fire twice — but the two sides handle contention differently: if a daemon already holds the lock, the bot's embedded scheduler defers silently (the bot keeps running, just without scheduling); if the bot holds it, a standalone odek schedule daemon refuses to start and exits non-zero. (Disable the bot's embedded scheduler with schedules.enabled = false if you prefer to run the daemon separately.)

Both also host the storage-maintenance janitor, which expires old sessions, audit records, and plans and rotates oversized logs — see MAINTENANCE.md.


Managing jobs

odek schedule list                          List jobs: id, on/off, cron, next fire, last status
odek schedule add --cron "<expr>" <task>    Add a job (flags below)
odek schedule rm <id>                       Remove a job
odek schedule enable  <id>                  Enable a job
odek schedule disable <id>                  Disable a job (kept, never fires)
odek schedule run  <id>                     Run a job once now and deliver (test it)
odek schedule next <id|"cron-expr">         Preview the next few fire times
odek schedule daemon                        Run the scheduler in the foreground

add flags

Flag Meaning
--cron "<expr>" 5-field cron or @macro (required)
--name <label> Human label (defaults to the first words of the task)
--deliver <dest> stdout (default), log, telegram, or telegram:<chatID>
--tz <IANA> Timezone, e.g. Europe/Berlin (default UTC)
--catchup If a fire was missed while the process was down, run once on startup
--disabled Add without enabling

Definitions are stored in ~/.odek/schedules.json (mode 0600); runtime state (last run, status, next fire) lives in ~/.odek/schedule-state.json. A running scheduler picks up edits to the definitions file automatically (no restart).


Managing from Telegram

When you run odek telegram, the same jobs can be managed from inside the chat — no shell access needed. Two slash commands mirror the CLI:

/schedules                                  List jobs (id, on/off, cron, next fire, last status)
/schedule add <cron> <task> [| opts]        Add a job (delivered to this chat by default)
/schedule view <id>                         Show a job's full detail + recent status
/schedule next <id|cron>                    Preview the next few fire times
/schedule run <id>                          Run a job once now, in this chat
/schedule enable | disable <id>             Toggle a job
/schedule rm <id>                           Remove a job

Because a cron expression has a fixed shape, add needs no quoting: an @macro is one token and a classic expression is exactly five fields; the rest of the line is the task. Options come after a literal |:

/schedule add 0 9 * * 1-5 Summarize my unread email
/schedule add @daily Daily standup digest | tz=Europe/Berlin name=standup
/schedule add */15 9-17 * * 1-5 Check the build | deliver=telegram catchup

| Option (after |) | Meaning | |---|---| | deliver=<dest> | stdout, log, telegram, or telegram:<chatID>. Default: this chat. | | tz=<IANA> | Per-job timezone, e.g. Europe/Berlin | | name=<label> | Human label (single token; default: first words of the task) | | catchup | Run a missed fire once on startup | | disabled | Add without enabling |

Notes:

  • Delivery defaults to the current chat (unlike the CLI, which defaults to stdout) — adding a job from a conversation sends its results back there.
  • /schedule run executes the task now, in this chat, through the normal agent pipeline (you see progress and answer any approval prompts) — a safe way to test a job. It does not change the job's own schedule or delivery.
  • Edits made from Telegram take effect immediately (the embedded scheduler reconciles on the spot, not on the ~30 s poll).
  • Only chats/users on the bot's allowlist (ODEK_TELEGRAM_ALLOWED_CHATS / ALLOWED_USERS) reach these commands.
  • Schedule management is further restricted to operator chats/users. Mutating commands (add, rm, enable, disable, run) are allowed only from the IDs listed in schedules.telegram_admin_chats / schedules.telegram_admin_users. These same IDs also authorize /restart. When neither list is configured, the bot falls back to telegram.default_chat_id; if that is also unset, mutating commands and /restart are rejected (read-only list/view/next still work). To keep schedule management CLI-only entirely, set schedules.allow_telegram_management = false.

Cron syntax

Standard 5-field Vixie cron:

┌ minute        0-59
│ ┌ hour        0-23
│ │ ┌ day-of-month 1-31
│ │ │ ┌ month   1-12 or JAN-DEC
│ │ │ │ ┌ day-of-week 0-6 or SUN-SAT (0 and 7 are both Sunday)
* * * * *

Each field accepts *, a value, a range a-b, a step */n / a-b/n / a/n, and comma-separated lists. Macros: @hourly, @daily (@midnight), @weekly, @monthly, @yearly (@annually).

Granularity is one minute (no seconds field). Times are in the job's --tz or, failing that, the scheduler's default timezone (UTC unless configured).

Day-of-month / day-of-week coupling follows Vixie semantics: when both fields are restricted, a day matches if either matches. So 0 0 13 * 5 fires on the 13th or any Friday — not only Friday the 13th.

odek schedule next "0 9 * * 1-5"   # validate an expression and see upcoming fires

Delivery

Kind Result goes to
stdout the daemon's stdout (or the bot's container logs)
log appended to ~/.odek/schedule.log
telegram the configured telegram.default_chat_id
telegram:<chatID> a specific chat

Telegram delivery needs telegram.bot_token and a chat ID (ODEK_TELEGRAM_BOT_TOKEN + ODEK_TELEGRAM_DEFAULT_CHAT_ID, or per-job telegram:<chatID>). When delivering from inside odek telegram, the live bot client is reused (shared rate limiting).


Safety: unattended tasks

A scheduled task runs with no human present to approve actions. The headless runner always applies a hard "deny" floor for prompt-class operations and clamps destructive, code-execution, install, system-write, network-egress, and unknown/blocked risk classes to deny — regardless of the configured dangerous profile. This prevents a compromised task definition from erasing files or exfiltrating data while unattended.

Read/summarise/deliver tasks work as usual. If you truly need a scheduled job that performs high-risk operations, run it interactively via odek run or the /schedule run command so an approver can review each action.

Task definitions in schedules.json are owner-authored (same trust level as config.json); the file is written 0600. Results written to ~/.odek/schedule.log are redacted for secrets before they hit disk.


Configuration

The schedules config section (in ~/.odek/config.json or ./odek.json) tunes the engine. Every field also has an ODEK_SCHEDULES_* environment override.

{
  "schedules": {
    "enabled": true,
    "max_concurrent": 2,
    "timezone": "UTC",
    "catchup": false,
    "allow_telegram_management": true,
    "telegram_admin_chats": [123456789],
    "telegram_admin_users": [987654321]
  }
}
Field Env Default Meaning
enabled ODEK_SCHEDULES_ENABLED true Run the embedded scheduler inside odek telegram
max_concurrent ODEK_SCHEDULES_MAX_CONCURRENT 2 Max jobs running at once
timezone ODEK_SCHEDULES_TIMEZONE UTC Default timezone for jobs without --tz
catchup ODEK_SCHEDULES_CATCHUP false Global default for the missed-run policy
allow_telegram_management ODEK_SCHEDULES_ALLOW_TELEGRAM_MANAGEMENT true Allow the in-chat /schedule commands to add/remove/toggle/run jobs (read-only listing always works)
telegram_admin_chats ODEK_SCHEDULES_TELEGRAM_ADMIN_CHATS [] Operator chat IDs that may use mutating /schedule commands
telegram_admin_users ODEK_SCHEDULES_TELEGRAM_ADMIN_USERS [] Operator user IDs that may use mutating /schedule commands

Missed runs

If the scheduler was down when a job was due, on startup it either skips (default — reschedules forward and records a skipped status) or runs once (when the job's --catchup or schedules.catchup is set). A burst of missed ticks never stampedes: at most one catch-up fire per job.