トリガー
トリガーは workflow がいつ発火するかを決めます。workflow は on: の下で宣言します。
1 つの workflow に複数持たせられ、それぞれをほかに触れずに個別に切れます。トリガーが
何であれ、workflow はいつでも手動で開始できます — アプリの ▶ 実行、または
lk run <workflow>。
| トリガー | 発火のきっかけ | 必要なもの |
|---|---|---|
schedule | cron 式(分単位 — CI ではなく自分のマシン) | — |
file-watch | 監視対象の glob 下でファイルが作成・変更・削除される | — |
git | ローカルリポジトリのイベント: commit、ブランチ作成 | git リポジトリ |
run-completed | 別の workflow の完了(done または failed) | — |
manual | あなた自身。lk run またはデスクトップアプリから | — |
github | issue、コメント、pull request、レビュー、push、CI | Console |
slack | メンション、DM、チャンネル内のキーワード、絵文字 | Console |
webhook | 自分で発行した専用 URL への POST | Console |
schedule
on:
schedule:
- cron: "*/15 * * * *"
daily、hourly、weekly といったエイリアスも使えます。GitHub Agentic Workflows に
合わせています。schedule の cron は UTC で評価します。個人の digest と quiet-hours の設定は、
代わりに daemon のマシンのシステムローカル時刻を使います。
file-watch
on:
file-watch:
paths: ["inbox/**/*.pdf"]
events: [create, modify] # create | modify | delete
debounce_ms: 2000 # 連続した保存を 1 回の発火にまとめる
min_size_kb: 1 # 空、または書き込み途中のファイルを弾く
git
on:
git:
events: [commit, branch-created]
branch: ["main", "release/*"] # glob
author_not: ["loopkeep[bot]"] # 自分の自動 commit で再発火しない
これらはローカルリポジトリのイベントです — forge も webhook もありません。
author_not は、workflow が自分の commit で自分自身をトリガーするのを防ぐ定番の
ガードです。値は commit author 名への素のグロブです — commit には bot の印が
無いので、*bot* のようなパターンは abbott という名前の人まで落とします。
ワイルドカードではなく loopkeep[bot] のように除きたい author を名指ししてください。
run-completed
on:
run-completed:
workflow: "test-fixer"
status: [failed] # done | failed
if: event.output.attempts >= 3 # 任意の絞り込み
別の workflow の完了を購読します。上流の run の
構造化出力 は if: フィルタで event.output として使え、
新しい run のトリガー context に注入されます。
リモートトリガー(GitHub / Slack / webhook)
GitHub のイベント、Slack のメッセージ、自分で発行した webhook は、Console 経由で
daemon に届きます。各アプリが on: の下の名前空間になり、その下のキーが購読する
イベントです:
on:
github:
repos: ["org/other-repo"] # 省略するとこの workspace の git origin に bind
issues: { actions: [labeled], labels: [needs-triage] }
issue_comment: { mentions: me, on: my-pr, from_not: me }
pull_request: { actions: [review_requested], reviewer: me }
workflow_run: { workflows: [ci], branches: [main], conclusions: [failure] }
push: { branches: [main] }
pull_request_review_comment: { from_bot: ["renovate[bot]"] } # bot は挙げたものだけが起こす
slack:
channels: ["C0123ABCD"] # 任意。slack ブロック全体に効く
mention: {} # loopkeep アプリが @ で呼ばれた
dm: {} # アプリへのダイレクトメッセージ
message: { mentions: me } # アプリが見えるチャンネルで、あなたが名指しされた
reaction: { emoji: [pushpin], on: my-message }
webhook: # 値なし = このアカウントのすべての hook URL
gateway が届いたイベントをこの宣言と突き合わせ、一致したものだけを配送します。購読して いないイベントがマシンに届くことはありません。daemon は gateway が一致させたものをその まま起動し、イベントを照合し直さないので、ここに書いた内容が購読のすべてです。
現在の会話を集める
gateway が起点 locator を渡した場合、対応する GitHub または Slack の発火には
trigger.resource として入ります。GitHub の issue と pull request は number、comment は
issue_number と comment_id、Slack メッセージは channel・ts・場合によって
thread_ts を持ちます。イベント本文だけに頼らず、その座標をプロンプトで使えます。
gateway から locator が渡らなければ、そのテンプレートパスは空の値に展開されます。
作業する前に issue と現在の comment を読んでください:
gh issue view {{ trigger.resource.number }} --comments
Slack にはローカルで遅延取得する補助があります。C で始まる channel ID で発火する workflow に、
次の正確なトークンを書きます。
返信する前に会話を読んでください:
{{ trigger.thread }}
取得を始めるのは正確な {{ trigger.thread }} パスだけで、{{ trigger }} では始まりません。
ペアリング済み daemon が bot token を受け取り、ローカルから Slack の
conversations.replies を呼び、エージェントを起動する前に最大 200 メッセージのページを 2 ページ
差し込みます。各行は {user_id}: {text} 形式です(Slack が user ID を返さない bot では bot ID、
それも無ければ bot が先頭に入ります)。さらにページがある場合、末尾に
[Slack thread truncated after the first 2 pages (up to 400 messages).] を付けます。
判定するのは現在の公開状態ではなく ID の接頭辞です。公開として作成されたチャンネルは C ID を持ち、
あとで private に変えても接頭辞は残ります。private として作成されたチャンネルの ID と DM の ID、
8 秒の timeout、そのほかの取得失敗では空文字になります。取得した thread の本文は信頼できない入力として
扱ってください。run 自体は続きます。resource の全形は
frontmatter リファレンス を参照してください。
4 つの述語
どのイベントも、固有のフィルタに加えて同じ 4 つの問いを取ります:
| 述語 | 問い | 例 |
|---|---|---|
from / from_not | どの人がやったか | from_not: [octocat] |
from_bot | どの bot なら起こしてよいか | from_bot: ["renovate[bot]"] |
mentions | 本文が誰を名指ししているか | mentions: me |
on | 誰のものに起きたか | on: my-pr |
値は me かリテラル(GitHub の login、Slack の user ID)で、1 つならリストにしなくて
構いません。from / from_not は * をワイルドカードとして使え、当たるのは人だけです。
from_not にどう書いても bot を締め出すことはできませんし、通すこともできません。
bot は 1 か所にまとめました。bot が何をしても、その bot を from_bot に挙げていない限り
workflow は動きません(from_bot: ["*"] ですべての bot)。GitHub では bot アカウントの
login(renovate[bot]、dependabot[bot] など)を書きます。GitHub がそのアカウントを Bot と
印を付けているので、abbott のような人が間違われることはありません。Slack ではそのイベントに
載っている bot の識別子ならどれでも構いません —— bot ID(B…)、app ID(A…)、bot の
user ID(U…)のうち、手に入るものを書いてください。プロフィールからコピーできるメンバー ID が
いちばん簡単です。
1 つのフィルタセットの中では、どの人が起こすかを from / from_not が、どの bot が起こすかを
from_bot が決めます。bot だけを挙げて人を挙げていないセットは bot 専用の購読になります ——
{ channels: ["C0ALERTS"], from_bot: ["B0SENTRY"] } はその bot の投稿で発火し、そのチャンネルで
人が話していても発火しません。from か from_not を併記すれば、人も自分の条件で戻せます。
{ from_not: [noisy-colleague], from_bot: ["B0SENTRY"] } は「その人以外の全員と、その bot」です。
行為者を見ないフィルタ(keywords / mentions / labels / channels)はどちらの場合も効きます。
on は my-pr / my-issue / my-message を取ります。他人のものを指すときは、対象を表すキーで
オブジェクト形を書きます。
| トークン | オブジェクト形 | 指す対象 |
|---|---|---|
my-pr | { pr: octocat } | GitHub の pull request |
my-issue | { issue: octocat } | GitHub の issue |
my-message | { message_from: U0123ABCD } | reaction が付いた Slack のメッセージ |
使えるキーはこの 3 つだけで、トークンも my-pr / my-issue / my-message で全部です。
それ以外を書くと(on: my-prs のような書き間違いや、{ message: U0123ABCD } のような
知らないキー)、loopkeep は何に一致させたいのか判断できないので、その条件は登録せずに警告を
出します。フィルタだけが黙って消えて全員のメッセージに当たる、ということにはならず、
トリガーはそもそも発火しません。1 つのイベントに条件を複数並べている場合は、読めなかった
ものだけが落ち、残りはそのまま登録されます。実際にどう登録されたかは
lk trigger explain で確認できます。
me は、アプリを接続したときに紐付いた identity(GitHub の login、Slack の user ID)です。
from / from_not / mentions と、イベントごとの assignee / reviewer に書けます。
on の my-pr / my-issue / my-message が指しているのも同じ identity です。それ以外の
欄には値そのものを書きます。紐付いていない間、me は全員に一致するのではなく何にも一致
しません。このマシンでどう解決されるかは lk trigger explain で確認できます。
デスクトップでは、これらの欄に加えて repos: / labels: / workflows: / actions: /
conclusions: も選択式です。打ち込んで目的のリポジトリ・相手・チャンネル・ラベル・
workflow・action を選ぶと、ファイルに書かれるのは値そのもの(Slack の user ID・
channel ID、GitHub の login、ラベル名・workflow 名)になります。値を調べに行く必要は
ありません。直接入力もそのまま使えるので、octo* のようなワイルドカードや bot の識別子は
そのまま書けます。一覧を取得できないときは、その欄だけただの入力欄に戻ります。
レシピ: アラート bot の投稿をトリアージする
bot を通したくなる理由は、たいてい監視系です —— Sentry・Datadog・デプロイ通知が、自分の見て いるチャンネルへ流れてくる:
on:
slack:
message: { channels: ["C0ALERTS"], from_bot: ["B0SENTRY"] }
x-loopkeep:
coalesce: 5m
このセットはチャンネルと bot だけを名指ししているので、workflow はアラートで起き、その周りの
会話には反応しません。bot のメンバー ID は Slack のプロフィールからコピーできます。どのアプリが
投稿しているか分からないうちは from_bot: ["*"] から始めても構いません。窓を書いておくと、
障害でアラートが連発しても run は 1 本にまとまります —— スレッドへ返信する workflow では
書かないでください(1 run が返せるのは 1 回だけです)。
GitHub でも形は同じです。pull_request: { actions: [opened], from_bot: ["dependabot[bot]"] } は
依存更新の bot が立てた pull request だけで発火します。
GitHub
repos: はイベントと並べて書き、その下のすべてのイベントに効きます。省略すると、
workspace は git の origin が指すリポジトリに設定なしで bind します。
| フィルタ | イベント | 照合先 |
|---|---|---|
actions | action を持つすべてのイベント(push などの ref 系は持たない) | イベントの action — opened / labeled / review_requested / completed など |
labels | issue・issue comment・pull request・discussion(とそのコメント) | labeled / unlabeled では今まさに付け外しされたラベル。それ以外の action では issue / PR / discussion が現在保持しているラベル |
assignee | issue と pull request | issue / pull request の assignee |
reviewer | pull request の review_requested | review_requested が指名したレビュアー |
workflows | workflow_run | workflow_run の workflow 名またはファイルパス |
conclusions | workflow_run | workflow_run の conclusion。completed のときだけ照合し、requested / in_progress は当たらない |
branches | push / create / delete / workflow_run / pull request | workflow_run の head branch、push / create / delete のブランチ、または pull request の取り込み先(base)ブランチ。glob が使えます |
mentions は issue・pull request・コメント・レビューの本文から @name を読むので、
commit コメントと discussion コメントにも効きます。from / from_not / from_bot は
イベントの行為者を見て、すべてのイベントで使えます —— 例外は下の projects_v2_item だけで、
これは専用の 2 つしか取りません。共有 App が受け取る GitHub イベントであれば
ここに書いた名前で購読できますが、各イベントが受け付けるのは照合できるフィルタだけです。
照合できないフィルタ(issues の reviewer、push の conclusions など)は、登録して
黙って当たらないままにせず、警告を出して受け付けません。
素の gh-aw イベントは、フィルタなしの同じ購読の短縮形です。on: issues、
on: [issues, push]、on.issues.types はいずれも on.github.issues を購読します。
フィルタは on.github.<event> の下に書きます — on.issues 配下の gh-aw のキーは上流の
意味のままで、loopkeep のフィルタとしては読みません。
プロジェクトボード(projects_v2_item)
loopkeep は GitHub イベントを Actions ではなく自前の App で受け取るので、Actions ではまったく
起動できない projects_v2_item —— Projects (v2) ボードのアイテムが追加・移動・編集・削除された
とき —— も購読できます。専用のフィルタが 2 つあり、どちらも粗い粒度です。上の issue /
pull request 向けのフィルタはボードのアイテムには効きません。
| フィルタ | 照合先 |
|---|---|
actions | アイテムの action — created / edited / deleted / reordered / converted / archived / restored |
content_type | アイテムの中身 — Issue / PullRequest / DraftIssue |
on:
github:
projects_v2_item: { actions: [edited], content_type: [Issue] }
フィールドの値で絞ること(「Status が In Progress になったとき」など)はトリガーの段階では
できません。webhook の payload はフィールド名やオプション名ではなく node ID を運ぶので、名前と
照合する材料がないためです。代わりにトリガーは action で発火します。issue や pull request の
{{ trigger }}.body が読めるタイトルと本文になるのと違い、ボードのアイテムには自然な本文が無いので、
{{ trigger }}.body には webhook の生の JSON が入ります。これは意図的で、エージェントはそこから
payload の content_type と node ID を読み(必要なら API で引いて)何をするか決めます。
このイベントには、loopkeep の GitHub App を入れた人による設定が 1 つ要ります。App に
Projects (read) 権限を与え、設定で projects_v2_item イベントを購読することです。そうしないと
webhook が loopkeep に届かず、トリガーは沈黙したままになります。
Slack
| イベント | 発火のきっかけ |
|---|---|
mention | loopkeep アプリが @ で呼ばれた |
dm | アプリへのダイレクトメッセージ |
message | アプリが見えるすべてのメッセージ(DM 含む)— フィルタが 1 つ必要 |
reaction | 絵文字リアクションが付いた / 外れた |
channels / from / from_not はイベントと並べて書き、そのブロックのすべてのイベントの既定に
なります。フィルタセットの中に同じキーを書けば、そのセットではそちらが使われます。ブロックが既定、
セットが上書きです。from_bot だけはここに書けません —— それを効かせたいフィルタセットの中に
書きます。ブロック直下に bot の allowlist を置くと、その下のすべてのイベント(dm: {} も含めて)が
黙って bot 専用の購読に変わってしまうためです。
on:
slack:
channels: ["C0123ABCD"] # 以下すべてのイベントの既定
mention: {} # C0123ABCD を引き継ぐ
message:
- { channels: ["C0999ZZZZ"], keywords: [deploy] } # このセットだけ C0123ABCD の代わりに
- { mentions: me } # C0123ABCD を引き継ぐ
channels はチャンネル ID だけを取ります(C / G / D で始まる文字列。チャンネルの
詳細からコピーできます)。#ops のような名前は解決できないので、警告して受け付けません。
イベントごとのフィルタは keywords(本文の部分一致・大文字小文字を区別しないので、単語を
空白で区切らない言語でも効きます)、emoji(コロン抜きのリアクション名)、mentions
(本文中の <@U…>。アプリではなくあなた自身が名指しされたのを拾うのはこれです)。
emoji は reaction だけ、keywords と mentions は本文を読むので message /
mention / dm で使えます。合わないイベントに書く(mention の emoji、reaction の
keywords など)と、GitHub と同じく警告して受け付けません。from / from_not /
channels はどの Slack イベントでも使えます。from_bot も同じですが、書く場所は
そのイベントのフィルタセットの中です。
絞り込みの無い message はアプリが見えるすべての発言に当たってしまうので、channels /
keywords / mentions / from / from_bot のどれかが最低 1 つ必要です。from_not だけでは
数えません —— 何人か除いたところで、ほとんどの発言は残るからです。何も絞れていない宣言は、
登録せずに警告します。
webhook
URL を発行して、workflow でその名前を指定します:
lk hooks add deploy-done # URL をこのときだけ表示する
lk hooks # 名前と作成日だけ。URL は出ない
lk hooks rotate deploy-done # 名前はそのままで URL を作り直す(workflow は書き換え不要)
lk hooks rm deploy-done # 以後、その URL への POST は拒否される
デスクトップアプリからも発行できます。workflow の webhook トリガーのカードに名前を入れると、 URL を一度だけ表示し、その名前を宣言に追加します。
on:
webhook:
- deploy-done # スカラ形
- name: nightly-report # オブジェクト形
URL 自体が資格情報です。それを知っていることが起床させる資格になるので、発行時にだけ表示
され、あとから一覧に出ることはありません。漏れたら rotate してください。webhook トリガーは
フィルタを取りません(gateway は任意の payload を解釈できないため)。POST された本文は
そのまま {{ trigger }} に入ります。空のリストと secret: を伴う宣言は、どちらも警告して
無視します — 送信側の署名鍵を検証できる主体が居ないためです。
発火しないもの
- 挙げていない bot。 bot が何をしても、それだけで workflow が起きることはありません ——
dependabot の pull request も、renovate のコメントも、Slack に流れるアラートもです。起こして
ほしい bot を
from_botに挙げるか、from_bot: ["*"]ですべてを通します。 - loopkeep 自身の bot。 loopkeep の GitHub App が起こしたイベントは照合の前に捨てられる
ので、run が投稿したコメントでその run がまた起きることはありません。これだけは戻せません ——
from_botに書いても通りません。 - 見えないもの。 private なリポジトリ・チャンネルのイベントは、そのリポジトリを見られる アカウント / そのチャンネルに居るアカウントにだけ配送されます。判断できなかったときは、 配送せずに捨てます。
- loopkeep が読めない宣言。 空のリストを書いたイベント(
issues: [])は何にも一致せず、on:に知らないトークンやキーを書いた条件はそもそも解釈できません。どちらも登録されず、 警告として報告されます —lk trigger explainがトリガーと並べて表示します。
bot の条件には、見た目どおりには働かない書き方が 4 つあります。いずれも登録せず、書き直し方を 添えて警告します:
| 書き方 | 働かない理由 |
|---|---|
from: ["renovate[bot]"] | from は人しか照合しないので、誰にも当たりません。from_bot: ["renovate[bot]"] と書きます |
from_bot: [] | 空の allowlist は 1 つも通さない = 書かないのと同じです。通したい bot を挙げるか from_bot: ["*"] にします |
from_not: ["*bot*"] | bot は from_bot に挙げない限り発火しないので除く相手が居ないうえ、ただの glob として abbott のような人も落とします |
Slack のイベントと並べた from_bot | ブロック直下の allowlist は、その下のすべてのイベント(dm: {} も含む)を bot 専用にしてしまいます。効かせたいセットの中に書きます |
3 つ目は以前 bot を除く定番の書き方で、loopkeep が Slack のトリガーへ自動で入れていたものです。
その綴りも自動の注入も今はありません —— 書いたものがそのまま動き、この綴りは黙って無視される
のではなく報告されます。読めない on: と同じく、この 4 つのどれかを含むフィルタセットは丸ごと
落とします(絞ったつもりの購読が黙って広がらないように)。同じイベントの他のセットはそのまま
登録されます。
アプリのインストール、identity の紐付け、hook URL の発行といった設定は Consoleとは にあります。
フィルタ
構造化フィルタ(上記のトリガーごとのキー)がほとんどの条件を扱います。残りには
if: があります。これは意図的に小さくした式言語で、event. のパス、比較
(== != > >= < <=)、contains、&& || !、そして括弧が使えます。
関数呼び出しもコード実行もありません — 評価は決定的です。フィルタで弾かれた発火も
記録され(filtered: true)、なぜ動かなかったのかを必ず確認できます。
集約
マシンが眠っている間に毎晩の発火を 3 回逃したとして、朝に溜まった分を 3 回動かすのは たいてい望むところではありません。トリガー種別ごとに既定があります:
| トリガー | 既定 | 理由 |
|---|---|---|
schedule | latest | 逃した cron の tick は 1 つにまとまる |
file-watch | latest | 連続した保存は 1 つの変更 |
git | all | commit はそれぞれに意味がある |
run-completed | all | 一環を飛ばすと連鎖が壊れる |
トリガーごとに coalesce: latest または coalesce: all で上書きします。まとめられた
発火は、何件のイベントが畳み込まれたかを記録します。
GitHub・Slack・webhook のバースト
リモートトリガーは既定ではまとめません。購読した GitHub のイベント・Slack のメッセージ・webhook は、 1 件ずつが「あなたに起きた別々のこと」なので、それぞれが run になります —— 届かなかった run は、 多すぎる run より気付きにくいからです。個々に反応するのではなく大量のイベントを見張る workflow では、 次のように窓を宣言すると、その窓に入った発火が 1 つにまとまります:
x-loopkeep:
coalesce: 5m # 30s / 5m / 2h。書かなければイベントごとに 1 run
窓の中では 1 件目がすぐ走り、残りは待たされて、窓が明けたら 1 本にまとまって走ります。捨てられるものは 無く、run はまとめられたイベントを全件受け取ります。発火元のスレッドへ返信する workflow には書かない でください —— 1 run が返せるのは 1 回なので、まとめると誰かへの返事が消えます。
この窓が効くのは GitHub・Slack・webhook の発火だけです。ローカルのトリガー(schedule /
file-watch / git / run-completed)は窓をまったく見ず、上の表のトリガーごとの
coalesce: latest | all を使い続けます。両方を持つ workflow は両方が効きます。窓は Console から
届いたものをまとめ、2 語のほうはローカルに溜まった発火を流し直します。
マシンがオフラインの間に溜まった分は別の扱いで、窓の宣言も要りません —— Console のガイドを参照してください。
連鎖には深さの上限がある
run-completed と dispatch-workflow の発火は、親 run の連鎖深さに 1 を足した値を
引き継ぎ、上限は 5 です。それより深い発火は拒否され、監査されます。これがループ防止の
モデルのすべてです — 設定する DAG エンジンはなく、暴走する自己トリガーを止める
深さの上限だけです。
止める
1 つの workflow の自動トリガーは、その ワークフロー カードのスイッチで切り替えます。 プロジェクト設定 → すべてのトリガーを停止 は、ポリシー画面で再開するまで、1 つの project の自動発火を止めます。設定 → すべての自動化を一時停止 は、すべての project を 横断したグローバルな kill switch です。
どちらも今後の発火にだけ効きます。手動の run と、すでに実行中の run は動き続けます。 グローバルの pause は 信頼 とは独立で、どの workspace の信頼 状態にも触れません。