Kon's DX Lab - Case Study

Day 38 | Notion × Slackで予定共有を自動化してみた実験記録【Webhook連携+GASコード付き】

Published on 2025-05-09

🔬 Case Study Summary
Problem

(ここに課題を記述)

Result

(ここに具体的な成果を記述)


Tech & Process

(ここに採用技術とプロセスを記述) コードを詳しく見る »

こんにちは、こんです。

今回は「予定は登録されているのに、誰も気づいていない…」という事態をなくすために、NotionとSlackをWebhookでつなぐ通知連携ツールをつくってみた実験記録を残します。

予定の登録も通知もリマインドも全部自動。しかもSlackではメンションで関係者にだけ届く。

GAS × Notion Automation × Slack API を組み合わせれば、意外と簡単に実現できました。


実験のきっかけ:Notion Calendarでは予定が「データ」にならなかった

こんなこと、起きていませんか?

  • 📆 Notion Calendarで予定を登録しても、Notion Databaseには追加されない

  • 🔗 Google Calendarには連携できるのに、Notion上でスケジュールを「蓄積」できない

  • 🧾 チームで予定一覧を管理・通知したいのに、カレンダーだけでは整理しきれない

「Calendarで予定は見えるけど、データとして使えない…」という違和感から、今回はNotionフォームで予定登録 → DBに蓄積 → Slackで通知・リマインドという仕組みを構築してみました。


作ってみたもの(システムの概要)

今回はこんな構成を実装しました:

  • ✅ Notion AutomationでWebhookをGASへ送信

  • ✅ Webhookを受け取ってGASがNotion APIで予定を取得

  • ✅ Slack APIで関係者にメンション付きで通知

  • ✅ 当日8時・週次月曜朝のリマインドも自動送信


使ってみて感じたメリット

1. 通知し忘れゼロ!Slackで即キャッチできる

フォームで予定を登録すると、その瞬間Slack DMやチャンネルに通知が飛ぶ。もう「伝えたつもりだった…」がなくなります。

2. Notionの参加者情報をそのまま使える

「参加者」プロパティ(Relation)+「SlackユーザーID」プロパティ(Text)を連動。Notionだけで完結した通知フローが実現。

3. リマインドもGASで制御できる

「当日朝にDMを飛ばしたい」「週次一覧をチームに出したい」といった実務ニーズもGASで柔軟にカバーできます。


システムの仕組み図(構成イメージ)

[Notion DB (Formビュー)]
    ↓ Automation Webhook
[Google Apps Script (doPost)]
    ↓ Notion API
    ↓ Slack API
[Slack](DM / チャンネル通知)

実装手順(要点だけ紹介)

🧩 Notion Automation Webhook

  • WHEN:Page added または Any property edited

  • DO:Send webhook

  • URL:GASでデプロイしたWebhook URL

  • プロパティ:APIで取得するため追加設定は不要

例:新規で登録された予定を検知するためのAutomation

🧩 Google Apps Script(Webhook受信)側のコード(※Page addedの事例)

//-------
// declaration
//-------
const NOTION_API_KEY = '';  // Internal Integration Secret
const SLACK_BOT_TOKEN = '';   // Bot User OAuth Token
const NOTION_MEMBERS_DB_ID = '';  // メンバーDBのID(SlackメンバーIDをプロパティとして保持)
const NOTION_SCHEDULE_DB_ID = ''; // スケジュールDBのID

//-------
// main 
//-------
function doPost(e) {
  try {
    const payload = JSON.parse(e.postData.contents);
    const pageId = payload.data.id;

    const pageData = getNotionPageDataWithRetry(pageId, 3, 2000);
    const projectRelation = pageData.properties['案件']?.relation || [];
    const participants = pageData.properties['参加者']?.relation || [];
    const start = pageData.properties['日時']?.date?.start || null;
    const end = pageData.properties['日時']?.date?.end || null;

    const projectName = projectRelation.length > 0 ? getTitleByPageId(projectRelation[0].id) : '(案件名未取得)';

    const slackMentions = participants.map(p => getSlackIdByMemberId(p.id))
                                     .filter(id => id)
                                     .map(id => `<@${id}>`);

    const dateStr = (start && end) ? formatJapaneseDateRange(start, end) : '🗓 日時未定';

    const message = `📢 *「${projectName}」のブレストスケジュールが登録されました!*\n${dateStr}\n👥 参加者: ${slackMentions.join(' ')}\n🔗 <https://www.notion.so/${pageId.replace(/-/g, '')}|Notionページはこちら>`;

    sendSlackMessage("C053QEK5NF2", message);  // ← 通知先のチャンネルIDに変更

    return ContentService.createTextOutput("通知完了");
  } catch (error) {
    return ContentService.createTextOutput("エラー: " + error.message);
  }
}

//-------
// common
//-------
function getNotionPageDataWithRetry(pageId, retries = 3, delayMs = 2000) {
  for (let i = 0; i < retries; i++) {
    const data = getNotionPageData(pageId);
    if (Object.keys(data.properties || {}).length > 0) return data;
    Utilities.sleep(delayMs);
  }
  throw new Error("プロパティ取得に失敗しました(空のまま)");
}

function getNotionPageData(pageId) {
  const options = {
    method: "get",
    headers: {
      "Authorization": `Bearer ${NOTION_API_KEY}`,
      "Notion-Version": "2022-06-28"
    }
  };
  const response = UrlFetchApp.fetch(`https://api.notion.com/v1/pages/${pageId}`, options);
  return JSON.parse(response.getContentText());
}

function getTitleByPageId(pageId) {
  const options = {
    method: "get",
    headers: {
      "Authorization": `Bearer ${NOTION_API_KEY}`,
      "Notion-Version": "2022-06-28"
    }
  };
  const response = UrlFetchApp.fetch(`https://api.notion.com/v1/pages/${pageId}`, options);
  const data = JSON.parse(response.getContentText());
  return data.properties['氏名']?.title?.[0]?.plain_text ||
         data.properties['案件名']?.title?.[0]?.plain_text ||
         data.properties['タイトル']?.title?.[0]?.plain_text ||
         '(名称未取得)';
}

function getSlackIdByMemberId(memberPageId) {
  const options = {
    method: "get",
    headers: {
      "Authorization": `Bearer ${NOTION_API_KEY}`,
      "Notion-Version": "2022-06-28"
    }
  };
  const response = UrlFetchApp.fetch(`https://api.notion.com/v1/pages/${memberPageId}`, options);
  const data = JSON.parse(response.getContentText());
  return data.properties['Slack']?.rich_text?.[0]?.plain_text ||
         data.properties['Slack']?.text || null;
}

function formatJapaneseDateRange(startISO, endISO) {
  const days = ['日', '月', '火', '水', '木', '金', '土'];
  const start = new Date(startISO);
  const end = new Date(endISO);

  const y = start.getFullYear();
  const m = start.getMonth() + 1;
  const d = start.getDate();
  const day = days[start.getDay()];
  const h1 = start.getHours();
  const min1 = String(start.getMinutes()).padStart(2, '0');
  const h2 = end.getHours();
  const min2 = String(end.getMinutes()).padStart(2, '0');

  return `📅 ${y} 年 ${m} 月 ${d} 日 (${day}) ${h1} 時 ${min1} 分 ~ ${h2} 時 ${min2} 分`;
}

function sendSlackMessage(channelIdOrUserId, message) {
  const payload = {
    channel: channelIdOrUserId,
    text: message
  };
  const options = {
    method: "post",
    contentType: "application/json",
    headers: {
      "Authorization": `Bearer ${SLACK_BOT_TOKEN}`
    },
    payload: JSON.stringify(payload)
  };
  UrlFetchApp.fetch("https://slack.com/api/chat.postMessage", options);
}

🧩 実際のSlack通知例

メンションされるので、見逃しがちなSlack通知でも目に入りやすく、 かつ「Notionに戻って確認」も1クリックで完了。

まとめ:Notionをスケジュールの“主語”にしよう

Notion Calendarは便利だけど、「予定がデータベースとして扱えない(と思われる)」という根本課題がありました。

今回の実験で、「Formで予定を登録→DBに蓄積→Slackで通知・リマインド」という一連のフローをNotion中心で回せるようになり、予定が“活きたデータ”として機能する状態を作れました。

日々の小さな混乱が、ちょっとずつ解消されていくのが気持ちいい。
それでは、また次の実験で!🦊

#Notion連携 #Slack通知 #Webhook #業務効率化 #GASで自動化