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は最速です。数クリックで終わります。
- 通知したいサーバーのサーバー設定を開く
- 連携サービス(Integrations) → ウェブフック(Webhooks) を選ぶ
- 新しいウェブフックをクリックし、名前と投稿先チャンネルを設定する
- ウェブフックURLをコピーする
これだけです。Bot作成もアプリの承認もありません。テスト用に自分のサーバーを1つ作れば、すぐ試せます。
Slackの場合
Slackはアプリを1つ作る形になります。
- api.slack.com/apps で Create New App →「From scratch」→ ワークスペースを選ぶ
- 左メニューの Incoming Webhooks を開き、トグルを On にする
- Add New Webhook to Workspace → 投稿先チャンネルを選んで Allow
- 生成された Webhook URL をコピーする
Discordとの違いは、3のアプリ承認が挟まる点です。会社のワークスペースだと、管理者の承認が必要な設定になっていることもあります。その場合は先に確認しておくと手戻りがありません。
⚠️ Webhook URLはシークレットです。 このURLを知っている人は誰でもそのチャンネルに投稿できます。GitHubに上げたりコードにベタ書きしたりしないでください。次のステップでスクリプトプロパティに入れます。
GASでWebhookに通知を送る
ここが本題です。仕組みはシンプルで、次の流れになります。
フォーム回答 → onFormSubmit(トリガー)→ 回答を整形 → Webhook URLへPOST → チャンネルに通知

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 という名前の関数を書いただけでは発火しません。手動でトリガーを登録する必要があります。
testDiscord(またはtestSlack)を手動実行し、承認画面で「外部サービスへの接続」を許可する。チャンネルに「✅ テスト通知」が来れば疎通は成功ですonFormSubmitを手動実行する。eが undefined でエラーになりますが、これは正常です(フォームの権限承認画面を出すのが目的)- スクリプトエディタの「トリガー」⏰ を開く
- 関数
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つです。
- Incoming Webhook URLを取得する(Discordは数クリック、Slackはアプリ作成+承認)
UrlFetchAppでPOSTする(DiscordとSlackの差はURLとpayloadのキーだけ)onFormSubmitトリガーを手動で登録する(書くだけでは発火しない)- つまずきを先回りする(DiscordはUA必須、Slackは
textキー、トリガー重複に注意)
まずは自分のテスト用サーバーで動かしてみてください。疎通が通れば、あとは本番のチャンネルにURLを差し替えるだけです。
この仕組み、あなたはどちらで進めますか?
🛠️ 作ってほしい人へ — あなたのフォームに合わせてSlack/Discord通知を実装します。ココナラで相談する(Googleフォーム自動化の専門サービス)
📦 自分で作りたい人へ — コピペで動くGAS自動化テンプレ集をnoteにまとめています。GAS自動化テンプレ集を見る
最後まで読んでいただきありがとうございました。


コメント