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 如何跨越时间执行动作」:

  1. 打破同步束缚 —— Claude 不再只能「有请求 → 回应」· 可以「安排一个未来的自我唤醒」
  2. 精确调度 —— 用标准 cron 语法(M H DoM Mon DoW)· 灵活到任意时刻或任意周期
  3. 一次性 / 重复两种模式 —— 用 recurring 布尔切换
  4. 轻量提醒 —— 不用起 background task 就能实现「30 分钟后 remind」
  5. 主动感知 —— 等外部状态时(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
2
3
while true:
Bash(command: "gh run list")
sleep 60

问题:每分钟消费一次上下文 · 8 分钟就是 8 次 · 主 Claude 的 context 被日志灌满 · 上下文浪费。

用 CronCreate 是怎么解决的

Claude 调 CronCreate,安排一次 8 分钟后的一次性唤醒:

1
2
3
4
5
CronCreate(
cron: "13 22 29 7 *", # 精确的时间 (7 月 29 日 22:13 一次)
recurring: false, # 一次性
prompt: "现在检查 CI 状态 · 用 gh run list · 如果失败告诉用户 · 成功就简短确认"
)

运行时会发生什么:

  • Runtime 把这个 job 记下来(session 内存里)
  • 主 Claude 立即回到用户 —— 不阻塞
  • 用户可以问别的 · 让 Claude 干别的
  • 到 22:13 · runtime 自动把 prompt 作为一次新的 Claude 调用触发
  • Claude 拿到 prompt · 跑 gh run list · 汇报状态

对用户来说,体验是:

1
2
3
4
5
[22:05] 用户: 我刚推了部署 · 8 分钟后帮我看下 CI
[22:05] Claude: 好的 · 我已经安排 22:13 自动检查
(你可以随便干点别的)
[22:05-22:12] 用户: (随便干别的 · Claude 有对话就答有对话就答)
[22:13] Claude(自动触发): CI 检查完毕 · 3 个 workflow 全绿 ✅

关键洞察:CronCreate 把「等待」从主 Claude 的责任变成runtime 的责任。主 Claude 完成安排就撤,不占对话时间也不占 context。

组合用法:CronList 看进度 · CronDelete 提前取消

如果用户忽然说「算了不用等 CI 了 · 我自己看」,Claude 可以:

1
2
CronList()  # 拿到之前那个 job 的 ID
CronDelete(id: "cron_xxx") # 取消

或者用户问「你安排了什么任务」· 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” gets 0 * — 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 分工的。