Googleフォーム回答をSlack/Discordへ通知|GAS実装手順

GASでGoogleフォーム回答をSlackとDiscordへ通知する実装手順のアイキャッチ AI自動化

Googleフォームの回答をSlackやDiscordへ自動で通知したい。GASとIncoming Webhookを使えば、Bot開発なしで実現できます。

フォームの回答をスプレッドシートで開いて確認していると、気づくのが遅れます。問い合わせが来ているのに半日返信できない、といったことが起きがちです。チームで動いているなら、回答が届いた瞬間にチャンネルへ飛ばすほうが確実です。

この記事では、Incoming Webhookの取得からGASのコード、トリガー設定までをコピペで動く形で解説します。Discordを中心に、Slackも併記します。私が実機で詰まった点も包み隠さず載せました。

なお、個人向けの通知は別記事で扱っています。メール通知はGoogleフォームの回答をメールで通知する方法、LINE通知はGoogleフォームの回答をLINEに通知する方法をご覧ください。今回はチーム向けのSlack/Discord編です。

なぜフォーム通知をSlack/Discordに送るのか

結論から言うと、チームで受けるフォームならSlack/Discordが最適です。

通知先は相手によって選び分けます。自分ひとりで受けるならメールやLINEで十分です。しかしチームで対応するなら、全員が見ている場所に落ちるほうが早い。SlackやDiscordなら、通知に対してその場でスレッドを立てて「これ私が対応します」と分担できます。メールだと各自の受信箱に散らばり、対応の重複や漏れが起きます。

技術的なハードルも低めです。SlackもDiscordもIncoming Webhookという仕組みを持っています。URLを1本発行して、そこにPOSTするだけで投稿できます。Bot開発もOAuthの実装も不要です。

LINE通知ではMessaging APIのチャネル作成が必要でした。それと比べると、Webhookは手軽さで一段上です。特にDiscordは数クリックでURLが手に入ります。

準備:Incoming Webhook URLを取得する

まずは通知先のWebhook URLを取得します。DiscordとSlackで手順が違うので、順に見ていきます。

Discordの場合

Discordは最速です。数クリックで終わります。

  1. 通知したいサーバーのサーバー設定を開く
  2. 連携サービス(Integrations)ウェブフック(Webhooks) を選ぶ
  3. 新しいウェブフックをクリックし、名前と投稿先チャンネルを設定する
  4. ウェブフックURLをコピーする

これだけです。Bot作成もアプリの承認もありません。テスト用に自分のサーバーを1つ作れば、すぐ試せます。

Slackの場合

Slackはアプリを1つ作る形になります。

  1. api.slack.com/appsCreate New App →「From scratch」→ ワークスペースを選ぶ
  2. 左メニューの Incoming Webhooks を開き、トグルを On にする
  3. Add New Webhook to Workspace → 投稿先チャンネルを選んで Allow
  4. 生成された Webhook URL をコピーする

Discordとの違いは、3のアプリ承認が挟まる点です。会社のワークスペースだと、管理者の承認が必要な設定になっていることもあります。その場合は先に確認しておくと手戻りがありません。

⚠️ Webhook URLはシークレットです。 このURLを知っている人は誰でもそのチャンネルに投稿できます。GitHubに上げたりコードにベタ書きしたりしないでください。次のステップでスクリプトプロパティに入れます。

GASでWebhookに通知を送る

ここが本題です。仕組みはシンプルで、次の流れになります。

フォーム回答 → onFormSubmit(トリガー)→ 回答を整形 → Webhook URLへPOST → チャンネルに通知

Googleフォームの回答がGASを経由してSlackとDiscordに通知されるまでの流れ図
フォーム回答→onFormSubmit→整形→Webhook POST→Slack/Discord通知の流れ

Webhook URLを安全に保存する

スクリプトエディタの左下「プロジェクトの設定」⚙️ →「スクリプト プロパティ」で、次を追加して保存します。

プロパティ名
DISCORD_WEBHOOK_URL DiscordでコピーしたウェブフックURL
SLACK_WEBHOOK_URL SlackでコピーしたウェブフックURL

片方だけ試すなら片方だけで構いません。後述のコードは、設定されている通知先にだけ送る作りにしてあります。

コード全文

そのまま貼って動きます。私の環境でフォーム送信から着弾まで確認済みのコードです。

// ① 権限を確実に要求させるための1行
//    e.response は静的解析でフォーム権限が検出されないため、明示的に呼んでおく
function _ensureFormPermission() {
  FormApp.getActiveForm();
}

// ② フォーム送信時に実行(インストール型トリガーで onFormSubmit を登録する)
function onFormSubmit(e) {
  try {
    _ensureFormPermission();

    var itemResponses = e.response.getItemResponses();
    var timestamp = e.response.getTimestamp();
    var message = buildMessage(itemResponses, timestamp);

    // 設定されている通知先すべてに送る
    var props = PropertiesService.getScriptProperties();
    if (props.getProperty("DISCORD_WEBHOOK_URL")) sendDiscord(message);
    if (props.getProperty("SLACK_WEBHOOK_URL"))  sendSlack(message);

    Logger.log("通知を送信しました: " + timestamp);
  } catch (error) {
    Logger.log("エラー: " + error.toString());
  }
}

// ③ 回答を通知本文に整形(Slack/Discord共通)
function buildMessage(itemResponses, timestamp) {
  var when = Utilities.formatDate(timestamp, "JST", "yyyy/MM/dd HH:mm");
  var lines = ["🔔 新しいフォーム回答", "🕒 " + when, ""];
  itemResponses.forEach(function (item) {
    lines.push("■ " + item.getItem().getTitle());
    lines.push(item.getResponse());
    lines.push("");
  });
  return lines.join("\n").trim();
}

// ④ Discordへ送る(payloadは {content: "..."})
function sendDiscord(text) {
  var url = PropertiesService.getScriptProperties().getProperty("DISCORD_WEBHOOK_URL");
  if (!url) throw new Error("スクリプトプロパティ DISCORD_WEBHOOK_URL が未設定です");

  var options = {
    method: "post",
    contentType: "application/json",
    // ⚠️ User-Agent必須:無いと Cloudflare に HTTP 429 で弾かれる
    headers: { "User-Agent": "Mozilla/5.0 (compatible; kverza-gas/1.0)" },
    payload: JSON.stringify({ content: text }),
    muteHttpExceptions: true
  };
  var res = UrlFetchApp.fetch(url, options);
  var code = res.getResponseCode();
  Logger.log("Discord応答: " + code + " " + res.getContentText());
  // ⚠️ Discordの成功は 204 No Content(bodyは空)。200ではない
  if (code !== 204 && code !== 200) {
    throw new Error("Discord送信失敗 HTTP " + code + " / " + res.getContentText());
  }
}

// ⑤ Slackへ送る(payloadは {text: "..."})← Discordとの差分はURLとpayload形だけ
function sendSlack(text) {
  var url = PropertiesService.getScriptProperties().getProperty("SLACK_WEBHOOK_URL");
  if (!url) throw new Error("スクリプトプロパティ SLACK_WEBHOOK_URL が未設定です");

  var options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify({ text: text }),
    muteHttpExceptions: true
  };
  var res = UrlFetchApp.fetch(url, options);
  var code = res.getResponseCode();
  Logger.log("Slack応答: " + code + " " + res.getContentText());
  if (code !== 200) {
    throw new Error("Slack送信失敗 HTTP " + code + " / " + res.getContentText());
  }
}

// ⑥ 動作確認用(フォームを送らずにテストできる)
function testDiscord() { sendDiscord("✅ テスト通知:GASからDiscordへの疎通確認"); }
function testSlack()   { sendSlack("✅ テスト通知:GASからSlackへの疎通確認"); }

DiscordとSlackの差分

コードを見ると分かるとおり、違いはごくわずかです。

項目 Discord Slack
Webhook取得 サーバー設定→連携→ウェブフック(承認不要) アプリ作成→Incoming Webhooks On→チャンネル承認
payloadのキー {content: "..."} {text: "..."}
User-Agentヘッダ 必須(無いと429で弾かれる) 不要
成功HTTPコード 204(bodyは空) 200(bodyは ok

つまり、片方を動かせればもう片方は流用できます。ただし、この「payloadのキーだけ違う」が後で私を刺しました。詳しくは次章で書きます。

onFormSubmitトリガーを登録する

ここが最大の落とし穴です。 onFormSubmit という名前の関数を書いただけでは発火しません。手動でトリガーを登録する必要があります。

  1. testDiscord(または testSlack)を手動実行し、承認画面で「外部サービスへの接続」を許可する。チャンネルに「✅ テスト通知」が来れば疎通は成功です
  2. onFormSubmit を手動実行する。e が undefined でエラーになりますが、これは正常です(フォームの権限承認画面を出すのが目的)
  3. スクリプトエディタの「トリガー」⏰ を開く
  4. 関数 onFormSubmit / イベントのソース「フォームから」/ イベントの種類「フォーム送信時」でトリガーを追加する

登録できたら、回答用リンク(公開URL)から実際にフォームを送信します。プレビュー👁からは送信できないので注意してください。

つまずいた点

ここからは実際に私が踏んだ点です。どれも手順を読んでいるだけでは気づけず、動かして初めて分かりました。

① Discordが初回から HTTP 429 で弾かれる

疎通確認の1発目で、いきなり HTTP 429 / error code: 1015 が返ってきました。429はレート制限のコードです。まだ1回しか送っていないのに、です。

原因はGASの通信元IPでした。GASは多数のユーザーが共有するGoogleのIPから外部へ通信します。そのため、Discordの前段にいるCloudflareから「このIPは大量にアクセスしている」と見なされ、まとめて制限されます。

解決は1行です。UrlFetchApp のオプションに User-Agent を足すと通りました。

headers: { "User-Agent": "Mozilla/5.0 (compatible; kverza-gas/1.0)" },

GASからDiscordへ送るなら、最初から入れておくことをおすすめします。

② Slackが HTTP 400 / no_text を返す

Discordを通したプロジェクトを流用してSlackを足したところ、HTTP 400/body no_text で弾かれました。

原因は payload のキーでした。sendSlack の中身が { content: text }(Discordのキー)のままだったのです。Slackはリクエスト自体は受け取りますが、text フィールドが無いので拒否します。

解決は { text: text } に1語変えるだけ。前章で「payloadの形だけ違う」と書きましたが、流用するとまさにここで踏みます。

③ トリガーの二重登録で通知が2通来る

以前のプロジェクトを使い回したとき、通知が2通届きました。既存の onFormSubmit トリガーが残ったまま、新しく追加してしまい、2個になっていたためです。

トリガー画面を開いて重複を削除し、1個だけ残せば解決します。使い回すときは既存トリガーの確認を習慣にしてください。

④(おまけ)日本語・絵文字・改行は崩れない

これはつまずきではなく、安心材料です。buildMessage で作った日本語・絵文字・改行(\n)は、SlackでもDiscordでもそのまま表示されました。エスケープ処理を足す必要はありません。

応用:他の通知手段との使い分け

チームへの通知だけでなく、回答した人本人へ自動返信メールを返したい場合は、Googleフォームの回答者に自動返信メールを送る方法(GAS)で解説しています。

通知先は目的で選びます。私の使い分けは次のとおりです。

通知先 向いている場面 準備の手間
メール 個人で受ける・記録を残したい 最小(GAS標準機能のみ)
LINE 個人で即座に気づきたい 中(Messaging API のチャネル作成)
Discord チーム・コミュニティで共有 小(Webhook 数クリック)
Slack 仕事のチームで共有・スレッド運用 小〜中(アプリ承認あり)

そして、これらは併用できます。上のコードはすでに複数同時通知に対応しています。onFormSubmit の中で、設定されているWebhookすべてに送る作りだからです。DiscordとSlackの両方にURLを入れれば、両方に飛びます。

メール通知やLINE通知を足したいなら、同じ要領で送信用の関数を1つ書き、onFormSubmit の中に並べるだけです。実装はメール通知の記事LINE通知の記事で解説しています。骨格は共通なので、送信部分だけ足せば済みます。

まとめ

GASでGoogleフォームの回答をSlack/Discordへ通知する手順を解説しました。要点は4つです。

  1. Incoming Webhook URLを取得する(Discordは数クリック、Slackはアプリ作成+承認)
  2. UrlFetchApp でPOSTする(DiscordとSlackの差はURLとpayloadのキーだけ)
  3. onFormSubmit トリガーを手動で登録する(書くだけでは発火しない)
  4. つまずきを先回りする(DiscordはUA必須、Slackは text キー、トリガー重複に注意)

まずは自分のテスト用サーバーで動かしてみてください。疎通が通れば、あとは本番のチャンネルにURLを差し替えるだけです。

この仕組み、あなたはどちらで進めますか?

🛠️ 作ってほしい人へ — あなたのフォームに合わせてSlack/Discord通知を実装します。ココナラで相談する(Googleフォーム自動化の専門サービス)

📦 自分で作りたい人へ — コピペで動くGAS自動化テンプレ集をnoteにまとめています。GAS自動化テンプレ集を見る

最後まで読んでいただきありがとうございました。

コメント

タイトルとURLをコピーしました