文档 运行 定时任务

定时任务

定时任务按 cron 自动运行应用,把"我做了一个有用的应用"变成"每周一早上自动出现在我的 Slack",让工作流持续产生复利。

心智模型。一个定时任务 = Cron 表达式 × 一组输入 × 一个交付目的地。你在说: "每周一上午 9 点,用这些默认参数运行这个应用,然后把结果发送到这里。"剩下的事情交给平台: 创建沙箱、运行各阶段、保存产物、交付结果、记录全过程。

定时任务的结构

平台把定时任务存储在 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

// schedule_type = "interval"
{ "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 处区分它们:

// action_type = "send_message" — 向一个会话排入一条提示词
{ "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。向导会问你三件事:

1
什么时候运行。 选择一个预设(每小时、每天、每周、每月),或者写一个 cron 表达式。时间使用你工作区的时区,在依赖定时任务之前请先把时区设好。
2
使用哪些输入。 用每次运行都该使用的默认值填写应用的表单。对于跟时间相关的输入,使用动态占位符 ({{today}}{{last_week_start}})。
3
结果送到哪里。 可以是一个或多个目的地:邮箱、Slack 频道、Drive 文件夹、Notion 页面、Webhook。

保存后定时任务就生效了。应用页面上的 Next run 字段会显示下一次触发时间。 你可以随时打开 Schedules 标签来暂停、编辑、立即运行或删除。

用 ScheduleConfig 表达的常见模式

你用 cron 想表达的那些定期任务,写成 Aitroop 预设的形状是这样的。 cron_expr 一列是服务器为展示而计算出来的,它永远不可由用户编辑。

模式schedule_typeschedule_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:30weekly{ 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 / monthlytime 字段按该时区解释,所以一个时区为 America/Los_Angeles 的每日 09:00 任务全年都在太平洋时间上午 9 点触发, 并通过基于 Intl 的时差运算自动跟随夏令时变化(参见 scheduledTasks.ts 里的 computeNextRun)。默认时区是 UTC

调度器内部机制

有一个工作进程大约每 60 秒驱动一次调度器。每个 tick 会以 50 个为一批,挑出所有 enabled = truenext_run_at <= NOW() 的任务。对每个到期的任务 调用 executeTask,然后把 last_run_atlast_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-31
yesterday2026-05-30
last7days2026-05-24 → 2026-05-31(用于 daterange
last30days2026-05-01 → 2026-05-31
thisMonth2026-05-01 → 2026-05-31
lastMonth2026-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 中留下记录,附带状态(queuedok / error),应用的 Executions 标签里照常显示产物。

从平台外部按需触发

定时任务由 cron 触发。要从脚本或 webhook 处理器手动触发:

POST /api/scheduled-tasks/:id/run
Authorization: Bearer $AT_USER_TOKEN

定时任务会以其配置的输入和交付目标立即运行一次。适合在下次定时触发之前提前刷一遍。

应用版本与定时任务

定时任务总是使用应用的当前版本。你编辑应用之后,下一次触发就会用到新版本,目前没有版本固定的界面。执行记录会保存运行时的 app_version, 所以历史记录依然清晰无歧义,但你无法把一个定时任务冻结到旧版本。 如果你需要版本稳定性,实用的变通方法是:克隆应用,把克隆冻结,让定时任务指向克隆。

定时运行失败时

每次触发都会在 app_task_runs 中写入一行,记录 statustriggeredscheduler | manual)。所属的任务行 会记录最近一次结果:last_statusok / error / queued)、last_errorlast_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/:idaction_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 端点,列出全部,再挑你要的那一行即可。