定时任务
定时任务按 cron 自动运行应用,把"我做了一个有用的应用"变成"每周一早上自动出现在我的 Slack",让工作流持续产生复利。
定时任务的结构
平台把定时任务存储在 app_scheduled_tasks 表中。无论这个任务是运行应用还是发送聊天提示词, 结构都是一样的,区别在 action_type。
"id": "task_...",
"name": string,
"enabled": boolean,
"schedule_type": "interval" | "daily" | "weekly" | "monthly",
"schedule_config": ScheduleConfig, // 预设形状,见下文
"cron_expr": string, // 从 config 计算得出;只读
"timezone": "America/Los_Angeles", // IANA 名称
"action_type": "send_message" | "run_app",
"action_config": ActionConfig, // 形状取决于 action_type
"session_id": string | null, // send_message 首次触发时自动创建
"run_count": number,
"last_run_at": timestamp | null,
"next_run_at": timestamp, // 调度器挑选 next_run_at <= NOW() 的任务
"last_status": "ok" | "error" | "queued" | null,
"last_error": string | null
}
ScheduleConfig:四种形态
你不需要写 cron。从四种 schedule_type 中选一种,每种都接收一个小型配置对象。 服务器据此推导出 cron_expr(一个便于阅读的标签),并基于 IANA 时区运算 计算 next_run_at。
{ "every": 15, "unit": "minutes" } // unit: "minutes" | "hours"
// schedule_type = "daily"
{ "time": "09:00" } // 任务所在时区的 HH:MM
// schedule_type = "weekly"
{ "time": "09:00", "days": [1,3,5] } // 0=周日 .. 6=周六 — 这里是周一/三/五
// schedule_type = "monthly"
{ "time": "09:00", "day": 1 } // 1–28(取 28 以兼容所有月份)
ActionConfig:每次触发会做什么
共有两类动作。校验器在 POST /api/scheduled-tasks 处区分它们:
{ "prompt": "汇总我从昨天到现在的 GitHub 活动。" }
// action_type = "run_app" — 触发一次 AppExecution
{
"app_id": "app_...",
"app_input": { company_name: "Stripe", ... }
}
对于 send_message,首次触发时会创建一个专用聊天会话 (session_type = 'scheduled'),并把 session_id 写回任务。 此后每次触发都会向同一个会话中加入一条新消息,这样代理在多次运行之间就有上下文连续性, "每天早上检查一下 X"才会真的像一段持续进行的对话。
对于 run_app,调度器在每次触发前会验证所需的 Connect 是否仍然有效。如果有缺失, 这次运行会记录 last_status = 'error' 和一条 "Missing required connections: …" 信息,而不会进入执行。
创建一个定时任务
在任意应用页面,点击 Schedules → New schedule。向导会问你三件事:
{{today}}、{{last_week_start}})。保存后定时任务就生效了。应用页面上的 Next run 字段会显示下一次触发时间。 你可以随时打开 Schedules 标签来暂停、编辑、立即运行或删除。
用 ScheduleConfig 表达的常见模式
你用 cron 想表达的那些定期任务,写成 Aitroop 预设的形状是这样的。 cron_expr 一列是服务器为展示而计算出来的,它永远不可由用户编辑。
| 模式 | schedule_type | schedule_config | 计算出的 cron_expr |
|---|---|---|---|
| 每 15 分钟 | interval | { every: 15, unit: "minutes" } | every 15 min |
| 每小时 | interval | { every: 1, unit: "hours" } | every 1 hour |
| 每天上午 9 点 | daily | { time: "09:00" } | 0 9 * * * |
| 周一/三/五上午 8:30 | weekly | { time: "08:30", days: [1,3,5] } | 30 8 * * 1,3,5 |
| 每周一上午 9 点 | weekly | { time: "09:00", days: [1] } | 0 9 * * 1 |
| 每月 1 号上午 9 点 | monthly | { time: "09:00", day: 1 } | 0 9 1 * * |
最小频率:5 分钟。服务器会以 422 invalid schedule 拒绝任何 低于 300 秒的 interval。调度器自身也会做防御,如果发现某个任务的配置漂移到 5 分钟以下,就会把 next_run_at 往后推并跳过这一次触发。这里说的 "cron" 是什么(和不是什么)
Aitroop 的 cron_expr 字段是一个派生的标签,而不是输入,没有地方让你粘贴像 */30 9-17 * * MON-FRI 这样的五段表达式。四种 schedule_type 覆盖了 平台支持的场景,执行器在运行时也不会去解析 cron 语法。如果你需要超出预设的表达能力, 标准做法是:
- 用
daily 把任务安排在它可能触发的最早时间。 - 在应用最上方加一段守卫(一个
script 阶段),判断"今天是不是真的该干这事的日子?",不是的话就低成本地直接退出。
时区
每个任务都带有自己的 IANA timezone 字符串(例如 America/Los_Angeles)。对于 daily / weekly / monthly,time 字段按该时区解释,所以一个时区为 America/Los_Angeles 的每日 09:00 任务全年都在太平洋时间上午 9 点触发, 并通过基于 Intl 的时差运算自动跟随夏令时变化(参见 scheduledTasks.ts 里的 computeNextRun)。默认时区是 UTC。
调度器内部机制
有一个工作进程大约每 60 秒驱动一次调度器。每个 tick 会以 50 个为一批,挑出所有 enabled = true 且 next_run_at <= NOW() 的任务。对每个到期的任务 调用 executeTask,然后把 last_run_at、last_status, 以及新计算出的 next_run_at 写回去。通过 POST /api/scheduled-tasks/:id/run 手动触发,走的是同一条 executeTask 路径,所以从下游看,手动运行和定时运行没有区别。
动态输入:每次运行都会变化的默认值
大部分定时运行的应用都需要一个"滑动锚点","昨天的工单"、"上月的支出"。 正确的实现位置是在应用的 input_schema 上,而不是在定时任务上。 在相应的输入字段上设置 dynamic_default,并且不要把这个字段写进 action_config.app_input;执行器会在运行时填入。
dynamic_default解析为(在 2026-05-31 的示例) today2026-05-31yesterday2026-05-30last7days2026-05-24 → 2026-05-31(用于 daterange)last30days2026-05-01 → 2026-05-31thisMonth2026-05-01 → 2026-05-31lastMonth2026-04-01 → 2026-04-30
一个每周一次的工程报告,把它的 daterange 输入设为 dynamic_default: "last7days",就始终覆盖正确的时间窗口,配置一次之后, 定时任务就再也不用碰日期了。
为什么不在 goal 一侧做模板?早期的文档曾暗示定时任务的输入可以用 {{last_week_start}} 这样的占位符。实际上不行,goal 展开器是一个字面 key 替换器 (见 核心概念)。所有时间相关性都放在输入 schema 上。结果去了哪里
一次定时运行产生的产物集合与手动运行完全相同,落点也一样:对于 run_app,是 AppExecution 下的 app_artifact 行;对于 send_message, 是聊天会话的消息历史。这里没有单独的"交付"子系统,要把结果送到外部,把它做进应用里:
- 要发送邮件报告,加一个使用用户已授权邮箱 Connect 的阶段去发送。
- 要发到 Slack,给应用配上 Slack 的 Connect,让最后一个阶段去发送。
- 要保存到 Drive / Notion,同样的模式,使用对应的 Connect。
- 要推送到自己的 Webhook,加一个
script 阶段去 POST。
每次运行也会在 app_task_runs 中留下记录,附带状态(queued → ok / error),应用的 Executions 标签里照常显示产物。
从平台外部按需触发
定时任务由 cron 触发。要从脚本或 webhook 处理器手动触发:
POST /api/scheduled-tasks/:id/run
Authorization: Bearer $AT_USER_TOKEN定时任务会以其配置的输入和交付目标立即运行一次。适合在下次定时触发之前提前刷一遍。
应用版本与定时任务
定时任务总是使用应用的当前版本。你编辑应用之后,下一次触发就会用到新版本,目前没有版本固定的界面。执行记录会保存运行时的 app_version, 所以历史记录依然清晰无歧义,但你无法把一个定时任务冻结到旧版本。 如果你需要版本稳定性,实用的变通方法是:克隆应用,把克隆冻结,让定时任务指向克隆。
定时运行失败时
每次触发都会在 app_task_runs 中写入一行,记录 status 和 triggered(scheduler | manual)。所属的任务行 会记录最近一次结果:last_status(ok / error / queued)、last_error、last_run_at。 失败不会自动重试,它们会等待下次触发。
常见失败模式
症状 可能原因 解决 last_error: "Missing required connections: …"应用所需的某个 Connect 在触发之前被撤销或过期。 在 设置 → Connects 中重新授权该 Connect。下次触发会照常进行。 阶段超时 目标随时间变得越来越重(数据更多、检索更长)。 调高受影响阶段的 timeout_ms。 结果为空 dynamic_default 解析出的窗口落在了一段没有数据的区间。扩大窗口,或者让阶段优雅地报告"没有活动"。 低于最小频率被跳过 间隔漂移到 5 分钟以下(通常是直接改数据库导致)。 调度器会把 next_run_at 往后推 5 分钟并跳过。通过 PUT 修正间隔。
暂停与编辑
可以把一个定时任务关掉而不删除它,在故障期间或者编辑应用时很有用。 编辑任意字段,cron、输入、交付,下次运行会使用新值。
关注成本的调度。15 分钟一次的频率是每小时一次的 4 倍成本,是每天一次的 96 倍。 调高频率之前,问问自己有没有东西真的变化得这么频繁。大多数所谓"实时"告警的需求, 其实每小时检查一次就足够了。常见问题与排查
我的定时任务没有在上午 9 点按预期触发。
最常见的原因:
- 时区不对。默认是 UTC。要么在创建任务时传入一个 IANA
timezone,要么通过 PUT /api/scheduled-tasks/:id 更新。 - 夏令时切换。
computeNextRun 的实现会查阅 IANA 数据库,让墙上钟的 time 在夏令时切换前后保持不变。可以查看 app_scheduled_tasks 里的 next_run_at,看看调度器认为下次触发是何时。 - 任务被禁用。有人把
enabled 设成了 false。直接检查行,或通过 GET /api/scheduled-tasks 查看。 - 触发窗口。调度器大约每 60 秒运行一次,在繁忙的服务器上,一次触发可能比预定时间晚最多一分钟。
"每个季度第二个工作周的星期二运行一次"怎么做?
四种预设不能覆盖这个场景,也没有原生 cron 的逃生口。支持的模式是"每天调度,在应用里守卫":
- 设置
schedule_type: "daily" 在你选定的墙钟时间。 - 把应用的第一个阶段做成一个
script 阶段,检查日期,如果今天不是合适的日子就低成本退出。
能不能从 Slack 的斜杠命令触发一个定时任务?
可以,把 Slack 的斜杠命令指向一个 webhook URL,让它 POST 到 /api/scheduled-tasks/:id/run。定时任务会以其配置的输入运行一次。 请在你这一侧的处理器里校验 Slack 的签名密钥。
我的应用现在多了一个输入。已有的定时任务会坏吗?
它们会继续运行。action_config.app_input 是原样传递的,所以缺失的键会回退到 应用的 input_schema 里定义的内容,字段的 default, 或者运行时的 dynamic_default。如果这个新输入是 required 且没有默认值, 下次触发会在校验阶段以 Missing required input 错误失败。请通过 PUT /api/scheduled-tasks/:id 在 action_config.app_input 中带上新键,更新任务。
在哪里能看到正在排队或正在运行的任务?
先列出任务本身,然后再钻到单个任务的运行历史:
# 当前用户的所有定时任务
curl https://app.aitroop.net/api/scheduled-tasks \
-H "Authorization: Bearer $AT_USER_TOKEN"
# 单个定时任务最近 50 次运行(status / triggered / started_at)
curl https://app.aitroop.net/api/scheduled-tasks/$ID/runs \
-H "Authorization: Bearer $AT_USER_TOKEN"能不能把定时任务纳入版本控制?
可以,通过 GET /api/scheduled-tasks 读取(或者筛选到某个任务,然后把 JSON 持久化 到仓库里)。通过 POST 重新创建,或者用 PUT /api/scheduled-tasks/:id 修改已有任务。没有按任务 ID 的 GET 端点,列出全部,再挑你要的那一行即可。