こんにちは。25新卒エンジニアの緑川です。
この記事では、私が社内でのルーティンを自動化しようとして、うまくいったりいかなかったことについて紹介します。
目的
セーフィーの開発本部では、メンバーの勤務状況を把握するため、毎日仕事の開始時と終了時に「始業」「終業」のスタンプをSlackチャンネルへ送信するルールがあります。
さらに、我々新卒エンジニアは研修期間中、その日やったことについてまとめた日報をNotionに書き、報告に添付することになっています。

こんな感じ
この投稿をするためには、
- 日報のURLをコピーする
- Slackに、
:syugyo:と打ち込む - 改行して、
日報と打ち込む 日報の部分を選択- URLをペースト
という、たいしたことはないものの毎日だと微妙に面倒くさい作業を要していました。
この日報報告の投稿を、たとえば、ボタンを一つ押すだけでできたら楽じゃないかと考えました。
先輩に倣う
そんなことを1年先輩のトレーナーに相談すると、まさに同じことを去年やったと教えていただきました。やり方は以下の通りです。
- GASのウェブアプリを作り、クエリパラメータつきのGETリクエストを送るとその内容がJSONでSlackに送られるようにする
- Notionの日報テンプレートに、クエリパラメータとして記事のIDと作成者を埋め込んだリンクを自動で作成する関数を置く
- 関数で生成されたリンクをクリックするとブラウザからGASにリクエストが飛び、それをもとにSlackに情報が送信される
Slackで、
{名前} :syugyo: 日報という投稿を行う
sequenceDiagram
ユーザー ->> Notion: 記事上の通知リンクをクリック
Notion ->> GAS: クエリパラメータ(記事URLなど)を埋め込んだURLで遷移
Note over GAS: クエリパラメータからSlack向けJSONを作成
GAS ->> Slack ワークフロー: JSONを含んだPOSTリクエストを送信
Slack ワークフロー ->> 出退勤連絡チャンネル: 記事リンク付き終業報告を送信
これを拝借(コピペ)すれば目的達成!ですが、それだけだと面白くないので、何かアップデートを加えたいものです。このやり方では、NotionとSlackの仲介役としてGASが使われています。このGASをなくし、SlackとNotionで直接通信することはできないか?と考えました。
Notionボタン
Notionでは、ボタンというオブジェクトが使えます。

ボタンが押されると指定した処理が実行可能という便利な機能ですが、その中にWebhook送信というものがあります。これは、POSTリクエストで指定URLにJSONを送信するというシンプルな動作をします。ここから作成者と記事のIDを送信するように設定することができれば、Slackを直接操作できそうです。
Slackワークフロー
ここでSlack側の機能について説明します。今回終業報告の自動投稿に使ったのは、Slackワークフローです。これは特定のイベントが起きたときに設定した処理を実行できるという機能です。よって、Webhookを受信したときに、そこに含まれた情報を埋め込んだ投稿を指定チャンネルに送信するように設定します。
今回は、日報のURLを特定するのに必要な記事のID(notion_id)と書いた人の名前(name)を受け取るように設定し、それをメッセージに埋め込むことにします。
手順は以下の通りです。
- Slack左下の「その他」から、「自動化」を選択
- 右上の「+新しいワークフロー」を押す
- ワークフローの開始条件を「Webhook を使って開始する」 受け取る変数として、notion_idとnameを設定

- 指定チャンネルに受け取った情報を埋め込んだメッセージを送信するように指定

- ワークフローのURLをコピーしておく
NotionボタンからSlackへwebhook送信
いよいよNotionボタンからSlackへのWebhook送信を試してみます。
まず、日報の記事テンプレートの中にボタンを作成し、日報の記事には自動でボタンが準備されるようにします。そして、以下のようにWebhookの設定を行います。
/buttonコマンドでボタンを作成する。- 下の画像のような設定画面が表示
- 実行を「Webhook」を送信する」に設定。
- 送信先のSlackワークフローURLを入力
- 「コンテンツ」の項目から送信したいページのプロパティを選択

これで完成!実際にボタンを押してSlackに投稿がされるかどうか試してみます。
失敗
しかし、いくらボタンを押しても何も起きませんでした。ここで、Notionから送信されるJSONの構造がどうなっているのか気にしていなかったことに気づきました。
調べてみると、Notionから送信されるWebhookに含まれるJSONは、設定した情報以外にも多くのメタデータを含み、階層も複雑になっていることがわかりました。一方、SlackワークフローではJSONの1階層目にあるキーだけを参照できるようでした。
{ "notion_id": "<id>", "name": "<name>" }
上は、Slackで受け取れるJSONのイメージです。階層構造はありません。
{ "id": "{ID}", "timestamp": "2025-01-01T07:00:00.001Z", "workspace_id": "{ID}", "workspace_name": "{ワークスペース名}", "subscription_id": "{ID}", "integration_id": "{ID}", "type": "page.created", "authors": [ { "id": "{ID}", "type": "person" } ], "accessible_by": [ { "id": "{ID}", "type": "person" } ], "attempt_number": 1, "entity": { "id": "{ID}", "type": "page" }, "data": { // この値がほしい "parent": { "id": "{ID}", "type": "page" } } }
上はNotionから送られるJSONの例です。多くのメタデータが入り、階層構造になっていることがわかります。Slackで受け取りたいNotion記事の情報は data キーの下に入るため、Slackは直接取得することができません。
つまり、NotionボタンとSlackだけの連携は難しいようです。GASを削除するという目論見は潰えてしまいました。
多重投稿の防止
仕方がないので、GASの削除はあきらめ運用しました。お手軽に報告できるのは機能自体は好評でよく使われたものの、日報内をワンクリックするだけで終業報告を送ることができるという手軽さから、一つ問題が発生してしまいました。日報は書いた本人以外も読むことがあるので、その時に誤って通知ボタンをクリックされた結果、その時点でもう一度終業報告が投稿されてしまったのです。しかも、Slackでの送信元が日報を書いたユーザーではなくワークフローのbotになるので、投稿を削除することもできません。これでは困ります。
この問題を解決するために、以下のような方法を考えました。
- GASで、記事IDをスプレッドシートに記録する。
- リクエストが来るたびに、スプレッドシートを確認し、既存の記事IDだったらSlackへ情報を送らない
これで、最初の一回しかSlackに通知が投稿できません。
以下はGASに実装したソースコードの全体です。
// GETリクエストが来たときに実行する関数 const doGet = (e) => { const params = e.parameter const workflow_url = "<SlackワークフローのURL>"; if (!params.id || !params.name) return ContentService.createTextOutput("パラメータエラー発生: " + e.message); const data = {"notion_id": params.id, "name": params.name} if (is_artice_already_saved(params.id)) { return ContentService.createTextOutput("指定された日報はSlackに投稿済みです "); } save_article_id(params.id, params.name) try{ UrlFetchApp.fetch(workflow_url, { method: "POST", contentType: "application/json", payload : JSON.stringify(data), }) }catch(e){ return ContentService.createTextOutput("Slackエラー発生: " + e.message); } return ContentService.createTextOutput("退勤を登録しました!"+JSON.stringify(params)); } // IDが登録済みか const is_artice_already_saved = (id) => { var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1'); const idIndex = 1; // A列 const lastRow = sheet.getLastRow(); const ids = sheet.getRange(1, idIndex, lastRow, 1).getValues(); // 各セルの値を入力IDと比較 for (let i = 0; i < ids.length; i++) { if (ids[i][0] === id) { return true; } } return false; } // スプレッドシートへの書き込み const save_article_id = (id, name) => { var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1'); sheet.appendRow([id, name]) }
また、最終的なシーケンス図は以下のようになりました。
sequenceDiagram
participant User
participant Notion
participant GAS
participant GoogleSpreadsheet as Google Spreadsheet
participant SlackWorkflow as Slackワークフロー
participant SlackChannel as 出退勤連絡チャンネル
User->>Notion: 記事上の通知リンクをクリック
Notion->>GAS: クエリパラメータ(記事IDなど)を埋め込んだURLで遷移
Note over GAS: クエリパラメータからSlack向けJSONを作成
GAS->>GoogleSpreadsheet: 記事IDの存在確認
alt 記事が存在する
GoogleSpreadsheet-->>GAS: 記事IDが存在する
Note over GAS: 処理を終了
else 記事が存在しない
GoogleSpreadsheet-->>GAS: 記事IDが存在しない
GAS->>SlackWorkflow: JSONを含んだPOSTリクエストを送信
SlackWorkflow->>SlackChannel: 記事リンク付き終業報告を送信
end
GASでの処理に、スプレッドシートに記事が記録されているかどうかの条件分岐が追加されました。この対策を行ったあと意図しない終業報告がされる事故は起こっていません。

自動終業報告の様子
上は、実際に終業報告が行われている様子です。
おわりに
結果的にはほとんど先輩の作ったものの模倣にはなってしまいましたが、このシステムの制作を通じてアプリ間の連携方法について知見を得ることができました。たとえば、同期の間で読んだ本や参加した勉強会についてNotionで紹介する場があるのですが、その更新通知を自動でSlackに送信する仕組みを整備できました。
また、なによりも実際に同期の新卒メンバーに作ったものが毎日使われているのがうれしいです。今後は課題を見つける能力と技術知識をさらに鍛え、ユーザーの課題を解決できるエンジニアになりたいです。
25年度新卒研修に関する記事はこちらをご覧ください。