はじめに
プロジェクトの予算管理や予実管理において、日々の「工数入力(稼働管理)」は会社運営に関わる非常に重要な業務です。
私の所属する組織では、正確なコスト管理を行うために、Slack Appを用いた工数管理を行なっており、コマンドで入力する方式を採用しています。
このルール自体は正しいコスト算出のために重要なものですが、
「カレンダーを確認」→「コマンドを確認して入力」 という作業をより効率化できないかと考え、Google Apps Script (GAS) を活用したBotを作成しました。
今回は、セキュリティポリシーにより Incoming Webhooks が使えない環境 でも実装できる、「Slackのメールインテグレーション機能」を使った連携方法を紹介します。
完成イメージ
毎月の最終営業日の朝(8:00〜9:00)、Slackに以下のような通知が届きます。
生成されたコマンドを、社内の工数管理Botに貼り付けるだけで、ルールに則った正確な入力が完了します。
件名: 工数入力用コマンド (12月分・最終営業日通知)
本文:add -u 2025/12/01 2025/12/31 [メイン案件コード] 100 rm 2025/12/05 add 2025/12/05 [メイン案件コード] 80 add 2025/12/05 [社内業務コード] 20 ...
仕組み
入力ミスを防ぎ、工数管理を効率化するために以下のフローを構築しました。
- トリガー実行: 毎日朝(8時〜9時)にGASが起動。
- 営業日判定: 「今日がその月の最終営業日(平日)」かどうかを判定。
- データ取得: Googleカレンダーから今月の予定を取得し、工数振替の対象となる特定のイベントを検索。
-
コマンド生成:
- 月初〜月末までをメイン案件でベース入力。
- 対象日のみ削除(
rm)し、ルール通り(80:20など)に按分した正しい数値を再登録(add)するコマンドを生成。
- Slack送信: Webhookの代わりに、Slackチャンネル固有のメールアドレス宛にメールを送信して通知。
手順1:Slackのメールアドレスを取得
Webhookが使えない場合でも、Slackの標準機能である「メールインテグレーション」を使えば外部からセキュアに通知が可能です。
- 通知したいSlackチャンネルを開く。
- チャンネル詳細 > [インテグレーション] > [このチャンネルにメールを送信する] を選択。
- 発行されたメールアドレス(例:
abcxyz@slack.com)を控える。
手順2:GASの実装
- Googleドライブから「新規」→「その他」→「Google Apps Script」を作成し、以下のコードを貼り付けます。
※設定エリアの部分はご自身の環境に合わせて書き換えてください。
ソースコード
// メインの実行関数(トリガーにはこれを設定します)
function notifyMonthlyAttendance() {
// 1. 今日が「今月の最終営業日」かどうかチェック
if (!isLastBusinessDay()) {
console.log("今日は最終営業日ではないため、処理をスキップします。");
return;
}
// 2. 最終営業日なら、コマンドを生成して送る
sendAttendanceCommandsToSlack();
}
// 実際にメールを送る処理
function sendAttendanceCommandsToSlack() {
// --- 設定エリア (書き換えてください) ---
const SLACK_CHANNEL_EMAIL = 'ここにSlackで取得したメールアドレス';
const CALENDAR_ID = 'ここに自分のメールアドレス';
const MAIN_PROJECT_CODE = 'Main_Project_Code'; // メイン案件のコード
const SUB_PROJECT_CODE = 'Internal_Work_Code'; // 按分用の社内業務コード
const TARGET_KEYWORD = '定期集会'; // 工数振替の対象となるキーワード
// ------------------------------------
const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
const now = new Date();
const startDate = new Date(now.getFullYear(), now.getMonth(), 1);
const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const formatDate = (date) => Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy/MM/dd');
let commandList = [];
// 1. 月初のまとめ入力(全期間をメイン案件100%で登録)
const startStr = formatDate(startDate);
const endStr = formatDate(endDate);
commandList.push(`add -u ${startStr} ${endStr} ${MAIN_PROJECT_CODE} 100`);
commandList.push(``);
// 2. 特定キーワードの日を上書き(正確な工数按分)
const events = calendar.getEvents(startDate, endDate);
let processedDates = [];
events.forEach(function(event) {
if (event.getTitle().includes(TARGET_KEYWORD)) {
const dateStr = formatDate(event.getStartTime());
// 重複除外処理
if (processedDates.indexOf(dateStr) === -1) {
processedDates.push(dateStr);
// 一度消して、正確な割合で入れ直す
commandList.push(`rm ${dateStr}`);
commandList.push(`add ${dateStr} ${MAIN_PROJECT_CODE} 80`); // メイン案件
commandList.push(`add ${dateStr} ${SUB_PROJECT_CODE} 20`); // 社内業務等
commandList.push(``);
}
}
});
// 3. Slack(メール)へ送信
const monthNum = startDate.getMonth() + 1;
const subject = `工数入力用コマンド (${monthNum}月分・最終営業日通知)`;
const body = `お疲れ様です。本日は${monthNum}月の最終営業日です。\n工数管理用のコマンドを生成しました。\n内容を確認の上、入力をお願いします。\n\n` +
"```\n" + commandList.join('\n') + "\n```";
// GmailAppを使うことで、送信履歴を残しつつ確実に届ける
GmailApp.sendEmail(SLACK_CHANNEL_EMAIL, subject, body);
console.log(`Slackへ送信しました: ${monthNum}月分`);
}
// --- 以下、日付判定ロジック ---
// 今日が「今月の最終営業日」か判定する関数
function isLastBusinessDay() {
const today = new Date();
// 今日が休みならNG
if (isHolidayOrWeekend(today)) return false;
// 明日から月末までの間に、平日が1日でも残っているかチェック
const nextDay = new Date(today);
nextDay.setDate(today.getDate() + 1);
while (nextDay.getMonth() === today.getMonth()) {
if (!isHolidayOrWeekend(nextDay)) {
return false; // まだ平日が残っている
}
nextDay.setDate(nextDay.getDate() + 1);
}
return true; // 明日以降は全て休み=今日が最終日
}
// 指定日が「土日」または「祝日」か判定する関数
function isHolidayOrWeekend(date) {
const dayOfWeek = date.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) return true; // 土日
// Googleの祝日カレンダー API
const holidayCalId = 'ja.japanese#holiday@group.v.calendar.google.com';
const calendar = CalendarApp.getCalendarById(holidayCalId);
const events = calendar.getEventsForDay(date);
return events.length > 0; // 祝日イベントがあれば休み
}
テスト実行
それでは実際に動くかテストしてみましょう。
下の判定コード部分をコメントアウトをして、実行を押してみましょう。
if (!isLastBusinessDay()) {
console.log("今日は最終営業日ではないため、処理をスキップします。");
return;
}
※初めての場合は実行時に許可が求められると思うので許可をしてください!
実行結果
下の画像の通り、Slackにメール通知が届いていれば成功です👍
これらをコピペするだけで工数入力が可能になります。
手順3:トリガーの設定
テスト実行ができたらコメントアウトを外し、毎日自動で実行されるようにトリガーを設定します。
- GAS画面左側の「トリガー(時計アイコン)」をクリック。
- 右下の「+ トリガーを追加」をクリック。
- 以下のように設定して保存します。
-
実行する関数:
notifyMonthlyAttendance - イベントのソース: 時間主導型
- トリガーのタイプ: 日付ベースのタイマー
- 時刻: 午前8時〜9時
-
実行する関数:
これで、「毎朝プログラムが起動し、最終営業日の場合のみ通知を送る」仕組みの完成です。
実装のポイント
1. 「最終営業日」をシステム的に判定
工数入力の締め切りを守るため、「その月の最終稼働日」にリマインドと共にコマンドを届ける必要があります。
残念ながら、GASには標準で「月末営業日」といったトリガーがありません。そこで、日本の祝日カレンダーAPIを参照し、「今日が平日かつ、明日以降月末まで営業日がない」 という判定ロジックを実装することで、正確なタイミングでの通知を実現しました。
2. セキュリティを考慮したメール通知
Incoming Webhooksが制限されている環境でも実装できるよう、Slackのメールインテグレーション機能を採用しました。また、GASの GmailApp を使用することで、個人の送信履歴として管理でき、確実に通知を届けることができます。
さいごに - 正確な工数管理と効率化の両立
工数管理(稼働管理)は、適切なプロジェクト運営のために不可欠な業務です。
しかし、その入力作業自体に多くの時間を割いたり、入力ミスが発生してしまっては本末転倒です。
今回、Googleカレンダーの実績情報をもとにコマンドを自動生成することで、以下の効果が得られました。
- 入力精度の向上: 計算ミスや計上漏れがなくなり、正しいコスト管理ができるようになった。
- 業務効率化: 月末の入力作業が一瞬で終わり、本来の業務に集中できるようになった。
「社内の管理ルールを効率化してみたい」「でもWebhookは使えない...」「一旦試しで作ってみたい」という方がいれば、ぜひこの GAS × Slackメール投稿 のアプローチを試してみてください。
最後までお読みいただきありがとうございました!

