ワークフローの frontmatter
workflow の YAML frontmatter がサポートするすべてのキーです。GitHub Agentic
Workflows と共有するキーは上流の意味を保ちます。loopkeep 独自の設定は x-loopkeep
の下に置きます。未知のキーは警告付きで保持されます。
on — トリガー
on:
schedule:
- cron: "0 3 * * *" # daily/hourly/weekly といったエイリアスも使える
file-watch:
paths: ["inbox/**/*.pdf"]
events: [create, modify] # create | modify | delete
debounce_ms: 2000
min_size_kb: 1
coalesce: latest # latest | all
git:
events: [commit, branch-created]
branch: ["main", "release/*"]
author_not: ["loopkeep[bot]"]
run-completed:
workflow: "test-fixer"
status: [failed] # done | failed
if: event.output.attempts >= 3
manual: {}
github: # GitHub イベント、Console 経由で届く
issues: { assignee: me, labels: [bug], actions: [opened] }
issue_comment: { mentions: me } # mentions / from / from_not は me か login
pull_request_review_comment: { from_bot: ["renovate[bot]"] } # bot は挙げたものだけが起こす
pull_request: { actions: [review_requested], reviewer: me } # reviewer = 依頼された宛先
pull_request_review: { on: my-pr } # on: my-pr | my-issue、他人は { pr: <login> }
workflow_run: { workflows: [ci], branches: [main], conclusions: [failure] }
push: { branches: [main] } # payload.ref の branch で照合
projects_v2_item: { actions: [edited], content_type: [Issue] } # ボードのアイテム。App の Projects 権限が要る
repos: ["org/other-repo"] # 省略すると、この workspace の origin リモートに bind する
slack: # Console 経由で届く
mention: {} # mention | dm | message | reaction
message: { keywords: [deploy], mentions: me } # フィルタが最低 1 つ必要
reaction: { emoji: [pushpin], on: my-message } # 他人なら { message_from: <user id> }
channels: ["C0123ABCD"] # チャンネル ID のみ(C / G / D 始まり)。#ops のような名前は照合されない
webhook: # 値なし = このアカウントのすべての hook URL
- deploy-done # スカラ形。オブジェクト形は { name: deploy-done }
on.schedule の cron は UTC で評価します。個人の digest と quiet-hours の設定は、代わりに
daemon のマシンのシステムローカル時刻を使います。
イベントの値はフィルタセットのリストにもでき、これは OR です。どれか 1 セットに当たれば 発火し、セットの中のフィルタは従来どおり同時に満たす必要があります。
on:
slack:
message:
- { channels: [C0123ABCD], keywords: [deploy] }
- { mentions: me }
- { channels: [C0ALERTS], from_bot: ["B0123ALERT"] } # このセットはその bot だけで発火する
外部イベント(github / slack / webhook)は同じ形です。各アプリが名前空間になり、その下のキーが
購読するイベント、各イベントにフィルタを書きます。多くのフィルタはリスト(assignee / labels /
actions / mentions / from・from_not・from_bot / reviewer / keywords / emoji、workflow_run の
workflows・branches・conclusions)です。各イベントが受け付けるのは、その payload で照合できる
フィルタだけです —— labels は issue・pull request・discussion(とそのコメント)、branches は
push / create / delete / workflow_run と pull request の取り込み先(base)ブランチ、reviewer
は pull request の review_requested、emoji は Slack の reaction、keywords と mentions は
Slack の message / mention / dm。照合できないフィルタは、登録して黙って当たらないままにせず、
警告を出して受け付けません。on.slack.message は keywords / mentions /
channels / from / from_bot のどれかが最低 1 つ必要で、無いとアプリから見えるすべてのメッセージに
当たってしまうため警告して無視します(from_not だけでは数えません)。bot は、そのフィルタセットが
from_bot に挙げていない限り何も起こしません(from_bot: ["*"] ですべての bot)。from / from_not は
人しか照合せず、bot だけを挙げて人を挙げていないセットはその bot 専用になります。from_bot は
フィルタセットの中に書くもので、Slack のイベントと並べては書けません —— そこに置くとブロック配下の
すべてのイベントが bot 専用になってしまうためです。ほかに、絶対に発火しない 3 つの書き方も警告して
受け付けません: from に書いた bot アカウント(renovate[bot])、空の from_bot、そして以前の bot
除けだった from_not: ["*bot*"](loopkeep はもう自動で入れず、ただの glob として abbott のような人も
落とします)。所有フィルタ on: はトークン —— my-pr / my-issue
(GitHub)や my-message(Slack の reaction)—— で自分の対象物に絞ります。他人を指すときは、対象を
表すキーでオブジェクト形を書きます — { pr: octocat }、{ issue: octocat }、
{ message_from: U0123ABCD } の 3 つだけです。キーもトークンもこれで全部で、それ以外は解釈できない
ため、その条件は登録されず警告になります(広く当たるようにはなりません)。空のリストを書いた
イベント(issues: [])も、同じ理由で何にも一致せず警告になります。
me は連携済みの GitHub / Slack アカウントに解決されます。Slack の
channels はチャンネル ID(C / G / D 始まり。チャンネル詳細からコピー)で、#ops のような名前は照合されず
警告になります。素の gh-aw イベント(on: issues)はフィルタなしの on.github.issues の短縮形として
受理されます。フィルタは daemon に届く前に Console 側で評価されるので、購読していないイベントが
配送されることはありません。
on.github.projects_v2_item は Actions では起動できない webhook 専用のイベントで、loopkeep の
自前の App から届きます。専用のフィルタは 2 つ —— actions と content_type
(Issue / PullRequest / DraftIssue)—— でフィールドの値では絞れません。payload が
フィールド名やオプション名ではなく node ID を運ぶためで、payload 全体が {{ trigger }}.body
(ボードのアイテムには自然なタイトルや本文が無いため、生の JSON)としてエージェントに届きます。
App に Projects (read) 権限と projects_v2_item の購読が要ります ——
トリガーガイド を参照してください。
on.webhook は値なし(このアカウントのすべての hook URL)か、エントリのリストを取ります。
エントリは hook 名か { name: … } です。空のリストは意図が読めないので警告して無視し、
secret: を伴う宣言も同じく無視します — 送信側の署名鍵を検証できる主体が居ないためです。
URL の発行と作り直しは lk hooks で行います。
意味論 — フィルタ、集約の既定、連鎖の深さ — は トリガーガイド で扱っています。
engine
engine:
id: claude # gh-aw の engine キー。loopkeep が動かすのは claude エンジンだけ
model: claude-sonnet-5
params: {} # engine 固有のパラメータ
permission_mode: auto # engine 自身の prompt の振る舞い。loopkeep のゲートはどのモードでも有効
engine は gh-aw の意味のままですが、loopkeep が動かせるのは claude エンジン
(Claude Code CLI をサブプロセスとして動かす)だけです。engine を省略しても run は
claude で動きます(gh-aw の既定は copilot ですが loopkeep はそれを動かさないので、
notice がログに出ます)。それ以外の engine id は run 開始時にエラーで失敗し、黙って
claude に差し替えることはしません。
bypassPermissions は明示的にオプトインしない限り拒否されます。plan は run には
使いません。gh-aw が定義しない engine id を使うときは、x-loopkeep
の下に engine オーバーライドを書けます。
tools
tools: [edit, bash, web-fetch]
workflow が使ってよいエージェントツールの部分集合です。サポート外のツールは警告 されます。
permissions
ローカルの safe-outputs ゲートへの入力として解釈されます。gh-aw での意味に合わせます。
safe-outputs
safe-outputs:
create-pull-request: {} # GitHub ネイティブの出力はあなたの gh 認証で動く
local-commit: {}
local-file-write: {}
notify: {}
run-command: {} # 常に policy 評価される
slack-post:
channel: C012345 # 必須。workflow を書く時点で固定
workspace: T012345 # 任意。複数の接続済み workspace から選ぶ
slack-reply: {} # Slack 起源の run が返信するために必須
dispatch-workflow:
allowed: ["incident-triage", "inbox-processor"] # 明示的な allowlist、必須
emit-output: {} # chaining 用の構造化 JSON。常に auto-approve
safe-output を宣言しても policy 評価は決して迂回されません。
emit-output だけは常に auto-approve です。下流の workflow に渡すだけだからです。
slack-post は mcp:loopkeep/slack_post を使えるようにします。呼び出しが受け取るのは
メッセージだけで、必須の channel が宛先を固定し、エージェントは差し替えられません。
workspace は、接続済み Slack workspace がちょうど 1 つに解決できるなら省略できます。
候補が 0 件または複数のときは、省略すると理由を示して失敗します。空または不正な channel は
宣言になりません。
slack-reply は、Slack の message・mention・reaction で始まった run にだけ
mcp:loopkeep/slack_reply を公開し、さらに slack-reply: {} の宣言を必須とします。
v0.3 より前に返信できていた既存 workflow にも、この宣言が必要です。どちらの Slack ツールにも
組み込みの notify floor があり、policy で ask-first へ引き上げられます。デスクトップと
ターミナル UI は workflow の設定から両方の宣言を編集できます。
mcp-servers
# workspace または daemon の設定で — 定義:
mcp-servers:
notion:
command: "npx -y @notionhq/notion-mcp-server"
env: { NOTION_TOKEN: "secret:notion-token" }
# workflow の中で — 名前だけで参照:
mcp-servers: [notion]
workflow はサーバを名前で参照し、接続の詳細は設定に置きます。workflow に接続情報を
インラインで書くと、漏れた認証情報として扱われます — 値はログにも警告にも出力され
ません。MCP のツール呼び出しも、ほかのアクションと同じように policy 評価を通ります
(policy の match では tool: mcp:notion/create-page)。
x-loopkeep
loopkeep 独自の設定は、1 つの x-loopkeep キーの下にネストして置きます。workflow の
root 直下のキーは gh-aw 互換でなければならず、gh-aw を超えて loopkeep が足すものはすべて
ここに入れます。
x-loopkeep:
engine: claude # engine: を loopkeep の engine id で上書き。文字列か { id, model }
concurrency:
max: 1
on_limit: queue # queue(既定)| skip | replace
budget:
tokens: 200000
time_ms: 900000
steps: 30 # ステップ N+1 で run を理由 budget で失敗させる
daily_runs: 300
daily_tokens: 500000
tags: [deploy, migration]
coalesce: 5m # この窓に入った GitHub/Slack/webhook の発火を 1 つの run にまとめる
branch_template: "{workflow_name}/{run_id}" # この workflow の run が作業する git ブランチ
execution_mode: attended # auto(既定)| headless | attended
# ターミナルが無いときの挙動を選ぶ、または起動前に確認するオブジェクト形:
# execution_mode: { mode: attended, on_no_host: ask, launch: confirm }
mux: herdr # どのマルチプレクサで開くか。下のマップ形でも書けます
# mux:
# driver: herdr
# session: work # pane を開くマルチプレクサのセッション名
# space: loopkeep # pane を置くマルチプレクサのセッション/workspace
# tab: automation # その中の tab/window
engine
loopkeep 独自の engine オーバーライドで、gh-aw が定義しない engine id を書くための欄です。
root の engine と同形 — 文字列か { id, model }。指定すると engine を完全に置き換え、
id も model もこちらから読みます。gh-aw に対応物が無いので、lk export --to-gh-aw はこれを
落とし、翻訳(yellow)項目として報告します。
concurrency
gh-aw ネイティブの concurrency: キーも上流の意味で尊重されます。
cancel-in-progress: true は replace のように、それ以外は queue のように
振る舞います。
budget
整数のみ。人を待っている時間は time_ms には数えません。
tags
policy の突き合わせ用の意味的なラベルです。自己申告なので — 硬い安全ルールは
paths/tool で突き合わせるべきです。
coalesce
期間で書く窓です(30s / 5m / 2h)。この窓に入ったこの workflow の発火は 1 つの run に
まとまり、その run はこのページ末尾のトリガー context で全件を受け取ります。既定は off で、
購読したイベントは 1 件ずつが run になります。個々のイベントに反応するのではなく、流れを
見張る workflow で on にしてください。
効くのは GitHub・Slack・webhook の発火だけです。ローカルのトリガーはこれを見ず、トリガーごとの
coalesce: latest | all を使い続けます — トリガー を参照してください。
branch_template
この workflow の run が作業する git ブランチ名を決め、config.yaml の
run.branch_template を上書きします。
{workflow_name}・{run_id}・{workspace_name} が展開され、既定は
{workflow_name}/{run_id} です。省略すると全体設定に従います。
{run_id} を省くのも正当な選択です。agent-work のような固定名にすると、この workflow は
run をまたいで 1 本のブランチを育てます。git は 1 つのブランチを同時に 1 か所でしかチェック
アウトできないため、そのブランチが使用中のときに始まった run は理由を示して失敗します。
execution_mode
run のエージェントがどこで動くかを決めます。
auto(既定)はトリガーで決めます。手動 run(あなたがそこにいる)はattended、unattended の 発火 — cron、webhook — はheadlessのままです。headlessはエージェントをサブプロセスで走らせます。監督は受信箱から行います。attendedはターミナルマルチプレクサの pane で走らせ、同じセッションを直接見て steer できます。policy のゲートはどのツールにも従来どおり掛かります。
attended には loopkeep が動かせるマルチプレクサが要ります。使えるものが無いときの挙動は
on_no_host で決めます(オブジェクト形で書きます)。
x-loopkeep:
execution_mode:
mode: attended
on_no_host: ask # ask(attended の既定)| headless
ask(attendedを宣言したときの既定)は run を黙って始めません。run を保留して受信箱に 承認を落とし、「今すぐ headless で走らせる」「ターミナルを待って attended で走らせる」の 2 つ (と通常の abort)から選ばせます。「ターミナルを待つ」は本当に待ちます — run は保留のまま残り、 マルチプレクサが起動した時点で、もう一度聞かれることなく attended で始まります。保留中の run は 並列スロットを占有しません。headlessは従来どおりです。ターミナルが無ければ run は黙って headless に落ちます。
auto は聞きません — unattended なら headless で走るのが auto の意味だからです。
ターミナルマルチプレクサのガイド を参照してください。
launch
execution_mode の配下に書きます。モードとは独立で、トリガーが来たときに run をそのまま始めるか、
先にあなたに聞くかを決めます。
x-loopkeep:
execution_mode:
mode: attended
launch: confirm # auto(既定)| confirm
confirm は run を始めずに保留し、何が走ろうとしていて何が発火させたのか(issue、メンション、
スケジュール)を示すカードを受信箱に出します。承認すれば run が始まり、中止すれば何も消費しません —
エージェントはまだ動き出していないからです。保留中の run は並列スロットを占有しません。
これは run そのものへのゲートで、使ってよい量へのゲートではないので、budget の代わりではなく
併用します。自分の手の外にあるもの — Slack のメンション、誰でも立てられる issue — で発火する
workflow で、エージェントが動く前に 1 件ずつ見たいときに使ってください。
mux
attended run をどのマルチプレクサで、どこに開くか。値をそのまま書くと driver の指定になります。
x-loopkeep:
mux: tmux # { driver: tmux } と同じ
マップ形では配置も指定できます。
x-loopkeep:
mux:
driver: tmux # herdr | tmux | zellij | cmux
session: work # pane を開くマルチプレクサのセッション名
space: loopkeep # pane を置くセッション/workspace
tab: automation # その中の tab/window
各キーは個別に、この project の .loopkeep/config.yaml、次に config.yaml、最後に組み込み既定の
loopkeep / automation へ落ちます。session に組み込み既定はありません。書かなければ、いま
使っているセッションに開きます。名指ししたセッションが動いていなければ起動します。名前付きの
セッションを持つのは herdr だけで、他のマルチプレクサはこのキーを無視します。pane 自体は run id で
名づけられます。ターミナルマルチプレクサのガイド を参照してください。
secret の参照
本文では {{ secret:<name> }}、設定値では "secret:<name>"。
Secrets を参照してください。
{{ trigger }}
トリガー context は、この run を起こしたものを表します。エージェントに届くのは本文が求めた場所だけで、 トークンを 1 つも書かなければ、何がこの run を起こしたかは渡りません。
{{ trigger }}— context 全体を JSON で{{ trigger.<path> }}— そのうちの 1 つの値を、ドット区切りで指定します ({{ trigger.event.branch }})。数字は配列の添字です({{ trigger.batch.0.title }})。 文字列はそのまま、オブジェクトと配列は JSON で入ります
context に無いパスは空文字に置き換わるので、書き間違えても波括弧は残りません(残るとエージェントが
それを指示として読んでしまいます)。エディタと lk trigger explain は保存前に警告します — 宣言した
どのトリガーでも運ばないパスと、トリガーがあるのに本文が context に一度も触れていない workflow の
2 つについてです。
トリガーを複数宣言したとき
1 つの run を起こすトリガーは 1 つだけで、context に入るのはそのトリガーの項目だけです。git と
cron の両方を宣言した workflow に {{ trigger.event.branch }} と書くのは正しい使い方です —
コミットで起きた run では値が入り、朝 3 時の run では空になります。宣言したトリガーで使うために
書くものなので、警告は出ません。
確実に書ける方法が 2 つあります。source はどのトリガーでも必ず入る唯一の項目なので、
「この run は {{ trigger.source }} で起きました」はいつでも正しく伝わります。{{ trigger }} は
どのトリガーで起きても、その発火が実際に運んできたものをそのまま渡します。一部のトリガーでしか
入らない項目は、エディタの補完がそう表示するので、空になり得る項目が書く時点で分かります。
どの発火にも source と、何が発火したかを特定できるだけの情報が載ります。ローカルのトリガーは
短く済みます(cron / file-watch / git / run-completed / manual / dispatch / resume と、
workflow 名、そのトリガーが知っていること — 変更された path、上流 run の status と output など)。
Console 経由で届いた発火は、イベントそのものを載せます。
| フィールド | 載る条件 | 値 |
|---|---|---|
source | 常に | github / slack / webhook |
event | 常に | 購読したイベント名(issues・mention など) |
occurred_at | 常に | 発生元での発生時刻 |
repo | GitHub | owner/name |
installation_id | GitHub | イベントが通った App インストール |
team_id | Slack | Slack の workspace |
hook_id | webhook | POST された hook URL がどれか |
title | イベントが持つとき | issue / pull request のタイトル |
author | 分かるとき | 書いた人 |
url | 分かるとき | issue・コメント・メッセージへの permalink |
channel | Slack | チャンネル名 |
body | 本文が送られたとき | 本文 — コメント、Slack のメッセージ、POST された payload |
下の 5 つは gateway が転送する本文から取るので、本文の無い配送では単に載りません。本文をプロンプトに 書くときは信頼できない入力として扱ってください。issue を立てられる人なら誰でもそこに文字を入れられます。
gateway が locator を渡した場合、対応する GitHub と Slack のイベントは resource として持ちます。
入るのはタイトルや本文ではなく座標で、無ければ該当するテンプレートパスは空の値に展開されます。
resource.type | ほかのフィールド |
|---|---|
github-issue | number |
github-pull-request | number |
github-comment | issue_number、comment_id |
slack-message | channel、ts、任意の thread_ts |
GitHub の座標は checkout 済みリポジトリと一緒に使い、たとえば
gh issue view {{ trigger.resource.number }} --comments で現在の会話を集めます。Slack の run では、
正確な {{ trigger.thread }} トークンを書くと、daemon がエージェントを起動する前に
conversations.replies でスレッドをローカル取得します。この遅延取得が起きるのは正確なパスだけで、
{{ trigger }} や {{ trigger.thread.author }} では取得しません。channel ID は C で始まる必要があり、
判定するのは現在の公開状態ではなく ID です。公開として作成されたチャンネルはあとで private に変えても
C ID のままで、private として作成されたチャンネルの ID と DM の ID は空文字になります。
待ち時間は 8 秒、取得は 200 メッセージを 2 ページまでです。各行は {user_id}: {text} 形式で、
user ID が無ければ bot ID、どちらも無ければ bot を使います。さらにページがある場合は末尾に
[Slack thread truncated after the first 2 pages (up to 400 messages).] を付けます。Slack へアクセス
できない場合と取得失敗でも、テンプレートの波括弧を残さず空文字になります。取得した thread の本文は
信頼できない入力として扱ってください。
まとめられた発火
coalesce を書いた workflow は、複数のイベントを 1 つの run で受けることがあります。
そのときはフィールドが 2 つ増えます。
| フィールド | 値 |
|---|---|
coalesced | この run が受け持った発火の件数 |
batch | その全件。新しいものが最後。各要素は上の表と同じ形 |
トップレベルのフィールドは最新の発火のままなので、発火元へ返信する workflow は正しいスレッドに
答えられます。窓の全体を読むには batch を見てください。1 件しか無いときはどちらのフィールドも
出ないので、載っていること自体が「この run は複数を受け持っている」という合図になります。