Skip to content

Latest commit

 

History

History
270 lines (214 loc) · 11.7 KB

File metadata and controls

270 lines (214 loc) · 11.7 KB

Scheduled Tasks (native cron)

odek can run agent tasks on a cron schedule and deliver each result somewhere — a Telegram chat or stdout. 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 compatibility alias for daemon stdout (redacted)
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 forces non_interactive to deny and applies a non-overrideable safety floor: the destructive, blocked, persistence (deferred-execution writes: shell profiles, git hooks, cron, CI lifecycle scripts), and unread_exec (executing a script whose contents were not read in the session) classes are always denied. Schedule-specific policy in schedules.dangerous can allow or deny the remaining classes — network_egress, system_write, code_execution, install, and unknown are the ones an unattended run can be granted — but the floor itself cannot be lifted. This prevents a compromised task definition from erasing files, installing persistence, or running unreviewed scripts 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. Delivery output is redacted before it is written to stdout. The unified runtime log never stores that output.


Configuration

The schedules config section (in ~/.odek/config.json; project ./odek.json may only set enabled/timezone) tunes the engine. Every field also has an ODEK_SCHEDULES_* environment override. Project files cannot raise max_concurrent, set catchup, toggle Telegram management, or replace telegram_admin_* lists.

{
  "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

Unattended danger policy (schedules.dangerous)

The dangerous subsection overrides the base dangerous policy for scheduled runs — same schema (per-class actions, allowlist, denylist, action, non_interactive), overlaid on the global config before the safety floor is applied. It is operator-only: project-level ./odek.json cannot set it.

{
  "schedules": {
    "dangerous": {
      "classes": { "network_egress": "allow", "system_write": "deny" },
      "allowlist": ["go test ./...", "go build ./..."]
    }
  }
}

Every field also has an ODEK_SCHEDULES_DANGEROUS_* environment override:

Env Format
ODEK_SCHEDULES_DANGEROUS_CLASSES JSON object, e.g. {"network_egress":"allow","system_write":"allow"}
ODEK_SCHEDULES_DANGEROUS_ALLOWLIST Comma-separated command strings
ODEK_SCHEDULES_DANGEROUS_DENYLIST Comma-separated command strings
ODEK_SCHEDULES_DANGEROUS_ACTION Global default action: allow, deny, or prompt
ODEK_SCHEDULES_DANGEROUS_NON_INTERACTIVE allow, deny, or read_only (ignored: scheduled runs force deny)

See CONFIG.md for the full field reference.


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.