---
title: Autopilots
description: 让智能体按 cron 定时自己开工，或在 webhook 到来时被触发——也可以通过 UI / CLI 手动触发一次。
---

import { Callout } from "fumadocs-ui/components/callout";

Autopilots 让 [智能体](/agents) **按调度自动开工**——配好 cron 和时区，到点 Multica 自己派发 [`task`](/tasks)，不需要你每次触发。适合定期巡检、周期性报告、凌晨跑的清理任务这类"standing order"场景。和前三种触发方式（[分配](/assigning-issues) / [@ 提及](/mentioning-agents) / [对话](/chat) 都是你主动喊一声）相比，Autopilots 的核心差别是**时间驱动**。

## 配置一个 Autopilot

在工作区的 **Autopilot** 页新建一条 autopilot，要定下：

- **名字** — 显示名
- **执行智能体** — 到点派给谁
- **优先级** — 继承给它产生的 `task`（语义同 issue 优先级）
- **描述 / Prompt** — 智能体每次执行拿到的工作说明
- **执行模式** — 见下节
- **触发器** — 至少加一条 `schedule`（cron + 时区）或 `webhook`

## 选择执行模式

Autopilot 有两种执行模式，**建议从"先建 issue 模式"开始**：

- **先建 issue 模式**（`create_issue`）—— 默认，**推荐**。每次触发先在工作区里建一个 issue（标题目前只支持一个占位符 `{{date}}`，会插值成 UTC 日期 `YYYY-MM-DD`；其他 `{{...}}` 形式的占位符会在创建时被拒绝，避免拼错以后悄无声息地把原文当成 issue 标题），再按分配流程把 issue 派给智能体。所有工作都落在 issue 看板上，历史、评论、状态和手动分配的 issue 完全一致。
- **直跑模式**（`run_only`）—— 不建 issue，直接入队一个 `task`。看板上看不到这一次运行——只能在 Autopilot 的运行历史里看到。

## 让它按时间跑

每个 Autopilot 至少要一个 `schedule` 触发器。Cron 是**标准 5 字段格式**（分 时 日 月 周），最小粒度 **1 分钟**（没有秒级）。时区用 IANA 格式（例如 `Asia/Shanghai`），决定 cron 表达式按哪个时区解读。

几个例子：

- `0 9 * * 1-5`，`Asia/Shanghai` —— 工作日北京时间早上 9 点
- `*/30 * * * *`，`UTC` —— 每 30 分钟一次
- `0 3 * * *`，`UTC` —— 每天 UTC 凌晨 3 点

Multica 服务器每 **30 秒**扫一次到期的触发器——**触发时刻最多延迟 30 秒**，不是秒级精准。服务器重启时如果恰好错过触发点，启动时会补扫漏掉的触发（不会丢触发，但会立刻补跑）。

## 手动触发一次

调试 Autopilot 时不想等 cron，可以手动触发一次：

- UI：在 Autopilot 详情页点"手动运行"
- CLI：

```bash
multica autopilot trigger <autopilot-id>
```

手动触发走和 `schedule` 触发完全相同的执行流程，只是运行记录里 `source` 字段标为 `manual`。

## 通过 Webhook 触发

Autopilot 也可以由入站 HTTP webhook 触发。在详情页添加一个 **Webhook**
触发器，Multica 会生成一个唯一的 URL：

```
https://<你的 Multica host>/api/webhooks/autopilots/awt_…
```

向这个 URL POST 任意 JSON——Multica 会记录一条 `source = webhook` 的
run，把请求体保存为 run 的 `trigger_payload`，然后按和 schedule 触发器
完全一致的方式派发给智能体。

```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d '{"event":"demo.received","eventPayload":{"message":"hello"}}'
```

在**先建 issue 模式**下，入站 payload 会附加在新 issue 的描述里供智能体
直接读到；**直跑模式**下，payload 也会随 run 一并交给 daemon。

### Payload 形态

可以发自己的封装：

```json
{ "event": "github.pull_request.opened", "eventPayload": { } }
```

也可以直接发任意 JSON 对象 / 数组。Multica 会规范化为内部封装：

```json
{
  "event": "<推断>",
  "eventPayload": <你的 body>,
  "request": { "receivedAt": "<rfc3339>", "contentType": "application/json" }
}
```

不带 `event` 字段时，Multica 会按以下顺序从常见 header 和 body 字段
推断：`X-GitHub-Event` + body `action`，`X-Gitlab-Event`、
`X-Event-Type`、body 里的 `event` / `type` / `action`。都不命中时事件
名退化为 `webhook.received`。

配置 GitHub 之类的来源时，请把 content type 设为 `application/json`——
表单编码的 webhook payload 在 v1 里不接受。

### 事件过滤

新建的 webhook 触发器对每一条入站 POST 都会触发，这在单一用途的 URL
上没问题，但对那种会扇出很多事件类型的来源（典型就是 GitHub——
一个仓库 webhook 就会同时下发 `push`、`pull_request`、`workflow_run`、
`check_suite` 等等）就会很吵。webhook 触发器上的**事件过滤**区块用来
限制哪些事件真正派发一次 run；其它的只记录到投递历史里，
`status = ignored`、`reason = event_filtered`，不会建任何 issue 或 run。

每一行是一条规则：一个**事件名**加可选的、逗号分隔的 **action** 列表。
**任意一行命中**即放行；区块留空则接受所有事件（即过滤前的行为）。

例子：

| 事件名           | Actions             | 命中范围                                                                 |
| ---------------- | ------------------- | ------------------------------------------------------------------------ |
| `workflow_run`   | `completed, failed` | 只有 `action` 为 `completed` 或 `failed` 的 `workflow_run` 事件          |
| `workflow_run`   | _(留空)_            | 所有 `workflow_run` 事件，不限 action                                    |
| `push`           | _(留空)_            | 所有 `push` 事件                                                         |

#### 事件名和 action 从哪来

Multica 按下面的顺序从入站请求里推断 `event` 和 `action`，**先命中先用**。

**1. Body envelope。** 如果 body 是一个 JSON 对象、且带字符串字段
`event`，就直接用它作为事件名。可选的 `eventPayload` 对象再
从自己的 `action` / `state` / `conclusion` / `status` 字段里提供
action 候选。

```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d '{"event":"trigger","eventPayload":{"action":"true"}}'
# 推断结果：event = trigger，action 候选 = true
```

**2. 请求头。** 没有 body envelope 时按以下头部识别：

- `X-GitHub-Event: <event>` —— 结合 body 顶层的 `action` 字段
  （如果有），拼成 `github.<event>.<action>`。
- `X-Gitlab-Event: <event>` —— 拼成 `gitlab.<event>`。
- `X-Event-Type: <event>` —— 原样使用。

```bash
# GitHub 风格：事件名来自 header，action 来自 body。
curl -X POST "$MULTICA_WEBHOOK_URL" \
  -H 'X-GitHub-Event: workflow_run' \
  -H 'Content-Type: application/json' \
  -d '{"action":"completed"}'
# 推断结果：event = github.workflow_run.completed
#        → 命中 workflow_run / completed 的过滤规则

# 通用 event-type 头部 —— 不需要任何 body 字段。
curl -X POST "$MULTICA_WEBHOOK_URL" \
  -H 'X-Event-Type: trigger.true' \
  -H 'Content-Type: application/json' \
  -d '{}'
# 推断结果：event = trigger.true → 命中 trigger / true
```

**3. Body 兜底。** Body envelope 和上面的 header 都没有时，
从 body 顶层字符串字段里依次找：`event` → `type` → `action`。

```bash
curl -X POST "$MULTICA_WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d '{"type":"trigger","action":"true"}'
# 推断结果：event = trigger（取自 `type`），action 候选 = true
```

**4. 默认值。** 以上都没命中时，事件名取 `webhook.received`，
没有 action 候选。

**action 候选的完整清单。** 事件名确定后，下面这些值都会被列为
可能的 action：

- 事件名后缀，当事件形如 `provider.event.<action>` 时
  （例如 `github.workflow_run.completed` → `completed`）。
- body 里 `action`、`state`、`conclusion`、`status` 这四个字段——
  **必须是 JSON 字符串**。布尔（`{"action": true}`）或数字都不算
  候选，所以 `event=trigger, action=true` 的过滤规则**永远命中不了**
  `{"trigger": true}` 这种 body，因为 `true` 是 bool 不是字符串。

**常见误区。** 一条 `Event name: trigger` / `Actions: true` 的规则
**不是**「body 里出现 `trigger: true` 就放行」的意思——事件过滤匹配的是
**推断出来的事件和 action**，不是任意 body 字段。要命中它，请用
`X-Event-Type` 头发送 `trigger.true`，或者用上面的 body envelope。
保存时带空格的值（例如 `" workflow_run "`）会被原样保存，但永远命中
不了——保存前请先 trim。

#### 快速验证

配好过滤后，可以用 `curl` 同时验证「命中」和「被过滤」两条路径：

```bash
# 命中 —— 请求头给出 event=workflow_run，body 给出 action=completed
curl -X POST "$MULTICA_WEBHOOK_URL" \
  -H 'X-GitHub-Event: workflow_run' \
  -H 'Content-Type: application/json' \
  -d '{"action":"completed"}'
# → 200 {"status":"accepted", ...}

# 被过滤 —— 同样的事件，但 action 不在白名单
curl -X POST "$MULTICA_WEBHOOK_URL" \
  -H 'X-GitHub-Event: workflow_run' \
  -H 'Content-Type: application/json' \
  -d '{"action":"in_progress"}'
# → 200 {"status":"ignored","reason":"event_filtered"}
```

### URL 即 bearer secret

生成的 URL **就是凭证**，谁拿到都能触发这个 Autopilot。请按 token 对待：

- **不要贴到公开 issue 评论、截图、聊天记录里。**
- **泄漏后立即重新生成**——在触发器上点"重新生成 URL"，或运行
  `multica autopilot trigger-rotate-url <autopilot-id> <trigger-id>`。
  旧 URL 立即失效。
- 对需要强来源认证的源，等 per-trigger HMAC 签名校验上线；v1 URL 仅
  bearer。
- 当前能查看 Autopilot 的工作区成员都能看到它的 webhook URL——更细的
  权限可见性是后续工作。

### 状态码语义

正常的 no-op 路径都返回 `200 OK` 加 `status` 字段，避免外部 webhook 重试
机制反复打：

- `{"status":"accepted","run_id":"…","autopilot_id":"…","trigger_id":"…"}`
  —— 已派发一次 run。
- `{"status":"skipped","run_id":"…","reason":"agent runtime is offline at dispatch time"}`
  —— 受派智能体的 runtime 离线，记为 `skipped` run。
- `{"status":"ignored","reason":"trigger_disabled"}` —— 触发器已禁用。
- `{"status":"ignored","reason":"autopilot_paused"}` —— Autopilot 已暂停。
- `{"status":"ignored","reason":"autopilot_archived"}` —— Autopilot 已归档。

非 2xx 是真正的失败：

- `400` —— 无效 JSON、scalar body、空 body。
- `404` —— 未知 token（`{"error":"webhook not found"}`）。
- `413` —— 请求体超过 256 KiB。
- `429` —— 单 token 速率限制（默认 60 次 / 分钟）。

### 自托管：配置公开 URL

服务端设置 `MULTICA_PUBLIC_URL`（例如 `https://multica.example.com`）后，
触发器响应里会带绝对的 `webhook_url`，UI 直接显示可复制的 URL。没设
时 UI 会用客户端的 API origin 拼出 URL——desktop 和同源 web 没问题，
但自定义反向代理就不行了。Multica **故意不**从 `Host` /
`X-Forwarded-Host` header 推断公开主机，避免反代配置失误时被诱导生成
指向攻击者域名的 webhook URL。

## 看运行历史

每次触发都会产生一条**运行记录**（run），可以在 Autopilot 详情页的"历史"tab 看到：

- 触发源（`schedule` / `manual` / `webhook`）
- 开始时间、完成时间
- 状态（`issue_created` / `running` / `completed` / `failed` / `skipped`）
- 关联的 issue（先建 issue 模式）或 `task`（直跑模式）
- 失败原因（失败或跳过时）

## Autopilot 失败会怎样

<Callout type="warning">
**Autopilot 失败不自动重试，也不发 inbox 通知。** 失败后只在运行历史里留一条 `failed` 记录——不会像分配 / @ 提及那样由系统重新排队，也不会给任何人发通知。如果这条 Autopilot 是周期任务，**下一次 cron 到点会重新触发一次**（新的 run），但这一次失败的工作不会被自动补跑。

如果 Autopilot 很重要，要自己设计监控——例如让智能体在成功时给自己发个评论，通过缺失评论来发现失败。
</Callout>

不自动重试的理由：Autopilot 本身是周期性的，系统层再加自动重试容易和下一次调度叠加，产生重复执行。调度权完全交给 cron 最干净。

## 暂不可用的能力

**API 类型触发器尚未接入。** 触发器 schema 里保留了 `api` 类型但没有
入站路由会触发它；UI 会给已有的此类记录打 Deprecated 标签，也不显示
copy / rotate 操作。Per-trigger HMAC 签名校验、IP allowlist、按提供方
的事件预设是后续工作；v1 URL 仅 bearer。

## 下一步

- [**分配 issue 给智能体**](/assigning-issues) —— 一次性把 issue 指派给智能体
- [**在评论里 @ 智能体**](/mentioning-agents) —— 评论里让智能体看一眼
- [**对话**](/chat) —— 独立于 issue 的一对一聊天
