トリガー

トリガーは workflow がいつ発火するかを決めます。workflow は on: の下で宣言します。 1 つの workflow に複数持たせられ、それぞれをほかに触れずに個別に切れます。トリガーが 何であれ、workflow はいつでも手動で開始できます — アプリの ▶ 実行、または lk run <workflow>

トリガー発火のきっかけ必要なもの
schedulecron 式(分単位 — CI ではなく自分のマシン)
file-watch監視対象の glob 下でファイルが作成・変更・削除される
gitローカルリポジトリのイベント: commit、ブランチ作成git リポジトリ
run-completed別の workflow の完了(done または failed)
manualあなた自身。lk run またはデスクトップアプリから
githubissue、コメント、pull request、レビュー、push、CIConsole
slackメンション、DM、チャンネル内のキーワード、絵文字Console
webhook自分で発行した専用 URL への POSTConsole

schedule

on:
  schedule:
    - cron: "*/15 * * * *"

dailyhourlyweekly といったエイリアスも使えます。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_numbercomment_id、Slack メッセージは channelts・場合によって 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 の投稿で発火し、そのチャンネルで 人が話していても発火しません。fromfrom_not を併記すれば、人も自分の条件で戻せます。 { from_not: [noisy-colleague], from_bot: ["B0SENTRY"] } は「その人以外の全員と、その bot」です。 行為者を見ないフィルタ(keywords / mentions / labels / channels)はどちらの場合も効きます。

onmy-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 に書けます。 onmy-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 します。

フィルタイベント照合先
actionsaction を持つすべてのイベント(push などの ref 系は持たない)イベントの actionopened / labeled / review_requested / completed など
labelsissue・issue comment・pull request・discussion(とそのコメント)labeled / unlabeled では今まさに付け外しされたラベル。それ以外の action では issue / PR / discussion が現在保持しているラベル
assigneeissue と pull requestissue / pull request の assignee
reviewerpull request の review_requestedreview_requested が指名したレビュアー
workflowsworkflow_runworkflow_run の workflow 名またはファイルパス
conclusionsworkflow_runworkflow_run の conclusion。completed のときだけ照合し、requested / in_progress は当たらない
branchespush / create / delete / workflow_run / pull requestworkflow_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 イベントであれば ここに書いた名前で購読できますが、各イベントが受け付けるのは照合できるフィルタだけです。 照合できないフィルタ(issuesreviewerpushconclusions など)は、登録して 黙って当たらないままにせず、警告を出して受け付けません。

素の gh-aw イベントは、フィルタなしの同じ購読の短縮形です。on: issueson: [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アイテムの actioncreated / 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

イベント発火のきっかけ
mentionloopkeep アプリが @ で呼ばれた
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…>。アプリではなくあなた自身が名指しされたのを拾うのはこれです)。 emojireaction だけ、keywordsmentions は本文を読むので message / mention / dm で使えます。合わないイベントに書く(mentionemojireactionkeywords など)と、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 回動かすのは たいてい望むところではありません。トリガー種別ごとに既定があります:

トリガー既定理由
schedulelatest逃した cron の tick は 1 つにまとまる
file-watchlatest連続した保存は 1 つの変更
gitallcommit はそれぞれに意味がある
run-completedall一環を飛ばすと連鎖が壊れる

トリガーごとに 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-completeddispatch-workflow の発火は、親 run の連鎖深さに 1 を足した値を 引き継ぎ、上限は 5 です。それより深い発火は拒否され、監査されます。これがループ防止の モデルのすべてです — 設定する DAG エンジンはなく、暴走する自己トリガーを止める 深さの上限だけです。

止める

1 つの workflow の自動トリガーは、その ワークフロー カードのスイッチで切り替えます。 プロジェクト設定 → すべてのトリガーを停止 は、ポリシー画面で再開するまで、1 つの project の自動発火を止めます。設定 → すべての自動化を一時停止 は、すべての project を 横断したグローバルな kill switch です。

どちらも今後の発火にだけ効きます。手動の run と、すでに実行中の run は動き続けます。 グローバルの pause は 信頼 とは独立で、どの workspace の信頼 状態にも触れません。