ワークフローの 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 / fromfrom_notfrom_bot / reviewer / keywords / emojiworkflow_runworkflowsbranchesconclusions)です。各イベントが受け付けるのは、その payload で照合できる フィルタだけです —— labels は issue・pull request・discussion(とそのコメント)、branchespush / create / delete / workflow_run と pull request の取り込み先(base)ブランチ、reviewer は pull request の review_requestedemoji は Slack の reactionkeywordsmentions は Slack の message / mention / dm。照合できないフィルタは、登録して黙って当たらないままにせず、 警告を出して受け付けません。on.slack.messagekeywords / 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 つ —— actionscontent_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-postmcp: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: truereplace のように、それ以外は 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.yamlrun.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 に入るのはそのトリガーの項目だけです。gitcron の両方を宣言した 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 の statusoutput など)。 Console 経由で届いた発火は、イベントそのものを載せます。

フィールド載る条件
source常にgithub / slack / webhook
event常に購読したイベント名(issuesmention など)
occurred_at常に発生元での発生時刻
repoGitHubowner/name
installation_idGitHubイベントが通った App インストール
team_idSlackSlack の workspace
hook_idwebhookPOST された hook URL がどれか
titleイベントが持つときissue / pull request のタイトル
author分かるとき書いた人
url分かるときissue・コメント・メッセージへの permalink
channelSlackチャンネル名
body本文が送られたとき本文 — コメント、Slack のメッセージ、POST された payload

下の 5 つは gateway が転送する本文から取るので、本文の無い配送では単に載りません。本文をプロンプトに 書くときは信頼できない入力として扱ってください。issue を立てられる人なら誰でもそこに文字を入れられます。

gateway が locator を渡した場合、対応する GitHub と Slack のイベントは resource として持ちます。 入るのはタイトルや本文ではなく座標で、無ければ該当するテンプレートパスは空の値に展開されます。

resource.typeほかのフィールド
github-issuenumber
github-pull-requestnumber
github-commentissue_numbercomment_id
slack-messagechannelts、任意の 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 は複数を受け持っている」という合図になります。