Skip to content

Schedule cron tasks

A cron task is a recurring agent prompt — “check the queue every 5 minutes”, “write a daily report at 9am” — that fires on its own while octo serve is running, with nobody watching.

Terminal window
octo serve

Each task is a JSON file in ~/.octo/tasks/, loaded by a scheduler that runs inside octo serve. When a task fires, the scheduler runs one agent turn with the task’s prompt. Each run is bounded by a 30-minute wall-clock timeout — the only hard cap.

Tasks only fire while octo serve is running. No serve, no runs — and a schedule missed while the server was down is not replayed on restart.

Field Required Meaning
name yes Human-readable task name
cron yes Schedule expression — see below
prompt yes The prompt sent to the agent on each run
model no Model override; defaults to the server’s model
agent_id no Id of the expert the run executes as; empty = the Default Agent
directory no Working directory the run executes in
notify no IM chats to push each run’s final reply (or failure) to
enabled yes Whether the schedule is currently active

The prompt runs in its own session with no access to whatever conversation created the task, so it needs to be self-contained: what to do, where, and what the output should look like. Give it an explicit stop condition too — an open-ended prompt keeps the model re-verifying until the 30-minute timeout instead of finishing once the answer is “nothing to report.”

Every run creates a new session, titled with the run’s local date and time (e.g. 2026-07-22 15:04) and starting from an empty transcript — runs never share a session.

Each task also gets its own project, named after the task and working in <workspace_dir>/<task name> (so ~/Octo/daily report by default), created the first time it is needed. That is where the task’s runs execute their tools, and — because memory is scoped by project — where the task’s own notes accumulate, separate from every other task on the machine. Set directory on the task to point it somewhere else instead.

The directory is derived from the name once and then belongs to the project: renaming the task renames the project, not the directory. Moving it would either strand whatever the task has already written there or have to relocate it, and neither is what renaming asks for.

The project appears in the sidebar’s Projects section like any other, marked with a small clock, and can be renamed, configured, or deleted from there. Deleting it leaves the run sessions on disk as ordinary tasks; the next run creates the project again.

Cron expression — 6 fields, seconds first

Section titled “Cron expression — 6 fields, seconds first”

The scheduler is robfig/cron with a seconds field — a standard 5-field crontab line is invalid here; always prepend a seconds field:

seconds minutes hours day-of-month month day-of-week
Want Expression
Every day at 09:00 0 0 9 * * *
Every 30 minutes 0 */30 * * * *
Weekdays at 18:30 0 30 18 * * 1-5
1st of each month at 08:00 0 0 8 1 * *

Descriptors also work: @hourly, @daily, @weekly, @every 90m. Times are in the server’s local timezone.

Click the edit button on a task in the scheduler panel. The new session opens with an edit form prefilled with the task’s name, cron expression (restated in plain words), prompt, model, directory, and enabled state.

  • Change the fields you want and save — the change takes effect right away. A bad or sub-hourly cron expression is rejected, not saved.
  • Or describe the change in the box at the bottom of the form (“move it to 8am on weekdays”) and the agent makes it. Notifications and the expert a task runs as are changed this way too.

A field too long to fit the form without being cut off (a very long prompt, say) is left out of it — describe that change in the bottom box instead.

Every change through the API reschedules the running process immediately — the recommended path whenever octo serve is up.

Terminal window
# Create — returns {"id":"task_..."}. Any optional field (directory, model,
# agent, notify) goes right in the create body.
curl -s -X POST http://127.0.0.1:8088/api/tasks \
-H 'Content-Type: application/json' \
-d '{"name":"daily-report","cron":"0 0 9 * * *","prompt":"Summarize ...","directory":"/srv/repo"}'
curl -s http://127.0.0.1:8088/api/tasks # list
curl -s -X DELETE http://127.0.0.1:8088/api/tasks/{id} # delete
# Run now, out of schedule
curl -s -X POST http://127.0.0.1:8088/api/tasks/{id}/run
# Edit any subset of fields — this is also how you enable/disable
curl -s -X PATCH http://127.0.0.1:8088/api/tasks/{id} \
-H 'Content-Type: application/json' \
-d '{"prompt":"new prompt ...","enabled":false}'

PATCH /api/tasks/{id} accepts name, enabled, cron, prompt, model, agent_id, directory, notify — send only what you’re changing; renaming via name also renames the task’s project (its directory stays put). The Web UI’s scheduler panel is a client of this same API, so a task created by curl shows up there and vice versa; the panel is also the recommended place to smoke-test a new task’s Run button rather than triggering /api/tasks/{id}/run from a chat session — a run is a full agent turn (up to 30 minutes) in the task’s own session, so firing it from a conversation just blocks that conversation while the actual output lands somewhere nobody is watching it.

Write ~/.octo/tasks/<id>.json directly (id format task_<unix-millis>; filename must equal <id>.json):

{
"id": "task_1717999999999",
"name": "daily-report",
"cron": "0 0 9 * * *",
"prompt": "Summarize ...",
"directory": "/srv/repo",
"enabled": true,
"created_at": "2026-06-10T09:00:00Z"
}

The file is picked up the next time octo serve starts. A hand-written file with a bad cron expression fails silently at load (logged to stderr only). File edits made while the server is already running are ignored until restart — once it’s up, go through the API instead.

A task that collects something each run — a price, a count, new items — can append it to a named database with the sqlite tool (a SQLite file at ~/.octo/databases/<name>.db, created on first use). A Light App can then read the same database by name and chart it; the task never needs to know which app shows the data.

If the job needs no judgment on each run — fetch a page, parse it, insert the rows — ask the agent to write it as a script for your OS scheduler (cron, launchd, Windows Task Scheduler) instead. It costs no tokens per run and runs even while octo is closed. The script writes the same database, and the agent has it record each run in a runs table so the app can show when it last ran and whether it failed — an OS job that fails is otherwise silent.

notify is a list of IM targets (a single bare object is also accepted); every entry gets pushed the run’s final reply on success, or a short failure note on error. A failed push is logged on the server and never affects the run itself.

Platform chat_id Notes
feishu oc_… chat id Needs app creds in channels.yml; get the id from chat settings or the server log after messaging the bot
dingtalk staff id (1:1) or cid… conversation id (group) A DM’s conversation id does not work — use the staff id
weixin (iLink) user id User must have messaged the bot at least once
telegram chat id (user/group/channel) Bot must already be able to message it
discord channel id Bot needs Send Messages permission there
wecom ignored Pushes go through a group-robot webhook bound to one group instead

Next: for a shorter-lived, in-conversation repeat that doesn’t need to survive a restart, see /loop instead.