排程
排程依 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 端點,列出全部,再挑你要的那一列即可。