第7章 Kortix 第 7 章:触发器详解
触发器
触发器用于在计划任务、Webhook 或监控器中启动会话。也就是说,触发器能在无人值守的情况下发起会话。你可以利用它自动化重复性或事件驱动型的工作。
若需在某次会话任务执行后再跟进(比如“明天检查他们是否回复了”),请改用提醒功能。提醒是绑定于该会话范围的触发器,它存储在数据库中,而非 kortix.yaml 文件里。
触发器类型
Kortix 支持三种触发器类型:
- cron:按设定的时间表运行。
- webhook:当外部服务向该项目的 Webhook URL 发送带有签名的请求时运行。
- monitor:全天候(24/7)运行仓库中的某条命令。该命令向 stdout 输出的每一行都会触发触发器。实验性功能:详见“监控器”章节。
你在项目清单文件 kortix.yaml 中定义触发器。每个触发器都持有一个提示词(prompt),该提示词会在触发会话时渲染为首条消息。运行时状态(如最近触发时间、状态等)存储在数据库而非清单中。触发触发器不会产生 Git 提交。
对于拥有大量触发器的项目,你可以将它们移出根文件。在 imports 下指定一个目录,然后将每个触发器或触发器分组放置在各自的 YAML 文件中。所有页面和命令在处理拆分后的清单时行为保持一致。对触发器的任何编辑都会写入声明该触发器的文件。
通过 API、SDK 或仪表板创建、更新或删除触发器时,会直接写入默认分支,而无需经过变更请求(CR)。但如果是在会话内编辑 kortix.yaml 并运行 kortix ship,则遵循标准的分支和 CR 流程。
设置 cron 触发器
添加触发器: kortix triggers add daily-digest --type cron \
--cron "0 0 9 * * 1-5" --timezone America/Los_Angeles \
--prompt "总结昨天的活动并保存为每日笔记。" cron 表达式由 6 个字段组成:秒、分、时、日、月、星期。此命令仅修改本地的 kortix.yaml。推送变更: kortix ship kortix ship 会提交 kortix.yaml 并推送到仓库。一旦该变更落地到项目的默认分支,计划任务即生效。确认运行: kortix triggers ls 列表会显示每个触发器的 slug、状态以及最近一次触发时间。若不想等待计划时间,想立即触发,可运行 kortix triggers fire daily-digest。
设置 Webhook 触发器
Webhook 触发器需要一个密钥。Kortix 使用它来校验每个传入请求的签名。
添加密钥: kortix secrets set WEBHOOK_SECRET= 关于密钥的更多信息,请参见“密钥”章节。添加触发器: kortix triggers add new-lead --type webhook \
--secret-env WEBHOOK_SECRET \
--prompt "收到新潜在客户:{{ body.name }} ({{ body.email }})。将其添加到 CRM。" 其中 --secret-env 指定了用于对此触发器请求进行签名的密钥。推送变更: kortix ship发送请求:Kortix 根据项目 ID 和触发器 slug 构建 Webhook URL:POST /v1/webhooks/projects/<project-id>/<slug> 外部服务需向此 URL 发送带有签名的 POST 请求。Kortix 会根据 WEBHOOK_SECRET 校验签名,随后开启一个新会话,该会话可在提示词中访问请求体。具体的请求头和格式见下文“Webhook 签名”。
默认情况下,每次触发都会在新的分支上创建新会话。触发器也可以选择复用或锁定会话,Webhook 触发器还可筛选哪些载荷能触发会话——详见下文“会话策略”与“载荷模板”。
监控器
实验性功能。 监控器仅在 monitors 特性开关开启时运行,默认关闭。若开关关闭,平台不会配置监控器实例,也不会触发监控器事件。
监控器用于观察那些既不推送 Webhook 也不符合定时计划的目标,如实时日志、队列深度、变动的页面或价格。Kortix 在项目专属的监控器实例中全天候运行你的命令——每个项目拥有独立的持久化 microVM,具备与沙箱会话相同的隔离边界和密钥访问权限。确定性代码负责监听,智能体仅在命令输出一行日志时唤醒。
四条规则定义了契约:
- stdout 中的行即事件,其他一律不是。stderr 仅作诊断用途,在监控器日志中可见,但不会触发事件。
- 每一行日志仅通过 Webhook 路径(filter → 提示词模板 → session_mode)触发一次。
- 监控器不会静默失败。进程退出、重启预算耗尽或静默时间超过 expect_event_within,都会在同一事件流中触发由平台编写的生命周期事件。
- 监控器的 session_mode 默认为 reuse 而非 fresh。鉴于监控器设计上会频繁触发,若设为 fresh 会为每个事件创建一个会话。
添加监控器: kortix triggers add checkout-errors --type monitor \
--run "./monitors/checkout-errors.ts" \
--mode poll --interval 60s --expect-event-within 24h \
--prompt "结账监控器输出:{{ line }}" --mode poll 表示每隔 --interval 重新运行命令,并要求其退出;--mode stream 表示运行一次并保持存活,stream 模式不使用 --interval。两种形式都产生日志行,下游无法区分。cron、run_at、timezone 和 secret_env 在监控器中会被拒绝。推送变更: kortix ship 该变更落地到默认分支后,平台即启动监控器实例。确认运行: kortix triggers ls kortix triggers info checkout-errors ls 命令对监控器显示其 mode 和 interval,而 cron 则显示其时间表。info 命令显示 run、mode、interval 及 expect_event_within。
监控器限制
以下限制均由平台强制执行:
| 每项目监控器数量 | 最多启用 10 个 |
| Poll 间隔 | 最少 30s |
| expect_event_within | 最少 5m |
| 每监控器事件速率 | 持续速率 60 次/小时,突发上限 30 次。超限后监控器会被抑制 10 分钟;24 小时内 3 次超限则自动禁用该监控器。 |
| 日志行长度 | 8 KiB,超长部分截断并标记 truncated: true |
| 重启预算 | 10 分钟内最多重启 5 次,超出后触发 restart_budget_exhausted 生命周期事件并退避 15 分钟 |
| 事件保留期 | 30 天 |
| 月度实例预算 | 默认 $75。超额后实例停止,并触发 budget_exceeded 生命周期事件。 |
监控器实例需要支持持久化沙箱的 provider。若项目的 provider 不支持,monitors 特性开关会报告不可用。
配置结构
# kortix.yaml
triggers:
- slug: daily-digest # 必填,仅限小写字母和连字符,每项目唯一
name: Daily digest # 可选,默认值同 slug
type: cron # "cron" | "webhook" | "monitor",必填
agent: kortix # 可选,默认 "default"
model: anthropic/claude-sonnet-4-5 # 可选,未设时于触发时解析
enabled: true # 可选,默认 true
cron: "0 0 9 * * 1-5" # 6 字段表达式,与 run_at 互斥
timezone: America/Los_Angeles # IANA 时区名,默认 UTC
session_mode: reuse # "fresh" | "reuse" | "pinned" | "keyed",默认 "fresh"
filter: # 可选,Webhook 载荷过滤
"body.data.direction": "inbound"
prompt: "总结 {{ body.text }}" # 必填,模板字符串
监控器用其命令和形态替代计划字段:
# kortix.yaml
triggers:
- slug: checkout-errors
type: monitor
run: ./monitors/checkout-errors.ts # 必填,仓库相对路径命令
mode: poll # "poll" | "stream",必填
interval: 60s # poll 模式必填,stream 模式无效
expect_event_within: 24h # 可选静默看门狗
agent: oncall
session_mode: reuse # 监控器默认值
filter: # 可选,与 Webhook 相同的过滤逻辑
"line.severity": "error"
prompt: "结账监控器输出:{{ line }}"
旧版 kortix.toml 使用相同的字段但容器不同,详见旧版 TOML 文档。
| 字段 | 必填 | 默认值 | 备注 |
| slug | 是 | — | 格式 [a-z0-9][a-z0-9_-]{{0,127}},每项目唯一。 |
| type | 是 | — | cron、webhook 或 monitor。 |
| prompt | 是 | — | 模板字符串。渲染为会话首条消息。 |
| name | 否 | slug | 人类可读标签。 |
| agent | 否 | default_agent | 须为 agents: 中的键名。省略则使用 default_agent,不要直接写字面量 default。 |
| model | 否 | 触发时解析 | 采用 provider/model 格式,例如 anthropic/claude-sonnet-4-5。 |
| enabled | 否 | true | 若为 false,调度和 Webhook 接收器将跳过该条目。 |
| session_mode | 否 | fresh(监控器默认为 reuse) | 详见“会话策略”。 |
| session_id | 设为 pinned 时必填 | — | 需重新提示的具体会话。 |
| session_key | 设为 keyed 时必填 | — | 模板字符串。仅设此项即隐式设为 session_mode: keyed。 |
| filter | 否 | — | 点分路径对应期望字符串。不匹配的 Webhook 交付返回 200 且不启动会话。 |
| cron | cron 类型时需填 cron 或 run_at 之一 | — | 6 字段表达式:秒 分 时 日 月 星期。 |
| run_at | cron 类型时需填 cron 或 run_at 之一 | — | ISO-8601 时间戳。仅触发一次,随后休眠。 |
| timezone | 否,仅 cron | UTC | IANA 时区名。 |
| secret_env | Webhook 类型必填 | — | 持有签名密钥的项目密钥名。密钥必须使用 broker 交付和 connector 消费者。 |
| run | Monitor 类型必填 | — | 仓库相对路径命令,其 stdout 行即为事件。仅限单行,最长 1024 字符。 |
| mode | Monitor 类型必填 | — | poll 每隔 interval 重新运行;stream 运行一次并保持存活。 |
| interval | mode: poll 时必填 | — | 时长字面量(30s, 5m, 24h, 7d),最小 30s。mode: stream 下无效。 |
| expect_event_within | 否,仅 monitor | — | 时长字面量,最小 5m。静默超过此时长将触发生命周期事件。 |
Cron 触发器必须填 cron 或 run_at,二者互斥。缺乏 secret_env 的 Webhook 触发器会被拒绝——不存在未认证的 Webhook。监控器必须包含 run 和 mode,并直接拒绝 cron、run_at、timezone 和 secret_env——若清单声称拥有监控器运行器根本不读取的计划时间表,那便是虚假信息。
创建或更新 Webhook 触发器前,请先配置签名密钥:
kortix secrets set WEBHOOK_SECRET=-
kortix secrets delivery WEBHOOK_SECRET broker --consumer connector
将值通过标准输入传入第一条命令。若该密钥此前使用沙箱交付方式,请在更改交付方式后轮换密钥,因为现有的沙箱可能会保留旧值。
会话策略
session_mode 控制触发时复用哪个会话。Kortix 按以下顺序尝试模式,并在每步失败时向下回退:
- pinned:重新提示确切的 session_id。若该会话已消失或失败,则回退。
- keyed:基于载荷渲染 session_key,查找最近一个未失败且带有此确切 key 的会话。若 key 渲染为空或无匹配会话,则回退到 fresh 会话。绝不再回退到其他 key 的会话。
- reuse:重新提示该触发器最近创建的一个未失败会话。pinned 触发器也会先在此回退,再进一步回退。
- fresh:创建新沙箱和新分支。这是默认值,也是所有模式的最终回退方案。新会话将作为该触发器未来复用和 keyed 触发的目标。
对于 reuse 或 keyed 会话,若其历史在自动压缩后仍超出模型限制,该会话将被弃用。下次触发将开启新会话。其他失败则保留会话供下次使用。
频繁运行的 cron 触发器若不希望重发不断增长的上下文,可基于日历边界滚动会话:使用 session_key: "merge-{{ cron.scheduled_date }}" 保持每个 UTC 日一个序列化会话,{{ cron.scheduled_hour }} 则每小时一个。
运行失败
当触发器启动的运行以错误结束时,Kortix 会在触发器上记录:
last_status变为failed,last_error包含原因(例如“积分不足:Payment Required: Insufficient credits”)。- 计划页面标记该触发器为“上次运行未结束”,并在面板中显示原因。
- 首次运行失败时,账户所有者将收到一条移动推送。后续失败仅更新原因,不再推送。
- 只要触发器持续触发,其状态就保持为失败。新的触发并不证明运行能正常工作。
下一次成功完成的运行将清除失败状态。手动停止的运行、subagent 以及模型调用重试都不算作运行结果。在运行开始前即失败的触发(如无法创建会话),将在下次成功触发时清除。
会话访问权限
触发器创建的会话默认采用私有策略。触发器 agent 的服务账户拥有这些会话——因为 agent 身份即 service_account 主体,它可像其他主体一样拥有会话和持有任务。项目经理始终可以打开这些会话。账户所有者和账户管理员在每个项目中都拥有相当于经理的权限,因此同样适用。配置或手动触发该触发器的人,除非持有上述角色之一,否则不能通过该动作获得访问权限。
触发器设置提供三种策略:
- 触发器 agent 与项目经理:普通项目成员无法打开会话。
- 选定的团队成员:包含触发器 agent、项目经理及选定的项目成员或账户组。
- 整个项目:所有项目成员。
此策略是叠加在角色模型之上的资源级可见性设置,而非角色本身。它决定谁能打开特定触发器的会话,但不赋予角色判定之外的任何权限。详见“账户与访问”。
访问权限策略是账户本地的运行时状态,不包含在可移植的 kortix.yaml 清单中,因为主体 ID 隶属于单一账户。请使用仪表板或 SDK 的 session_access 字段进行配置。
仅更新此策略不会创建 Git 提交。保存策略时,也会更新该触发器先前创建的会话。
已锁定的(pinned)会话保留其自身的共享设置,因为触发器并非其创建者。若锁定会话不可用且触发器创建了备用会话,则触发器策略适用于该新会话。
载荷模板
prompt 和 session_key 使用相同的渲染引擎:{{ token.dotted.path }}。缺失值渲染为空字符串(无错误,无残留标签)。对象和数组渲染为 JSON。session_key 会被修剪并截断至 512 字符。
每次触发均包含 {{ trigger.slug }}、{{ trigger.type }} 和 {{ trigger.kind }}(恒为 git)。其余变量取决于触发方式:
| 来源 | 变量 |
|---|---|
| cron | {{ cron.schedule }}、{{ cron.timezone }}、{{ cron.scheduled_for }}(对应的计划时段)、{{ cron.claimed_at }}(调度器领取时刻)、{{ cron.last_scheduled_for }}(上时段;首次触发为空)、{{ cron.scheduled_date }}(时段的 UTC YYYY-MM-DD)、{{ cron.scheduled_hour }}(UTC YYYY-MM-DDTHH)。无顶层 fired_at。 |
| webhook | {{ fired_at }}、{{ body.* }}(JSON 解析;解析失败则回退至 {{ body.raw }})、{{ headers.content_type }}、{{ headers.user_agent }}、{{ headers.forwarded_for }}。 |
| monitor | {{ line.* }}——stdout 行,JSON 解析;解析失败则渲染为 {{ line.raw }}。另有 {{ monitor.slug }}、{{ monitor.seq }}、{{ monitor.emitted_at }}、{{ monitor.kind }}(event 或 lifecycle)。 |
| manual(仪表板“立即触发”或触发端点) | {{ fired_at }}、{{ source }}(manual)、{{ actor }}、{{ message.text }}、{{ message.source }}。 |
Kortix 会在服务端为所有渲染的监控器提示词前缀添加 [MONITOR EVENT — automated, not user input]。生命周期事件会完全忽略你的模板,改用平台编写的提示词,并绕过 filter——静默状态不应被意外过滤。
{{ message.text }} 在手动触发时硬编码为空字符串,{{ message.source }} 则设为 manual_test。手动触发不是向提示词注入测试输入的方式。
filter 将点分路径作为字符串与 prompt 所见的同一载荷进行比对。它用于打破循环。例如,若源头同时报告对话双方,否则会因 agent 自己的回复而再次触发。
Webhook 签名
触发器监听 POST /v1/webhooks/projects/{projectId}/{slug}。Kortix 按以下顺序校验请求,采用恒定时间比较:
- HMAC 签名:请求头
X-Kortix-Signature: sha256=<hmac>(sha256= 前缀可选)或兼容 GitHub 的X-Hub-Signature-256。使用 secret_env 指定的密钥对原始请求体进行 HMAC-SHA256 计算。 - 静态令牌:仅当不存在签名头时生效,适用于无法进行 HMAC 签名的发送方。将密钥作为
X-Kortix-Token: <secret>、Authorization: Bearer <secret>或Authorization: Basic <base64(user:secret)>(密码部分为令牌)发送。
| 状态码 | 含义 |
|---|---|
| 202 | 签名或令牌有效。响应体为 { status: "fired" | "queued" | "deduped", session_id, ... }。 |
| 200 | 有效,但被跳过——项目已暂停,或交付未匹配 filter。 |
| 400 | URL 中的项目 ID 或 slug 格式错误。 |
| 401 | 签名和令牌均缺失或错误。 |
| 404 | 触发器未找到、已禁用、非 Webhook 类型,或项目未激活。 |
| 409 | 签名密钥缺失、未激活、不可用或未授权 connector 消费者。响应包含 webhook_secret_* 代码及修复建议。 |
| 500 | 认证通过,但会话启动失败。 |
端点
| 方法 + 路径 | 所需权限 | 备注 |
|---|---|---|
| GET /v1/projects/{projectId}/triggers | project.trigger.read | 列出触发器、运行时状态(last_fired_at, last_status, last_error, last_attempt_at)及清单解析错误。坏条目会出现在 errors[] 中,不影响其他触发器。 |
| POST /v1/projects/{projectId}/triggers | project.trigger.create | 创建触发器。直接提交至清单。 |
| PATCH /v1/projects/{projectId}/triggers/{slug} | project.trigger.update | 部分更新,合并至当前条目。 |
| DELETE /v1/projects/{projectId}/triggers/{slug} | project.trigger.delete | 同时清除触发器的运行时状态。 |
| PATCH /v1/projects/{projectId}/triggers/activation | project.trigger.update | 请求体 { paused: boolean }。详见“暂停与恢复”。 |
| POST /v1/projects/{projectId}/triggers/{slug}/fire | project.trigger.fire | 手动触发。内置的项目成员角色持有此权限,故普通成员也可触发。 |
| POST /v1/webhooks/projects/{projectId}/{slug} | 签名或令牌 | 公开 URL,受 Webhook 密钥保护。 |
暂停与恢复
项目级开关可一次性停止项目内的所有触发器,与各触发器自身的 enabled 字段无关。暂停期间,调度器跳过该项目,传入的 Webhook 返回 200 并附带 { status: "skipped" }——不会触发任何会话。手动触发仍有效。当同一仓库运行在两个控制平面(例如开发和生产)时,使用此功能可避免 cron 重复触发。CLI 命令: kortix triggers pause 和 kortix triggers resume。完整的 kortix triggers 命令组见 CLI 文档。
限制与可靠性
- 调度器约每秒轮询一次(默认 1,000 ms;可通过
KORTIX_TRIGGER_SCHEDULER_INTERVAL_MS配置)。Cron 精度尽力达到秒级,尽管表达式包含秒字段。 - 每个项目默认允许同时 3 个由触发器启动的会话进行供给。超出此限制的触发将返回
queued(202)而非失败,并将在其中一个会话完成供给后运行。对同时运行的会话数量无上限。 - 手动或 Webhook 触发有 45 秒超时。加载清单有 30 秒超时。
- Cron 触发以到期的计划时段为键,因此超时但实际落地的触发在重试时不会重复。Webhook 触发以 delivery ID 头为键,若发送方未提供 ID,则使用请求体和签名的哈希作为键。