BacklogのWebhookをClaude Code Routines APIにつないで課題の一時捌きをしてみた

BacklogのWebhookをClaude Code Routines APIにつないで課題の一時捌きをしてみた

Backlog Webhook と Claude Code Routines の API トリガーを Lambda アダプタで連携させ、課題起票時に自動でコード調査を走らせる方法を試してみました。実装パターンと運用上の注意点をまとめます。
2026.07.13

こんにちは、せーのです。

Claude Code の Routines は cron や GitHub の PR / Release から回せるようになりましたが、Backlog で課題が追加された瞬間に Claude Code を起動したい、というニーズは公式トリガーだけでは埋まりません。2026年4月に追加された API トリガー(/fire)なら、任意の HTTP イベントから Routine を起動できます。

今日は Backlog Webhook → 薄いアダプタ(Lambda)→ Claude Code Routines /fire という構成を、2026-07-13 時点の公式情報に沿って整理し、手元で試した流れを共有します。細部は Routines ドキュメント/fire API を正として確認してください。

やりたかったこと

私は Backlog をチケット管理に使っています。バグが起票されたら、関連リポジトリを調査して原因仮説や draft PR まで持っていきたい、という場面は多いです。

これまで n8n の AI Agent ノード + MCP で似たことをやろうとすると、Plan → Execute → Verify のループやツール配線を 自前で組む 必要がありました。一方 Claude Code Routines は、Anthropic 管理クラウド上で フルエージェントセッション として動きます。todo 追跡、実行→観察→再計画、コーディング特化ツールの使い分けなど、ハーネス部分は Claude Code 側に任せられる のが利点です。

さらに課金面でも、Routines は サブスクリプション枠(おさいふ1) を消費します。n8n + API 直叩きだと従量課金になりがちなので、Max プランで日次 run cap(15回/日)の範囲に収まる運用なら、コスト面でもメリットがあります。

Slack の特定文言を拾って同じ /fire を叩く、というパターンも同じ薄いアダプタで実現できます。本稿ではエンジニア作業の見え方がわかりやすい Backlog を主軸 に書きます。

Claude Code Routines とは(おさらい)

Routine は「保存済みの Claude Code 設定」です。プロンプト、対象リポジトリ、コネクタをパッケージ化し、Anthropic 管理クラウドで自動実行します。Research Preview なので仕様変更の可能性はあります。

トリガー地図

トリガー 内容 備考
Scheduled hourly / daily / weekdays / weekly 等 最小間隔 1 時間
API POST .../routines/{id}/fire + per-routine bearer CLI ではトークン発行不可(Web UI のみ)
GitHub event Pull request / Release Issue / push / discussion は公式表になし

GitHub Issue、Slack、Backlog の ネイティブトリガーは現状ありません。Anthropic 公式ブログでも「Webhook ソースの拡張を予定」と書かれており、現時点では API トリガーで代替 するのが実務的です。

運用上の特性(押さえておきたい点)

  • 個人アカウントに帰属し、実行は個人の daily allowance を消費
  • permission picker なし。コネクタ書き込みも承認なしで走る
  • プロンプトは 自己完結 であること。成功条件を明示する
  • 日次 run cap: Pro 5 / Max 15 / Team・Enterprise 25(Extra usage 有効時は超過をメーター課金)

なぜ「薄いアダプタ」が必要か

Backlog Webhook は指定 URL へ JSON を HTTP POST するだけ です。カスタム Authorization ヘッダや anthropic-beta ヘッダは 設定できません

04_backlog_lambda_claude_instructions.png

したがって Backlog → Anthropic /fire を直接結線することは不可能 です。

ハーネスとコストの比較

n8n AI ノード + MCP Routine /fire
実行単位 ノード単位の LLM 呼び出し フル Claude Code セッション
ツール ワークフローで配線した MCP セッション内で自律選択・再計画
レスポンス 数秒〜十数秒(同期向き) /fire は即レス、実作業は非同期
コスト API 従量 サブスク枠 + 日次 run cap

違いを整理すると、n8n は「手順固定のルーティング・分類・通知」Routine は「探索的なコード調査・draft PR」、というイメージです。定型処理は n8n、重いコード作業だけ Routine、というハイブリッドも実務的です。

やってみた

前提

  • Claude Max プラン(Claude Code on the web 有効)
  • 対象 GitHub リポジトリ: 個人用 Obsidian Vault(Claude Code 接続済み)
  • AWS アカウントで Lambda + Function URL を作成可能
  • Backlog プロジェクト管理者権限

Step 1: Routine を作る(API トリガー)

  1. claude.ai/code/routinesNew routine
  2. Name: Backlog issue triage
  3. 対象リポジトリを選択
  4. Environment: Default(Trusted network)
  5. Connectors: 不要なものは外す

プロンプトはバグ調査用に設定しました。API /fire 向けには、後述の 入力(API トリガー) 段落を足す必要がありました(Step 4 で後から判明)。

あなたは Backlog から起票されたバグ調査エージェントです。

## 入力(API トリガー / Backlog Webhook 経由)
この Routine は API `/fire` で起動します。セッション冒頭に届く
「Backlog課題イベントを受信しました。」で始まるテキストブロックに
課題キー・件名・本文が含まれます。そこから抜き出して調査してください。

## 手順
1. text から課題キー・件名・再現手順・エラーメッセージを抜き出す
2. このリポジトリを調査し、関連しそうなファイル・直近コミットを特定する
3. 原因仮説を最大3つに絞り、それぞれ根拠となるファイルパスと行の根拠を示す
4. 修正が小さく安全なら `claude/` 接頭辞のブランチに最小修正を実装し、テストまたは静的確認を行う
5. 修正できない/危険な場合はコード変更せず、調査レポートだけ残す

## 成功条件
- 調査結果がセッションに残っている
- コードを変えた場合は変更差分がレビュー可能な状態
- 推測と事実を分けて書く
- Backlog や外部サービスへの破壊的書き込みはしない

## やってはいけないこと
- 本番設定の変更
- 無関係なリファクタ
- 依存関係のメジャーアップデート

Select a triggerAPI を追加。Save 後に Edit で URL と token が表示される場合があります。URL は .../routines/trig_.../fire まで 末尾までフルコピー してください。

curl でスモークテスト:

export ROUTINE_FIRE_URL='https://api.anthropic.com/v1/claude_code/routines/trig_01XXXX/fire'
export ROUTINE_TOKEN='sk-ant-oat01-XXXX'

curl -sS -X POST "$ROUTINE_FIRE_URL" \
  -H "Authorization: Bearer $ROUTINE_TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: experimental-cc-routine-2026-04-01" \
  -H "Content-Type: application/json" \
  -d '{"text": "スモークテスト: READMEを1行要約して終了してください。"}'

結果: 最初 /fire なしの URL で 404 page not found/fire 付きに修正後、セッションが起動し README 要約まで完了。

README.mdの1行要約:
せーのの個人用Obsidian Vault(業務メモ・技術ドキュメント・ブログネタ管理)のセットアップ手順、
ディレクトリ構成、主要スキル、セキュリティ方針をまとめたガイド。

観察メモ: /fire はセッション作成時点で 200 を返す非同期 API です。完了待ちはありません。

Step 2: 薄いアダプタ(Lambda Function URL)

アダプタは LLM を呼びません。Backlog JSON を受信し、text を散文に整形して /fire に POST するだけです。

環境変数:

名前 内容
ROUTINE_FIRE_URL UI でコピーした /fire URL
ROUTINE_TOKEN sk-ant-oat01-...
ALLOWED_TYPES 既定 1(課題追加)。コメントも見るなら 1,3
WEBHOOK_SHARED_SECRET (推奨)クエリパラメータ検証用

Lambda ハンドラ(Python 3.12):

import json
import os
import urllib.error
import urllib.request

FIRE_URL = os.environ["ROUTINE_FIRE_URL"]
TOKEN = os.environ["ROUTINE_TOKEN"]
ALLOWED_TYPES = {
    int(x.strip())
    for x in os.environ.get("ALLOWED_TYPES", "1").split(",")
    if x.strip()
}

def build_text(payload: dict) -> str:
    project = payload.get("project") or {}
    content = payload.get("content") or {}
    user = payload.get("createdUser") or {}
    project_key = project.get("projectKey") or "UNKNOWN"
    key_id = content.get("key_id")
    issue_key = f"{project_key}-{key_id}" if key_id is not None else "(unknown)"
    issue_type = (content.get("issueType") or {}).get("name") or "(unknown)"
    summary = content.get("summary") or "(no summary)"
    description = content.get("description") or "(no description)"
    created_by = user.get("name") or "(unknown)"

    return (
        f"Backlog課題イベントを受信しました。\n"
        f"activity_id: {payload.get('id')}\n"
        f"type: {payload.get('type')}\n"
        f"プロジェクト: {project_key} ({project.get('name')})\n"
        f"課題キー: {issue_key}\n"
        f"件名: {summary}\n"
        f"種別: {issue_type}\n"
        f"起票者: {created_by}\n"
        f"本文:\n{description}\n"
    )

def fire_routine(text: str) -> dict:
    body = json.dumps({"text": text[:65000]}).encode("utf-8")
    req = urllib.request.Request(
        FIRE_URL,
        data=body,
        method="POST",
        headers={
            "Authorization": f"Bearer {TOKEN}",
            "anthropic-version": "2023-06-01",
            "anthropic-beta": "experimental-cc-routine-2026-04-01",
            "Content-Type": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=20) as resp:
        return json.loads(resp.read().decode("utf-8"))

def lambda_handler(event, context):
    qs = event.get("queryStringParameters") or {}
    if qs.get("token") != os.environ.get("WEBHOOK_SHARED_SECRET"):
        return {"statusCode": 401, "body": "unauthorized"}

    try:
        raw = event.get("body") or "{}"
        if event.get("isBase64Encoded"):
            import base64
            raw = base64.b64decode(raw).decode("utf-8")
        payload = json.loads(raw)
    except (TypeError, ValueError, json.JSONDecodeError):
        return {"statusCode": 400, "body": "invalid json"}

    activity_type = payload.get("type")
    if activity_type not in ALLOWED_TYPES:
        return {
            "statusCode": 200,
            "body": json.dumps({"skipped": True, "type": activity_type}),
        }

    text = build_text(payload)
    try:
        result = fire_routine(text)
    except urllib.error.HTTPError as e:
        err_body = e.read().decode("utf-8", errors="replace")
        return {
            "statusCode": 502,
            "body": json.dumps({"error": err_body, "status": e.code}),
        }

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(
            {
                "ok": True,
                "session_id": result.get("claude_code_session_id"),
                "session_url": result.get("claude_code_session_url"),
            }
        ),
    }

Function URL の設定:

  1. Lambda 作成(Runtime Python 3.12)
  2. Configuration → Function URL → Create(Auth: NONE
  3. タイムアウト 30 秒程度
  4. Webhook URL 例(?token= に共有シークレットを付与):
https://xxxxxx.lambda-url.ap-northeast-1.on.aws/?token=YOUR_SHARED_SECRET

Lambda 直 POST の結果:

{
  "ok": true,
  "session_id": "session_01YFERE4c3rFjjei9uhCCBiJ",
  "session_url": "https://claude.ai/code/session_01..."
}

観察メモ:

  • type=2(課題更新)は ALLOWED_TYPES=1 により skipped: true で正しくフィルタされた
  • Function URL は lambda:InvokeFunctionUrl に加え lambda:InvokeFunction の resource policy が必要な場合がある(403 Forbidden 対策)

Step 3: Backlog Webhook 設定

CM_BACKLOGTEST_MARKDOWN(Backlogテスト(Markdown))プロジェクトで設定しました。

  1. プロジェクト設定 → インテグレーション → Webhook → 追加
  2. Webhook 名例: routines api test
  3. Webhook URL: Lambda Function URL + ?token=...
  4. 通知イベント: 課題の追加 のみ
  5. 保存 → 実行テスト(イベント: 課題の追加)

01_backlog-webhook-settings-cm-backlogtest-markdown.png

Step 4: E2E 確認

Backlog 実行テストの payload(test issue / test description)で Routine が起動しました。

02_claude-routine-execution-history-api-trigger.png

Routine 詳細ページの 実行 セクションに、13:48・13:50 など API バッジ付きの Run が並びます(UI 上は「Runs タブ」ではなく詳細ページ下部の実行一覧)。

プロンプトと /fire text のズレ(1回目):

初回は「実行時コンテキスト(text)に課題が渡される」前提のプロンプトのため、「課題内容が渡されていない」と判断されました。API トリガー向けの入力説明を足したところ、2回目以降は text ブロックから課題を読み取れました。

2回目以降の調査結果:

03_claude-code-session-backlog-triage-result.png

  • 課題 CM_BACKLOGTEST_MARKDOWN-100「test issue」を確認
  • 件名・本文がプレースホルダー的なテスト起票と判断(Webhook 疎通確認目的)
  • scripts/routines/backlog_fetch.sh 等を確認したが、この課題に対応する障害の手がかりなし
  • 再現手順・エラー情報がなく コード変更は不要 と結論。調査記録のみで完了

結論: Backlog Webhook → Lambda → /fire → Claude Code セッションまで E2E 成功。Run の緑/Backlog 200 はインフラ成功であり、今回のテスト課題では「調査記録のみで完了」が正しい挙動でした。

よくある失敗

症状 原因候補
curl で 404 page not found /fire なしの URL
invalid routine ID trig_... ID の末尾欠落(UI からフルコピー)
Backlog 送信履歴 502 Lambda 内 ROUTINE_TOKEN 無効。再発行後に環境変数未更新
Backlog 送信履歴 401 Function URL の ?token= 不一致
400 invalid_request anthropic-beta 欠落、Routine paused、text 過長
401(Anthropic 直叩き) トークン不一致・再発行後の旧トークン
429 日次 run cap または usage 上限
Backlog 200 だが Routine が「課題未受信」 プロンプトが API /firetext の見え方とズレている
Backlog 200 だが動かない type フィルタで skip、または URL 誤り
二重実行 Webhook リトライ + idempotency なし

運用のガードレール

  1. 発火条件を絞る — 全課題ではなく Bug 種別 / 特定カテゴリ / 件名プレフィックス
  2. 冪等payload["id"] を DynamoDB に Put(条件付き)してから /fire
  3. 日次 budget — Max 15 回を前提に、朝会トリアージ級の頻度に抑える
  4. プロンプトの成功条件と禁止事項を必ず書く。API トリガー時は text ブロックの読み方も明示する
  5. コネクタ最小権限 — 書き込み不要なら外す
  6. token 再発行時は Lambda 環境変数も更新 — 更新漏れで Backlog 502 になる
  7. 結果の人間レビュー — run の緑ステータスはインフラ成功であり、タスク成功ではない

発展パターン

Slack 起点

同じ Lambda(または別 Function)を Slack Workflow の HTTP ステップから呼びます。メッセージ本文を text に載せれば、Backlog と同型です。

AI なし n8n 最小構成

既存 n8n がある場合:

Backlog Trigger / Webhook
  → IF(type=1)
  → HTTP Request(/fire + 必須ヘッダ)

AI Agent ノードは使いません。ワークフロー JSON はリポジトリ内 backlog-webhook-claude-routines/n8n-backlog-routines-fire.json に同梱しています(n8n の Import from File で取り込み)。インポート後、以下を差し替えてください。

  1. Fire Claude Code Routine ノードの URL → Routine の /fire URL
  2. httpHeaderAuth Credential を新規作成(Header: Authorization、Value: Bearer sk-ant-oat01-...
  3. Backlog Webhook ノードの Production URL を Backlog に登録

ハイブリッド

Backlog → n8n(分類・優先度・通知)
         └─ 優先度 High かつ Bug のときだけ /fire

精度・コスト・レイテンシのバランスが最も実務的です。

まとめ

  • Backlog Webhook はカスタムヘッダを付けられないため、薄いアダプタ(Lambda 等)が必要
  • アダプタは LLM を呼ばず、Backlog JSON を散文 text に整形して /fire に POST するだけ
  • n8n バイパス = AI Agent / MCP ハーネスを捨て、Claude Code 側の agentic ループに処理本体を寄せる
  • 課金は サブスク枠(おさいふ1) + 日次 run cap。テスト用プロジェクトで絞って検証するのが安全
  • /fire の URL は .../fire まで、Routine ID は UI から フルコピー
  • token 再発行後は Lambda 環境変数も更新(Backlog 502 の典型原因)
  • API トリガー向けプロンプトで text の読み方を明示 しないと「課題未受信」と誤判断される
  • Backlog 200 / Run 緑 = インフラ成功。タスク成功はセッション内容で人間確認
  • /fire に idempotency はない。Webhook リトライ対策はアダプタ側で自前管理

参考資料


Claudeならクラスメソッドにお任せください

クラスメソッドは、Anthropic社とリセラー契約を締結しています。各種製品ガイドから、業種別の活用法、フェーズごとのお悩み解決などサービス支援ページにまとめております。まずはご覧いただき、お気軽にご相談ください。

サービス詳細を見る

この記事をシェアする

AI白書

関連記事