Scheduled Tasks
Supports three schedule types plus natural-language input. Tasks run at their configured execution position when due. In group chats, the default is chat top level, even when the task is created inside a topic. To continue in the current topic, explicitly pass --topic when creating the task through the CLI, or select the original topic under Execution position in the Dashboard.
There are four execution positions: chat top level (the default; all top-level tasks in one group share the chat-scope session context), a specified topic (--topic, bound to one existing topic), a new topic per run (--new-topic, every run stands alone), and a dedicated task topic (opt-in; each task owns its own topic session — see below).
Two Ways to Create
- Slash command (quick):
/schedule 每日17:50 帮我看看AI圈有什么新闻 - Conversational trigger (flexible): just tell the agent "add me a scheduled task to check deployment every day at 18:00", which automatically triggers the
botmux-scheduleskill.
To create a task that stays in the current topic, run this inside its topic session:
--topic can infer the anchor from the current topic session. Use --root-msg-id <om_...> to specify a target topic explicitly.
Cross-bot delegated creation
When a human explicitly asks Bot A to have Bot B create a schedule, A must request the persistent capability on the managed dispatch:
The host must also opt in through ~/.botmux/config.json (off by default):
Enable the identity wrappers on both the source orchestrator and target
generalist in bots.json. The target bot must govern both tools so a scheduled
turn cannot inherit an ambient login through an unmanaged tool:
The initial version is single-hop and bound to the target bot's current dispatch turn. It has no fixed five-minute deadline: multiple distinct tasks may be created while that turn is live, and the authority ends with the turn. A turn may create at most 64 tasks by default; maxTasksPerTurn accepts a host-configured limit from 1 through 1024. Each canonical request gets a deterministic task ID, so an identical retry returns the original task without consuming another slot. Tasks may run only in the original dispatch chat, at chat top level or in the current topic. --new-topic, --follow-active, multi-chat targets, and onward delegation are rejected. The dispatch grant authorizes creation only; optional task-local self-management is described below.
To make every managed dispatch from a selected orchestrator request the capability without changing each SOP, configure the source bot id:
Use --no-delegate schedule:create to opt out for one dispatch.
runScopes is empty by default. Setting it to ["bytedcli"] lets delegated tasks use the original human's bytedcli authorization on each future fire. Both source and target bots must enable triggerUserAuth, and the target must govern both lark-cli and bytedcli, so a reused session cannot inherit an earlier identity; creation or execution otherwise fails closed. This does not turn the scheduled turn into a general human current actor and never persists lark-cli authority.
With selfManageEnabled:true, a delegated scheduled turn may stop its current task using botmux schedule pause self or botmux schedule remove self. It cannot change the prompt or target, resume or force-run a task, manage another task, or create a successor. createEnabled:false stops new grants, runEnabled:false revokes future runs of existing delegated tasks, and removing runScopes or disabling self-management revokes those persistent capabilities independently.
The host SQLite store is authoritative for task definitions, grants, pause/completion state, and run claims; schedules.json is a rebuildable projection. The first upgraded start records the then-existing task inventory once as legacy. Later JSON additions, copies, or edits gain no execution authority. This boundary protects managed CLIs and the file sandbox; it does not claim to defend against a process running as the same OS user with unrestricted access to host keys and authority databases.
Consequently, writes from schedule add/update/remove/pause/resume/run must reach the owning bot daemon. If that daemon is unavailable or its authority store failed to initialize, the command fails explicitly instead of editing schedules.json and leaving behind a task that appears to exist but can never run.
Supported Formats
Bash Preconditions (Dashboard)
Enable Bash precondition when a scheduled task should check external state before calling the model. This setting is available when creating or editing a task in the Dashboard. Turning the switch off disables the precondition, but the scheduled task continues to run and the saved content is retained. Clear the script or file path and save to remove it.
Two source modes are available:
- Enter Bash directly: the script content is stored with the task configuration.
- Bash file path: the file must be a readable, regular UTF-8 Bash file on the daemon host and must be inside the
<dataDir>/schedule-preconditions/trusted-files/directory shown in the Dashboard for the current bot. The page also shows a complete absolute-path example that can be entered directly. Relative paths, paths using~expansion, directories, files outside this directory, and symbolic links in any path component are rejected.
File-path configuration demo
Suppose the Dashboard shows the following values for the current bot:
This is a Linux example. Your actual dataDir may differ, so use the values shown in the Dashboard instead of copying the example username. Then:
- On the same host that runs the daemon, create or copy a regular UTF-8 Bash file into the directory shown in the Dashboard. The daemon creates this directory.
- Paste the complete absolute path into Bash file path. For this example, enter
/home/alice/.botmux/data/schedule-preconditions/trusted-files/check-ready.sh, not~/.... - Click Test precondition. Save the task after the test passes.
You can start check-ready.sh with this minimal content:
Botmux validates and reads the file again for every test and scheduled trigger. You do not need to save the task again after changing the file.
Both modes use the same protocol. The script must exit with code 0, and trimmed stdout must be exactly 1, before the task calls the model. Output 0, any other output, empty output, or a script error stops that run.
To append context to the model prompt for this run, write it to file descriptor 3 (FD 3):
Click Test precondition to execute the current unsaved form content for real on the daemon host. The result shows pass, skip, or the full error and exit codes. Testing does not save the configuration, call the model, write a task execution log, or advance repeat counts. File, network, and other side effects produced by the script still happen for real.
Migrating existing configurations: File paths saved before this upgrade remain configured when they are outside the trusted directory, but tests and scheduled runs fail closed while the precondition is enabled, so the model is not called. Botmux does not copy the file or rewrite the task automatically. Move or copy the file into the current bot's trusted directory, update the field with the new complete absolute path, test it, and save. FD 3 content is sent to the model and may enter session history, so do not output secrets or tokens.
A New Topic Per Run
For chat-top-level execution, the bot/chat session mode determines how messages are organized. To explicitly make each run land in a brand-new topic in the same chat with its own isolated session (ideal for daily-report style tasks where each run should stand alone), there are three ways:
You can also edit a task on the Dashboard's Schedules page and use Execution position to choose the original topic, chat top level, a new topic for every run, or the task's dedicated topic.
A Dedicated Topic Per Task
"A new topic per run" keeps every run of the same task unrelated; chat top level goes the other way, with all top-level tasks in one group sharing one chat-scope session. A dedicated task topic gives each task a topic session that belongs to it alone:
- Every run of the same task lands in the same topic, so the context carries over — just like a topic-pinned task.
- Different tasks in the same group use separate topics and are isolated from one another, never sharing context — a sentinel task and a daily-report task never end up in the same session history.
- The topic is created on the first fire: a non-silent task posts a seed message (the "task started" banner) first, anchored as that topic's root; a silent task defers creation until the bot's first
botmux send, exactly like "new topic per run" combined with silent.
How to set it:
- Natural language: prefix the prompt with the dedicated-topic keyword —
独立话题/专属话题in Chinese, "dedicated topic" in English. It combines with the silent keyword in either order, for example:
- Lark schedule card / Dashboard: on the card, the execution-position button cycles chat top level → new topic per run → dedicated task topic → chat top level; the Dashboard Schedules form lets you pick the position directly.
Limits and notes:
- Single-group tasks only; a multi-group task cannot choose the dedicated task topic.
- The per-task model behaves exactly like
--topic: it applies only on the first fire that creates the session; later runs reuse the model that session started with (see the next section).
Follow the Active Topic
A task explicitly pinned with --topic keeps firing into its specified topic; once that topic is closed and the conversation has moved on, reminders land where nobody is looking — and re-lighting a closed topic is one more topic session to carry. --follow-active makes the task re-resolve its target at every fire.
Tried in order at every fire:
- The last landing point is still open and a human has spoken in it (some bot still holds an active session under that topic, and that session has seen a human message) ⇒ fire there, even if the person spoke elsewhere more recently. Until the first fire, the last landing point is the creation topic.
- Otherwise (closed; or open but only bots ever wrote there) ⇒ fire into the topic in this chat where a human most recently spoke, and record it as the new landing point. Only human messages count — bot replies and scheduled-task output are ignored, otherwise a task firing every 30 minutes would keep its own topic "most active" and follow itself forever. The lookup spans every bot: where the person is, is a property of the person, not of one bot. Note that this fire is injected into the person's live session, so the task's own
--workdirdoes not apply there; the session's working directory is used. - No open topic with human activity in this chat, but the last landing point is still open (e.g. the topic step 4 opened, with only the task writing in it) ⇒ stay there; no new topic is opened.
- Nothing open at all ⇒ this fire opens a fresh top-level topic, exactly like
--new-topic, and records it as the new landing point — the next fire stays in it under step 3, and once the person replies there it is held under step 1.
Step 1 requiring a human is deliberate: a topic the task opened itself must not pin the task, so a bot-only landing point yields to any topic where a person is. If the session records cannot be read (e.g. sqlite temporarily unavailable) the task does not move and keeps its last landing point. A silent task reaching step 4 gets a deferred topic (created by the first botmux send), which is not recorded; the next fire re-resolves.
Cross-topic notice: a plain topic-pinned task that fires somewhere other than its creation topic posts a "delivered to …" notice back into the creation topic. A follow-active task moves by design, so it posts that notice only when it fires into a different chat; a move within the same chat just posts the "task started" banner in the topic it landed in.
--follow-active only makes sense for topic execution and cannot be combined with --top-level / --new-topic. Tasks of this kind show a ↷跟随活跃话题 marker in schedule list.
A Model Per Task
Under one bot, different tasks usually deserve different model tiers: a sentinel firing every 30 minutes runs fine on a cheap model, while the nightly code review is the one that needs the strongest. --model / --reasoning-effort give a task its own model without touching the bot config or any other task.
The Dashboard "Schedules" page has both fields too; empty means "follow the bot config".
Only a run that creates a session can apply the model. Model and reasoning effort are CLI process launch arguments (codex --model X -c model_reasoning_effort=Y) and cannot be changed once the process is up. So:
Use --new-topic when it must apply on every run. Both the CLI and the Dashboard say which case you are in when you save.
Supported CLIs are Codex, Claude Code, Grok and TraeX (the same gate as the trigger API's options.model); a task on any other CLI ignores both fields at fire time and logs a warning. Which effort levels exist depends on the model (gpt-5.6-sol goes up to ultra, gpt-5.5 stops at xhigh), and the Dashboard rejects an unsupported pairing on save. If the bot later switches CLI, or the model stops offering the level, the fire drops that field and runs anyway with a warning — stale configuration never skips a run.
Management
Execution behavior: the execution position determines the target first. With an explicit
--topic, an active session in the target topic receives the prompt directly (no new worker); otherwise, a new worker starts in the task's saved working directory. Chat-top-level tasks select a session according to the bot/chat session mode.--new-topicuses a fresh session for every run; combined with--silent, it creates the topic only when the firstbotmux sendneeds to deliver content. A dedicated task topic creates the task's own topic on its first fire (a non-silent run posts a seed message anchored as the root; a silent run defers materialization to the firstbotmux send), and every later run is appended to that same session.
Update a prompt in place
Use update to change an existing task without deleting and recreating it:
Files are read as UTF-8 with line breaks preserved. Only the prompt changes: the task ID, schedule, enabled state, execution position and run history remain intact. Updating does not trigger a run. Already dispatched runs keep their original prompt; subsequent runs use the replacement. Input or authorization failures do not delete the task. Updates use the same locked atomic storage as other task operations.
For ownerless topics created by scheduled tasks, the host daemon authorizes the current caller without exposing bot configuration or credentials to the sandbox. Upgrade the CLI and daemon together: an older daemon without this authorization capability is rejected explicitly, without deleting the task or bypassing verification.
A task bound to a protected precondition cannot be changed with update: the precondition records a hash of the task input, while its definition file is host-only and cannot be rebound from inside the sandbox. Rewriting the prompt would make every later fire fail validation and stop the task silently. Edit such tasks on the Dashboard scheduled-tasks page instead.
Custom Work Calendar
Bind an independent named calendar to a recurring task and choose Workdays only or Rest days only. Tasks without a calendar keep their original behavior. Existing bindings default to workdays. The scheduler reads local data; it does not query online APIs or ask a model to decide holidays.
Calendar IDs are stable configuration keys; display names follow the Dashboard language. Calendars have their own time zone, confirmed coverage, normal workweek and date overrides. Region is optional descriptive metadata. Built-in and custom calendars use the same validation and admission logic. Add future built-in calendars in src/services/work-calendars/catalog.ts with verified data; add custom calendars to the Bot's local file without changing code.
China Statutory Work Calendar
The built-in calendar displays China Statutory Work Calendar / 中国法定工作日历, with ID cn and time zone Asia/Shanghai. It represents the nationwide mainland China holiday and makeup-workday arrangement. Confirmed coverage is 2026-01-01 through 2026-12-31. No local calendar file is required:
The authoritative source is the State Council General Office notice for 2026, document 国办发明电〔2025〕7号, published 2025-11-04. The Beijing government republication provides the full notice. The repository snapshot src/services/work-calendars/cn-2026.json and schedule calendars expose provenance, scope and version metadata.
The calendar starts with Monday–Friday and applies the official holiday and makeup-day overrides. It does not represent every employer's attendance rules. Regional extra holidays, holidays for specific groups, company shifts, Hong Kong, Macau, Taiwan and other countries are outside the initial built-in scope. A time zone does not select a region.
Annual data is verified against the official notice, updated with date tests, and shipped with a release. Dates outside confirmed coverage, including unverified 2027 data, fail closed with calendar_out_of_coverage; the scheduler does not infer the next year or reuse the previous year's exceptions. Upgrade to verified next-year data before crossing the coverage boundary.
The built-in ID cn is reserved and cannot be overridden by a local file. A missing or damaged local extension file does not affect cn; missing custom IDs never fall back to it.
Custom Calendar Data
Save work-calendars.json beside the Bot's schedules.json, normally at ~/.botmux/bots/<appId>/work-calendars.json. Calendars are isolated per Bot. Keep your source data under version control and replace the deployed file atomically; the next check reloads it without restarting the daemon.
This is synthetic demonstration data, not a statutory holiday arrangement:
All rule fields are required except displayNames. Dates must be valid and within coverage. Duplicate dates or weekdays, conflicting rest/work dates, invalid time zones and reversed coverage are rejected. Maximum file size is 1 MiB. An invalid definition does not affect other valid definitions; a malformed file blocks automatic tasks bound to its local calendars. Built-in calendars remain available.
Add other independent IDs such as company-shifts to the same map, using their own time zone, workweek and overrides. Reserve your own IDs for company data rather than modifying the official built-in calendar.
Binding, Display and Execution
none is reserved for removing the binding. Edit tasks with a protected Bash precondition in the Dashboard so its canonical-input binding is updated together with the task.
The Dashboard's Custom Work Calendar section lists built-in and local calendars for the selected Bot. Choose Workdays only or Rest days only, or remove the binding. Rest days include confirmed nonworking weekdays, ordinary weekends without a makeup override, and holidays. Unknown dates are not eligible rest days.
Create/update API requests use calendar: "cn" and calendarDayType: "workday" | "restday"; workday is the default. Updating calendar: null clears the binding. Rows expose calendarCheck, lastCalendarCheck and nextEligibleRunAt alongside the original nextRunAt.
The eligible-next-run preview only filters existing cron/interval candidates. It does not create weekend triggers or guarantee precondition, model or delivery success. It scans at most 10,000 candidate local dates and returns null with an explicit reason when it cannot confirm a date. Use a daily cron + calendar to include weekend makeup days; a Monday–Friday cron has no weekend candidates.
A CLI manual request is persisted and consumed once by the owning Bot. A request surviving downtime beyond the catch-up grace window executes once after restart, then subsequent automatic occurrences check the calendar normally. Pausing cancels any unconsumed queued manual request. CLI run requests on paused tasks are rejected with a prompt to resume first. Resuming a paused task also clears legacy pending requests; repeating resume on an enabled task does not defer its valid pending request. Changing the schedule cancels an unconsumed manual request associated with the old rule; automatic occurrences under the new rule check the calendar. Editing only other fields or saving the same rule preserves a valid pending request. Dashboard Run now executes directly for that invocation; it does not queue a future bypass or resume the automatic schedule.
A nonmatching date records calendar_skipped / lastStatus: skipped without consuming finite repeat counts. Missing, invalid or out-of-coverage calendar data records an error and blocks execution without consuming counts or automatically disabling the task. Fixing the data allows later eligible candidates. Unbound tasks remain unaffected.
Calendar dates do not override a business worker's own weekday checks. If a worker hardcodes weekday < 5, that separate filter must be removed before weekend makeup runs work end to end. Use a separate unbound task for compensation or other workflows that must run across nonworking dates.
