こんにちは、こんです。
今回は「予定は登録されているのに、誰も気づいていない…」という事態をなくすために、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で取得するため追加設定は不要

🧩 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通知例

まとめ:Notionをスケジュールの“主語”にしよう
Notion Calendarは便利だけど、「予定がデータベースとして扱えない(と思われる)」という根本課題がありました。
今回の実験で、「Formで予定を登録→DBに蓄積→Slackで通知・リマインド」という一連のフローをNotion中心で回せるようになり、予定が“活きたデータ”として機能する状態を作れました。
日々の小さな混乱が、ちょっとずつ解消されていくのが気持ちいい。
それでは、また次の実験で!🦊
#Notion連携 #Slack通知 #Webhook #業務効率化 #GASで自動化