「Backlogのタスク、気づいたら期限が過ぎて放置されていた…」
「チームごとに通知先を分けたいけれど、設定が難しそう…」

そんな悩みを解決するために、Google Apps Script(GAS)を使って、期限切れ課題をSlackへ自動通知し、結果をスプレッドシートに記録する「自動パトロールシステム」を作ってみました!
プログラミング未経験の方でも、この記事の通りに作業すれば30分で導入可能です!

事前準備

プログラムがSlackにメッセージを届けるためには、Incoming Webhookという連携用のURLを発行する必要があります。

  1. ブラウザで Slack App ディレクトリ(Incoming Webhooks) にアクセス
  2. 「Slackに追加」をクリック
  3. 「チャンネルへの投稿」という項目があるので、通知を飛ばしたいSlackのチャンネルを選択
  4. [Incoming Webhook インテグレーションの追加] という緑色のボタンをクリック
  5. Webhook URLをコピーする

※このURLは「STEP 3」で使うので、メモ帳などに貼り付けておいてください。
※通知先を分けたい場合は、この手順を再度実行する

導入手順

以下の4ステップで進めます。

  1. スプレッドシートの準備:パトロール結果を記録する台帳(ログシート)を作成
  2. スクリプトのセットアップ:ツールの頭脳となるプログラムを貼り付け
  3. スクリプトプロパティの設定:Webhook URLやAPIキーを安全な場所に保管
  4. テスト実行と承認:手動でプログラムを動かし、Googleの承認を済ませる
  5. 自動化(トリガー)の設定:決まった時間に勝手に動くように設定

STEP 1:スプレッドシートの準備

まずは、パトロールの結果を記録するためのシートを用意します。

  1. 新しいGoogleスプレッドシートを作成する
  2. 画面下のシート名(タブ名)をダブルクリックして「送信ログ」に変更

注意: 漢字やスペースが違うと正しく記録されないので正確に入力してください!

STEP 2:スクリプトの貼り付け

次に、プログラム(コード)をコピー&ペーストします。

  1. スプレッドシートのメニューから [拡張機能] > [Apps Script] を開く
  2. 最初から書いてある function myFunction() {...} をすべて消して、以下の完成版コードをそのまま貼り付ける

【コピペ用】完成版コード

/**
 * Backlog 期限超過課題パトロール通知スクリプト
 */
function notifyTeamActiveTasks() {
  const now = new Date();
  const dayOfWeek = now.getDay();

  // --- 【1. 曜日リミッター】月(1)と木(4)のみ実行 ---
  if (dayOfWeek !== 1 && dayOfWeek !== 4) {
    console.log("本日は実行対象日ではありません。");
    return;
  }

  // --- 【2. 設定情報の読み込み】 ---
  const props = PropertiesService.getScriptProperties();
  const BACKLOG_API_KEY = props.getProperty('BACKLOG_API_KEY');
  const DEFAULT_WEBHOOK = props.getProperty('DEFAULT_WEBHOOK');
  const BACKLOG_SPACE_ID = 'あなたのスペースID'; // 例: example-space
  const BACKLOG_DOMAIN = 'backlog.com';
  const TARGET_PROJECT_KEYS = ['PROJ_KEY']; // 例: PRJ_ABC

  // --- 【3. メンバーリスト】 ---
  const USER_LIST = {
    // 別のチャンネルに飛ばしたい人だけ propKey を書く
    'チームA担当者': { id: 'U11111111', propKey: 'WEBHOOK_TEAM_A' },
    
    // デフォルト(共通)のチャンネルで良い人は名前とIDだけでOK!
    '一般担当者1': { id: 'U22222222' }, 
    '一般担当者2': { id: 'U33333333' }
  };

  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const todayStr = Utilities.formatDate(today, "JST", "yyyy-MM-dd");
  const oneWeekAgo = new Date(today.getTime());
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);

  let finalExportData = [];
  let channelBuckets = {};

  // --- 【4. 課題データの取得と判定】 ---
  TARGET_PROJECT_KEYS.forEach(projectKey => {
    try {
      const baseUrl = `https://${BACKLOG_SPACE_ID}.${BACKLOG_DOMAIN}/api/v2`;
      const projectRes = UrlFetchApp.fetch(`${baseUrl}/projects/${projectKey.trim()}?apiKey=${BACKLOG_API_KEY}`, { "muteHttpExceptions": true });
      if (projectRes.getResponseCode() !== 200) return;
      const projectId = JSON.parse(projectRes.getContentText()).id;

      let allIssues = [];
      for (let offset = 0; offset < 400; offset += 100) {
        const issuesUrl = `${baseUrl}/issues?apiKey=${BACKLOG_API_KEY}&projectId[]=${projectId}&parentChild=0&count=100&offset=${offset}`;
        const fetched = JSON.parse(UrlFetchApp.fetch(issuesUrl).getContentText());
        if (!fetched || fetched.length === 0) break;
        allIssues = allIssues.concat(fetched);
      }

      for (const [backlogName, info] of Object.entries(USER_LIST)) {
        const targetIssues = allIssues.filter(issue => {
          if (!issue.dueDate || issue.status.name.includes("完了")) return false;
          const d = new Date(issue.dueDate); d.setHours(0,0,0,0);
          if (d > today) return false;
          const isDirect = issue.assignee && issue.assignee.name === backlogName;
          let isChild = false;
          if (issue.parentIssueId) {
            const p = allIssues.find(i => i.id === issue.parentIssueId);
            if (p && p.assignee && p.assignee.name === backlogName) isChild = true;
          }
          return isDirect || isChild;
        });

        if (targetIssues.length > 0) {
          finalExportData.push([now, backlogName, projectKey, targetIssues.length]);
          
          // ★修正ポイント:propKeyがなければDEFAULT_WEBHOOKを使用
          const targetWebhook = (info.propKey ? props.getProperty(info.propKey) : null) || DEFAULT_WEBHOOK;
          if (!channelBuckets[targetWebhook]) channelBuckets[targetWebhook] = "";

          let memberMsg = `\n\n🔔 <@${info.id}> さん 【${projectKey}】\n`;
          targetIssues.forEach(issue => {
            const dateStr = issue.dueDate.split('T')[0];
            const d = new Date(issue.dueDate); d.setHours(0,0,0,0);
            let statusMark = (dateStr === todayStr) ? "🚨 " : (d >= oneWeekAgo) ? "⚠️ " : "🔥 ";
            memberMsg += `${issue.parentIssueId ? " ┗ " : "・"}${statusMark} (${issue.status.name} / 期限: ${dateStr})\n`;
          });
          channelBuckets[targetWebhook] += memberMsg;
        }
      }
    } catch (e) { console.error("Error: " + e.message); }
  });

  // --- 【5. Slack送信とログ記録】 ---
  for (const [url, msg] of Object.entries(channelBuckets)) {
    UrlFetchApp.fetch(url, { "method": "post", "contentType": "application/json", "payload": JSON.stringify({ "text": "📢 *Backlog期限パトロールレポート*\n" + msg }) });
  }
  writeToSheetNow(finalExportData);
}

function writeToSheetNow(dataRows) {
  if (!dataRows || dataRows.length === 0) return;
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName('送信ログ') || ss.insertSheet('送信ログ');
  if (sheet.getLastRow() === 0) sheet.appendRow(['送信日時', '担当者名', 'プロジェクト', '課題件数']);
  sheet.getRange(sheet.getLastRow() + 1, 1, dataRows.length, dataRows[0].length).setValues(dataRows);
}

コードの解説:何をしているの?

【1. 曜日リミッター】
プログラムが起動した直後に今日は何曜日か?を確認し、月曜(1)と木曜(4)以外であれば、そこで処理を中断します。

設定:
dayOfWeek !== 1 && dayOfWeek !== 4 の数字を書き換えます(1=月、4=木)。テストで今すぐ動かしたい場合は、この if 文を一時的に // でコメントアウトしてください。毎日動かしたい場合は、このブロックごと削除またはコメントアウトします。

【2. 設定情報の読み込み】
スクリプトプロパティに保存したAPIキーや、BacklogのスペースURLなどを読み込みます。機密情報をコードに直接書かないことで、セキュリティを担保しています。

<コード内には記載せず、スクリプトプロパティから読み込んでいるもの>※STEP3で設定方法を説明しています

  • BACKLOG_API_KEY:Backlogのサーバーが誰がアクセスしているかを識別し、データの取得を許可するための認証情報です。
  • DEFAULT_WEBHOOK:Slackの特定のチャンネルにメッセージを届けるための送信先URLです。
    ★APIキーやURLは、銀行の暗証番号のようなものです!コード内に直接書くと、誰かにコードを見られた際に悪用される恐れがあるため、GASのスクリプトプロパティという安全な場所に保管して、プログラムから呼び出す形にしています。

<コード内に直接指定するもの>※ここは、それぞれの環境に合わせて書き換える必要がある場所です。

  • BACKLOG_SPACE_ID:自分のBacklog URLのhttps:// の直後から .backlog.com の前までにある文字列(例:https://example-space.backlog.com/dashboardであれば、example-space がID)
  • TARGET_PROJECT_KEYS:監視したいプロジェクトのキーを登録

【3. メンバーリスト】
Backlogの担当者名と、Slackの通知先(メンバーID)を紐付けます。

  • 名前:Backlogの表示名と1文字も違わないように入力してください。
  • id:Slackプロフィールから「メンバーIDをコピー」で取得した U から始まる英数字、これがないとメンションが飛びません。(Slackのプロフィールを表示 > [その他(…)] > [メンバーIDをコピー])
  • propKey:送信先チャンネルを分けたい人だけ、スクリプトプロパティに登録する名前(例: WEBHOOK_TEAM_A)を書きます。共通のチャンネルで良い人は、この項目を丸ごと削除してOKです!

【4. 課題データの取得と判定】
Backlogから最大400件の課題を取得し、期限日をもとにアイコンを割り当てます。

  • 🚨:本日が期限(当日の対応が必要)
  • ⚠️:超過1週間以内(早急な対応を推奨)
  • 🔥:超過1週間以上(最優先の確認が必要)

【5. Slack送信とログ記録】
まとめたメッセージをSlackへ送信し、スプレッドシートの「送信ログ」シートに、いつ、誰に、何件送ったかを追記します。

STEP 3:スクリプトプロパティの設定

  1. エディタ左側の [歯車アイコン] > [スクリプトプロパティを編集] を開く
  2. 名前と値をセットで登録する

設定項目一覧

項目名 定義・役割
BACKLOG_API_KEY Backlog APIにアクセスするための個人用認証キー
DEFAULT_WEBHOOK Slack通知を実行するためのWebhook URL(エンドポイント)
WEBHOOK_TEAM_A 必要に応じて別SlackチャンネルのURLを追加

STEP4:テスト実行と承認

トリガーを設定する前に、一度だけ手動で動かしてGoogleの承認を済ませる必要があります。これを忘れると、自動実行がエラーで止まってしまいます。

  1. エディタ上部の [実行] ボタンをクリック
  2. 「承認が必要です」というポップアップが出るので、 [権限を確認] をクリック
  3. 自分のGoogleアカウントを選択し、警告画面が出たら [詳細] > [(プロジェクト名)に移動(安全ではない)] をクリック
  4. 内容を確認し、最後に [許可] をクリック
  5. 下のログに「実行完了」と出れば、Googleからの許可が完了しています!

STEP 5:自動化(トリガー)の設定

  1. エディタ左側の [時計アイコン(トリガー)] をクリック
  2. [+ トリガーを追加] を押し、以下のように設定
  • 関数:notifyTeamActiveTasks
  • ソース:時間主導型
  • タイプ:日付ベースのタイマー
  • 時刻:午前9時〜10時など(通知したい時間)
    • ※Google Apps Script(GAS)の仕様上、トリガーで設定した時刻に分単位でぴったり通知が届くわけではありません。例えば「午前9時〜10時」と設定した場合、その1時間の枠内のどこかで実行されます。

※初回実行時の承認手順

  1. 「承認が必要です」というポップアップが出るので、[権限を確認] をクリック
  2. 自分のGoogleアカウントが表示されたらクリック
  3. 画面左下にある [詳細] という小さな文字をクリック
  4. 下の方に表示される [(プロジェクト名)に移動(安全ではない)] をクリック※Googleから警告画面が出ますが、自分で作成したスクリプトなので安全です。
  5. 内容を確認し、一番下の [許可] をクリック

通知の完成イメージ

実行されると、担当者ごとに整理されたレポートがSlackに届きます。期限の超過具合に合わせてアイコンが変化するため、優先すべき課題が一目で判断できます。

プロジェクトで活かす際の注意、アイデア

運用上の注意点

  • APIキーの権限と有効期限BACKLOG_API_KEY は、発行したユーザーの権限に依存するため、そのユーザーがプロジェクトから脱退したり、アカウントが無効化されたりすると、スクリプトも停止してしまいます。
  • APIの実行制限:Backlog APIには一定時間内のリクエスト回数制限があります。このスクリプトでは最大400件の課題を取得するように設計されていますが、極端に巨大なプロジェクトや、数十個のプロジェクトを一度にスキャンする場合は、エラーが発生する可能性があります。その際は実行時間をずらすなどの工夫が必要です。
  • 情報の同期(表示名の不一致):Backlogの表示名は、名字と名前の間のスペースの有無まで含めて判定しています。通知が届かない場合は、まずBacklog上の名前とコード内の記述が完全に一致しているかを確認してください。

さらに業務を効率化するアイデア

  • 週次レポートとしての活用:スプレッドシートの送信ログに蓄積されたデータをグラフ化すれば、どの時期にタスクが滞留しやすいか、誰に負荷が集中しているかを可視化でき、チームの生産性向上のための分析資料として活用できます。
  • 監視対象(Backlogプロジェクト)を増やすTARGET_PROJECT_KEYS の配列にプロジェクトキーを追加するだけで、1度の実行で複数のプロジェクトを横断的にスキャンできます。

まとめ

今回は、Google Apps Scriptを活用してBacklogの期限超過課題を自動通知するシステムの構築方法を解説しました。
本ツールの導入メリット

  • タスク放置の抑止:3段階の緊急度アイコンにより、優先すべき課題が一目で判別可能になります。
  • 管理コストの削減:手動のリマインド作業が自動化され、担当者は別のタスクに時間を割けるようになります。
  • 確実なエビデンス保持:スプレッドシートへのログ記録により、パトロールの実施状況を後から客観的に振り返ることができます。

最後に
このパトロール通知が始まったからといって、チームの全タスクが綺麗に片付くわけではありません。(通知をスルーする人がいたり、Backlogの更新を忘れる人がいたりと…..)
大切なのは、このシステムを一度作って終わりにせず、運用しながら微調整を繰り返すことです。例えば、通知の最後に自分らしい励ましのコメントを一行足すだけでも、チームの受け取り方は変わります。もし、こんな機能を追加してもっと自分をラクにしたい!というアイデアが浮かんだら、迷わずGeminiに相談してみましょう。自分の意図を汲み取ったコードの提案や、運用のコツをいつでもアドバイスしてくれます。

AIを壁打ち相手にしながら、チームにとって最高の仕組みを作っていきましょう!