Claude Code Tools 研究系列(十二)—— Cron 家族:把动作安排到未来
Claude code tools 研究系列第十二篇。前十一篇拆完了 Claude Code 的空间维度工具集 —— 从本地文件系统到互联网 · 从单 Claude 到多 Claude · 从当下动作到待办清单。所有工具的时态本质上是「同步」 —— Claude 调 tool,立刻执行,立刻返回。
但真实工程里有一类需求这套体系解决不了:
- 「30 分钟后提醒我 check 一下 CI」
- 「每 5 分钟看看部署好了没」
- 「明天早上 9 点跑一遍晨间自检」
- 「等一个小时后,重新审阅一下我的这份方案」
这些需求的共同点:动作不是「现在做」· 是「未来某个时刻自动被触发」。
这需要时间原语。Claude Code 的答案是 Cron 家族 —— 3 个工具(CronCreate / CronDelete / CronList)组成的定时调度系统。
本系列先读 前置篇 —— 讲清楚 tool 是什么、Claude 怎么用。本篇按前置篇提出的 4 层骨架展开。
Cron 家族(CronCreate / CronDelete / CronList)
跟第十篇 Task 家族一样,3 个工具语义高度耦合 · 共享同一数据模型(session 内的 cron jobs 列表) · 合并写更利落。
家族概览
| 工具 | 职责 |
|---|---|
| CronCreate | 创建一个未来触发的 prompt · 用标准 5 字段 cron 表达式 |
| CronDelete | 取消一个已调度的 job |
| CronList | 列出当前 session 里所有调度中的 jobs |
「亲戚工具」:除了 Cron 三件套,系列里还有一个相关的 ScheduleWakeup —— 专门给 /loop skill 的动态模式用,安排下一次自唤醒。它的定位是 Cron 家族的特化版本(为循环任务优化),这一篇顺带提一下。
核心分工:
- CronCreate(引擎)—— 90% 的调用集中在这里
- CronList / CronDelete(管理)—— 看进度、清理
跟 Task 家族最大的不同在于:Task 家族记「待办的事」· Cron 家族安排「未来的动作」。Task 是等 Claude 有空时来做 · Cron 是到时间自动触发 —— 更主动、更精确。
作用
Cron 家族解决的核心问题是「Claude 如何跨越时间执行动作」:
- 打破同步束缚 —— Claude 不再只能「有请求 → 回应」· 可以「安排一个未来的自我唤醒」
- 精确调度 —— 用标准 cron 语法(
M H DoM Mon DoW)· 灵活到任意时刻或任意周期 - 一次性 / 重复两种模式 —— 用
recurring布尔切换 - 轻量提醒 —— 不用起 background task 就能实现「30 分钟后 remind」
- 主动感知 —— 等外部状态时(CI / 部署)· 主动到点检查
它跟前面所有工具的关键差异:这是唯一能「跨越时间」的工具家族。
前十一个工具都是「点」上的动作 —— tool call 触发就执行完毕。Cron 家族是「线」上的调度 —— 在时间线上标一个点 · 到了自动引爆。
一个具体例子
场景:用户说 「我刚推了个部署 · 大概 8 分钟出结果 · 你等好了帮我看下 CI 状态 · 有问题告诉我」。
这是一个典型的「等外部状态变化」 任务。Claude 有几种做法:
反例 1:纯 sleep
1 | Bash(command: "sleep 480 && gh run list", timeout: 500000) |
问题:主 Claude 被 sleep 阻塞 8 分钟 · 期间没法跟用户对话 · 用户想问别的都得等。同步阻塞浪费了对话时间。
反例 2:每分钟循环 poll
1 | while true: |
问题:每分钟消费一次上下文 · 8 分钟就是 8 次 · 主 Claude 的 context 被日志灌满 · 上下文浪费。
用 CronCreate 是怎么解决的
Claude 调 CronCreate,安排一次 8 分钟后的一次性唤醒:
1 | CronCreate( |
运行时会发生什么:
- Runtime 把这个 job 记下来(session 内存里)
- 主 Claude 立即回到用户 —— 不阻塞
- 用户可以问别的 · 让 Claude 干别的
- 到 22:13 · runtime 自动把
prompt作为一次新的 Claude 调用触发 - Claude 拿到 prompt · 跑
gh run list· 汇报状态
对用户来说,体验是:
1 | [22:05] 用户: 我刚推了部署 · 8 分钟后帮我看下 CI |
关键洞察:CronCreate 把「等待」从主 Claude 的责任变成runtime 的责任。主 Claude 完成安排就撤,不占对话时间也不占 context。
组合用法:CronList 看进度 · CronDelete 提前取消
如果用户忽然说「算了不用等 CI 了 · 我自己看」,Claude 可以:
1 | CronList() # 拿到之前那个 job 的 ID |
或者用户问「你安排了什么任务」· Claude CronList 一下就能答。
「主动」vs「被动」两种唤醒模式
Cron 家族有两种典型使用模式:
一次性 (recurring: false)
用于已知时刻的动作:
- 「明天 9 点提醒我 review 这份 PR」
- 「30 分钟后再看一次 CI」
- 「12:00 到点吃饭提醒」
Cron 表达式里 minute / hour / dom / month 都固定 · 到时间触发一次就消失。
周期性 (recurring: true)
用于未知截止时间的监控:
- 「每 5 分钟检查一次 CI · 直到我说停」
- 「每小时看一下队列长度」
- 「每天早上跑一遍晨间自检」
用 */5 * * * * / 0 * * * * / 0 9 * * * 这类表达式。注意 recurring 任务最多存活 7 天 · 到期自动最后一次触发后删除。这个上限是防漂移设计:防止 session 结束后 job 还在(实际上不会 · 见下文技术实现)· 也防止 job 无限存活占资源。
触发条件
该用 Cron 的场景:
- 等外部异步事件 —— CI / 部署 / 长任务
- 提醒 / 到点执行 —— 「X 时候做 Y」
- 周期性监控 —— 「每 N 分钟 check 一次 X」
- 对话已结束但想让 Claude 未来自己接手 —— 一次晨间自检
不该用 Cron 的场景:
- 秒级 / 亚秒级动作 —— cron 分辨率是分钟 · 太快用 sleep
- 需要精确响应外部事件 —— 用 Monitor tool(比 cron 更贴合「等某事发生」)
- harness 已经会自动通知的等待 —— 比如 background bash / subagent 完成 harness 会通知 · 不用 poll
- 跨 session 的持久任务 —— session-only! cron job 不写盘 · Claude 一退出就没了
跟其他等待原语的分工:
| 需求 | 用什么 |
|---|---|
| 一次性事件通知(CI 完成) | Bash run_in_background(harness 自动通知) |
| 无固定时间点的事件监听(文件改动) | Monitor |
| 到点提醒 / 一次性延迟 | CronCreate + recurring: false |
| 周期性监控 | CronCreate + recurring: true |
| /loop skill 里的自唤醒 | ScheduleWakeup(特化版本) |
这张表很关键 —— 等待原语不止 Cron 一个 · Claude 应该按语义选。
技术实现
1 · 命名
CronCreate / CronDelete / CronList
三件套是又一组对偶闭环 —— Create 挂上、Delete 摘下、List 观察。定时任务的生命周期是「创建 → 存在 → 到期或被删」,需要「观察当前状态」和「主动取消」两个反向动作,所以 3 件而不是 2 件。
「Cron」这个词本身是借用 —— 不自造 DSL,直接沿用 Unix crontab 40 年的行业约定。用户在自己的 Linux/macOS 终端里写过 crontab -e 就懂,不用再学一套语法。复用行业约定、减少认知门槛是这个命名的核心设计。同样 List 用复数而不是 Get · 暗示返回多条。
2 · 工具级描述
Cron 家族的描述围绕六件事:session-only 生命周期 / 7 天上限主动告知 / 负载分散(避开 :00 和 :30) / 何时反而应该用 :00/:30 / 不用 Cron 的场景 / 一次性 vs 循环的语言信号 / 本地时区语义 / 抖动机制透明。
Session-only 明示 · 生命周期开篇就说
Jobs live only in this Claude session — nothing is written to disk, and the job is gone when Claude exits.
开篇就把重大约束说清楚 —— 让 Claude 不至于在 tool call 之后跟用户说「已安排每周一次」这种做不到的事。透明比华丽重要。这条约束背后是 Anthropic 的设计选择:持久 cron 要处理用户权限验证、错误处理、多 session 状态同步,复杂度爆炸。选简化路径 —— cron 只是 session 内的定时器,用户能全权控制。代价是长期任务(几天几周)Cron 家族做不到,得靠系统级 cron / 云服务。
7 天上限 · 主动告知用户
Recurring tasks auto-expire after 7 days — they fire one final time, then are deleted. This bounds session lifetime. Tell the user about the 7-day limit when scheduling recurring jobs.
要求 Claude 主动告知用户 7 天上限。不是被动回答问题,是主动预告约束。这是「诚实的默契」—— Claude 帮用户设时不能藏着掖着。7 天上限本身是防遗忘设计:用户可能建了个「每小时监控」然后忘了,这个上限保证不会永久占资源。一次性任务不受此限(反正只 fire 一次)。
「负载分散」意识写进 prompt · 避开 :00 和 :30
Every user who asks for “9am” gets
0 9, and every user who asks for “hourly” gets0 *— which means requests from across the planet land on the API at the same instant. When the user’s request is approximate, pick a minute that is NOT 0 or 30
这是把「系统级负载分散」写进 tool prompt 的稀有设计 —— 一般 tool 只关心 Claude 使用行为,不管服务器压力。Cron 例外,因为它是唯一一个可能造成用户不察觉的定期请求的 tool。所有用户对「9 点」的直觉都是「9:00」,所有请求都会挤在同一秒,Anthropic 后端会同时被打(负载尖峰)。让 Claude 自动挑一个偏移分钟(如 :57 或 :03),请求分散,后端稳定。
明确解释理由 · 不只是「按这规则来」—— Claude 理解规则背后的意图,才能在边界情况下自行判断。
何时反而应该用 :00 / :30 · 给出反例
Only use minute 0 or 30 when the user names that exact time and clearly means it (“at 9:00 sharp”, “at half past”, coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.
给出反例 —— 明确什么时候用 :00 是对的(用户明确要求或有会议对齐)。避免 Claude 教条化:规则有例外,exception 也写清楚。
不用 Cron 的场景 · 指路 Monitor
Not for live watching. CronCreate re-runs a prompt at fixed wall-clock intervals. To watch a log file, process, or command output and be notified the moment something changes, use the Monitor tool instead — Monitor streams events as they happen; cron polls on a schedule.
明确告诉 Claude 别拿 Cron 当 Monitor 用。tool description 里直接指路兄弟 tool,而不是指望模型自己去比对多个工具。这也是「工具间协作契约写进单个工具描述里」的又一个例子。
一次性任务的判断 · 从用户语言反推
For “remind me at X” or “at
给出 recurring: false 的具体触发信号 —— 「remind me at X」/「at
本地时区语义 · 避免 UTC 换算
Uses standard 5-field cron in the user’s local timezone: minute hour day-of-month month day-of-week. “0 9 * * *” means 9am local — no timezone conversion needed.
明确本地时区语义,避免 Claude 手动做 UTC 转换。这类「习惯误区」写进 prompt 是从血泪教训里长出来的 —— 老一辈 sysadmin 都遇过时区搞错的坑。用起来跟用户在自己终端里写 crontab 一样直觉。
抖动机制透明
The scheduler adds a small deterministic jitter on top of whatever you pick
告诉 Claude 有 jitter —— 让 Claude 别以为「我写了 :57 结果 :58 fire,是不是有 bug」。透明化让 Claude 建立合理预期。Jitter 具体规则:周期性任务实际触发最多延迟 10%(上限 15 分钟);一次性任务写在 :00 或 :30 时,自动提前最多 90 秒 fire。又是负载分散 —— 就算 Claude 教条化选了 0 9 * * *,runtime 也会加抖动让请求分散。
REPL idle 才触发 · 保护 Claude 不被打断
Jobs only fire while the REPL is idle (not mid-query).
如果 cron 到期时 Claude 正在处理另一个用户 prompt,触发会延迟到当前处理完。这防止 cron 和用户 prompt 撞车打断 Claude 思路。
3 · 字段级描述
CronCreate 的字段清单:
cron—— 5 字段表达式(local timezone):"minute hour day-of-month month day-of-week"prompt—— 到时间要触发的 prompt 内容recurring—— 布尔 · 默认true(重复)·false为一次性durable—— 遗留字段 · 无实际效果
CronDelete 只要一个 id(CronCreate 返回的);CronList 无入参。真正有意思的字段设计都在 CronCreate。
几个关键设计点:
cron 用行业标准字符串 · 不自造 DSL
"0 9 * * *" 表示每天 9 点 —— 直接沿用 Unix crontab 语法,不发明新语法。这带来两个直接好处:一是用户看 Claude 输出直接懂,不需要再解释;二是 Claude 训练数据里已经有大量 cron 语法示例,不用再教。复用行业约定,减少认知门槛是这个字段的核心设计。反例设计会是自造一个 { minute: "*/5", hour: "*", ... } 的 JSON 结构,看起来更「结构化」,但用户和模型都要重新学。
recurring 默认 true 的价值取向
默认 true 意味着「不写就是循环」—— 这个默认值有意导向监控用途。因为 Cron 家族典型场景就是 CI 监控、部署观察、周期性自检,这些都是循环。「一次性提醒」反而是要显式声明 recurring: false 的少数场景。默认值不是随手一挑,是对典型用途的隐式偏好声明。
durable 遗留字段的诚实透明度
tool description 明确写「durable has no effect」是诚实的透明度。这个字段是历史痕迹 —— 早期可能试图做持久版本,后来撤了,但字段留下来避免 breaking change。不删也不藏,明确告诉 Claude「这个字段没用,不要浪费精力设置它」。
4 · schema 校验规则
CronCreate 的 schema 层约束很稀薄,大部分约束在 runtime:
| 字段 | 类型 | 默认值 | schema 约束 |
|---|---|---|---|
cron |
string | 无(必填) | 5 字段格式 · 不做深校验 |
prompt |
string | 无(必填) | 无长度限制 |
recurring |
boolean | true |
布尔 |
durable |
boolean | 无 | 无(遗留) |
真正的约束都在 runtime:
- 7 天上限 —— runtime 定时到期自动删,不是 schema 层拦
- REPL idle 触发 —— runtime 状态机,schema 表达不了
- Jitter 分散 —— runtime 自动加 offset,schema 里的
"0 9 * * *"到 runtime 会被自动 nudge - Session-only 生命周期 —— runtime 内存态,不是持久化行为
Cron 家族的关键特征:schema 层几乎没有硬约束,行为主要靠 tool description 里的自然语言劝导 + runtime 机制兜底。这跟 AskUserQuestion 那种「schema minItems / maxItems 硬拦截」的风格完全相反 —— 因为 cron 语法本身太灵活,"7 * * * *" 和 "0 * * * *" 都合法,靠 schema 分不出好坏,只能靠 description 教 Claude 挑好的。
ScheduleWakeup —— 特化版本
CronCreate 是通用调度器。/loop skill 有自己特化的 ScheduleWakeup,专门给「动态间隔的循环」用:
- 调用者是 Claude 自己,不是外部触发
- 循环上下文自动传 —— 上一次 /loop 的 prompt 会自动再次触发
- 有 5 分钟 prompt cache TTL 意识 —— tool description 教 Claude 如何在 cache 窗口内外做不同选择
- 建议 60-1200 秒(1 分钟到 20 分钟)是主流
跟 CronCreate 的分工:通用调度用 CronCreate,/loop 里的自调度用 ScheduleWakeup。ScheduleWakeup 是「Cron 家族的循环特化亲戚」。
与邻居工具的分工
Cron 家族跟前十一个工具形成对照:
| 维度 | 三交互原语 | 定位 + 感知 + 执行 | Bash | Agent | Task 家族 | Web 双工具 | Cron 家族 |
|---|---|---|---|---|---|---|---|
| 定位 | 协作对齐 | 改代码 | 命令执行 | 派生 Claude | 外化工作记忆 | 触达公网 | 未来触发 |
| 时态 | 现在时 | 现在时 | 现在时 | 现在时 | 跨时 | 现在时 | 未来时(定时) |
| 状态位置 | 无 | 磁盘 | 无 | subagent | runtime 存储 | 无 | session 内 · 有 7 天上限 |
| 命名对偶 | Enter/Exit | Read/Edit/Write | 单一 | 单一 | CRUD 六件套 | Fetch/Search 姊妹 | Create/Delete/List 三件套 |
| 主要红利 | 用户对齐 | 精准改代码 | 工程流程 | context 空间 | 对抗遗忘 | 可控信息接口 | 等待外部世界变化 |
Cron 家族与 Task 家族的对比 —— 两组都是跨时状态,但方向相反:
- Task 家族:留下现在没做完的活 —— 现在时里挂一个未来时的 todo · 状态记录「什么该做」
- Cron 家族:约定未来主动做某事 —— 现在时里挂一个定时触发的 prompt · 状态记录「什么时间做什么」
一个像便签盒(手动查),一个像闹钟(自动响)。Task 是「Claude 主动去 List」,Cron 是「时间到了 Claude 被 wake」。两者都突破了「AI 主循环阻塞就没法做事」这个限制,但用的是不同的通道。
Cron 家族与 Bash run_in_background 的对比 —— 都是异步:
- Bash 后台:「机器等命令结束」,收到通知就完事(单次)
- Cron:「机器等时间到」,每次时间到都触发一次(周期或单次)
前者是IO 异步,后者是时间异步。Bash 后台能干 Cron 干不了的事(比如等 CI 结束),Cron 能干 Bash 干不了的事(比如每 5 分钟检查)。
Cron 家族与 Agent 的对比 —— 都是创造并行:
- Agent:空间维度的并行 —— fork 出新 context 让子 Claude 干活
- Cron:时间维度的并行 —— 排队未来触发让主 Claude 稍后干活
Cron 家族在工具生态里的位置 —— 前 11 个工具都是「当下的动作」,Cron 家族是唯一把未来时间当一等公民的原语。它不是新增了某个能力,而是为其它所有能力提供了触发时机。
小结
Cron 家族最有意思的信号,是「复用行业约定 · 减少认知门槛」这条主线在每一层都能看到:
- 命名层:直接借 Unix crontab 40 年的词,不发明新概念
- 字段层:
cron字段用 5 字段字符串标准语法,不自造 JSON DSL - 默认值层:
recurring: true对齐典型监控用途 —— 一次性反而是要显式声明的少数 - 时区语义:本地时区默认,避开 UTC 换算这个 sysadmin 世代都踩过的坑
- schema 层:反常地稀薄 —— 因为 cron 语法太灵活,
"7 * * * *"和"0 * * * *"都合法,靠 schema 分不出好坏
另一条独有信号是「服务器视角写进 tool prompt」 —— 避免 :00 和 :30 这条约束,是把系统级负载分散的责任写到 Claude 使用行为里。一般 tool 只关心 Claude 使用是否正确,不管服务器压力,Cron 是稀有例外,因为它是唯一一个可能造成用户不察觉的定期请求的 tool。
「诚实透明」也贯穿始终:session-only 生命周期开篇就说、7 天上限要求 Claude 主动告知用户、durable 遗留字段明确写「no effect」、jitter 机制显式暴露。透明比华丽重要 —— Claude 承诺不了的事就说清楚,用户和 Claude 之间不留误解空间。
对偶闭环的结构跟第十篇 Task 家族一致:Create / Delete / List 三件套共享一份 session 状态,合并写更利落。真正有设计密度的都在 Create,Delete 和 List 是配套的观察 + 管理工具。
下一篇继续拆 Monitor —— Cron 是「时间到了就唤醒」,Monitor 是「有事件就唤醒」。前者是主动定时轮询,后者是被动事件驱动。看看这个「事件流原语」是怎么设计的,又是怎么和 Cron 分工的。