cron-task-creator · git:20260610.0aabf15 · 2026-06-10 · sha256 94d90ff09fe4e27a

cron-task-creator git:20260610.0aabf15B

Immutable. This exact content is served forever at /api/v1/blob/94d90ff09fe4e27a.

---
name: cron-task-creator
description: Create, inspect, run, enable/disable, and delete octo's scheduled cron tasks — recurring agent prompts stored in ~/.octo/tasks/*.json and executed by the octo serve scheduler. Use when the user wants to schedule a recurring task, e.g. "run X every morning", "schedule a daily report", "set up a cron job", "定时任务", "每天自动跑".
---

# Create and manage octo cron tasks

octo can run an agent prompt on a schedule. Each scheduled task is a JSON file
in `~/.octo/tasks/`, loaded by the scheduler inside `octo serve`. When a task
fires, the scheduler runs one agent turn with the task's prompt (30-minute
timeout) and reuses the same session across runs, so the task accumulates
history from previous executions.

## Task schema

| Field | Required | Meaning |
|-------|----------|---------|
| `name` | yes | Human-readable task name (also addressable via the API) |
| `cron` | yes | Schedule expression — see format below |
| `prompt` | yes | The prompt sent to the agent on each run |
| `model` | no | Model override; defaults to the server's model |
| `agent` | no | `"general"` or `"coding"` |
| `directory` | no | Working directory hint, prepended to the task session's system prompt |
| `notify` | no | IM chats to push each run's final reply (or a failure note) to: `[{"platform": "feishu", "chat_id": "oc_..."}, {"platform": "weixin", "chat_id": "..."}]` — every entry gets the push. A bare object (single target) is also accepted. |
| `enabled` | yes | Whether the schedule is active |

## Cron expression format — 6 fields, seconds first

The scheduler uses 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 interpreted in the server's local timezone.

## Workflow

1. **Gather** the schedule, the prompt, and any optional fields. If the user
   gave a vague schedule ("every morning"), pick a concrete time and confirm.
2. **Translate** the schedule to a 6-field expression and **echo it back in
   plain words** ("every weekday at 18:30") before creating anything.
3. **Write a self-contained prompt.** The task session has no access to this
   conversation — the prompt must carry all context: what to do, where, and
   what the output should look like.
4. **Give the prompt an explicit stop condition.** Task runs are capped at
   100 turns and 30 minutes; an open-ended prompt makes the model keep
   re-verifying instead of finishing (a real "check for new issues" task spent
   18 minutes and all 100 turns re-confirming that zero issues existed). Spell
   out when the task is done, especially for the empty case:
   - Bad: "Check the repository for any new open issues that need attention."
   - Good: "List open issues created in the last 24h via one
     `gh issue list` call. If there are none, reply exactly 'no new issues'
     and stop. Otherwise summarize each in one line and stop — do not
     re-check."
5. **Create** the task (see below), then **verify** by listing tasks. Offer a
   one-off immediate run to test.

## Creating a task

**Preferred — via the running server.** If `octo serve` is up (default
`:8080`), POST to the API; the task is registered and starts firing
immediately:

```bash
curl -s -X POST http://127.0.0.1:8080/api/tasks \
  -H 'Content-Type: application/json' \
  -d '{"name":"daily-report","cron":"0 0 9 * * *","prompt":"Summarize ..."}'
# → {"id":"task_1717999999999"}
curl -s http://127.0.0.1:8080/api/tasks   # verify
```

**Fallback — direct file write.** If the server is not running, write
`~/.octo/tasks/<id>.json` with `write_file` (id format: `task_<unix-millis>`,
filename must equal `<id>.json`):

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

The file is picked up the next time `octo serve` starts.

## Editing a task

**Always use the API.** After you edit a task file with `edit_file` or `write_file`,
you **must** immediately PATCH the same change through the API so the running
scheduler picks it up. File edits alone are ignored until the next restart.

```bash
# 1. Edit the file (example: change the prompt)
edit_file ~/.octo/tasks/task_1781090471651.json  # or write_file

# 2. PATCH the same field via API so the change takes effect NOW
curl -s -X PATCH http://127.0.0.1:8080/api/cron-tasks/<name-or-id> \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"new prompt ..."}'

# Verify
curl -s http://127.0.0.1:8080/api/tasks
```

Supported PATCH fields: `enabled`, `cron`, `prompt`, `model`, `agent`, `directory`, `notify`.

## Other operations

```bash
curl -s http://127.0.0.1:8080/api/tasks                          # list
curl -s -X POST   http://127.0.0.1:8080/api/tasks/{id}/run      # run now
curl -s -X DELETE http://127.0.0.1:8080/api/tasks/{id}          # delete
curl -s -X PATCH  http://127.0.0.1:8080/api/cron-tasks/{name} \
  -H 'Content-Type: application/json' -d '{"enabled":false}'    # disable
```

## Caveats — tell the user when relevant

- **Tasks only fire while `octo serve` is running.** No daemon, no serve → no
  runs. Missed schedules are not replayed on restart.
- **API changes take effect immediately; file edits don't.** Create, update,
  enable/disable, and delete through the API reschedule the running process on
  the spot. Editing a JSON file under `~/.octo/tasks/` by hand only takes
  effect the next time `octo serve` starts — prefer the API whenever the
  server is up.
- **Validate before creating.** A malformed cron expression is rejected at
  creation time by the API, but a hand-written JSON file with a bad expression
  fails silently at load (logged to stderr only) — double-check the 6-field
  format when writing files directly.
- **IM notification (`notify`) — per-platform `chat_id` rules.** All three
  platforms can be pushed to; a failed push is logged on the server and never
  affects the run.
  - **Feishu**: works with app credentials alone (`~/.octo/channels.yml`);
    `chat_id` looks like `oc_…` — get it from the chat's settings or by
    messaging the bot and reading the server log.
  - **DingTalk**: pushes via the proactive robot APIs. `chat_id` is a staff
    id (one-on-one) or a `cid…` openConversationId (group) — a DM's
    conversation id does NOT work, use the user's staff id. Requires the
    "robot message send" permission on the app in the DingTalk admin console.
  - **Weixin**: `chat_id` is the iLink user id, and the user must have
    messaged the bot at least once (the receive loop persists each user's
    latest `context_token` to `~/.octo/weixin-contexts.json`, which the push
    reads; a long-stale token may be rejected by WeChat — chatting with the
    bot refreshes it).
  - **Telegram**: `chat_id` is the Telegram chat id (user, group, or
    channel); the bot must be able to message it (user has started the bot,
    or bot is a member of the group/channel).
  - **Discord**: `chat_id` is the channel id; the bot needs the Send
    Messages permission in that channel.
  - **WeCom**: pushes go through a group-robot webhook — set `webhook_key`
    (or full `webhook_url`) in the channel config. The webhook is bound to
    one group, so `chat_id` is ignored for pushes (use the group name as a
    label).