- 技術 💻
GASでGoogleフォーム自動返信メール|onFormSubmit早見表
GAS(Google Apps Script)でフォームの自動返信メールを送るには、「フォーム送信時」のインストール型トリガーで関数を動かし、引数のイベントオブジェクト(e)から回答を取り出して MailApp.sendEmail で送ります。 トリガーはフォームにもスプレッドシートにも付けられ、どちらに付けたかで e の中身が変わります。
この記事では、2つのトリガーで受け取れる値の早見表、両方の方式の自動返信コード、管理者への通知、回答内容による文面の出し分け、try-catch での記録、FormApp での質問の一括作成、送信数の上限と権限をまとめます。メソッド名・引数・上限の数値は、Google の Apps Script 公式リファレンスを開いて照合したものだけを載せています。
確認環境:Google Apps Script 公式リファレンス・ガイド(developers.google.com/apps-script、英語版)と Google ドキュメント エディタ ヘルプを 2026-10-05 に照合。コードは Node.js 22 で構文を検査し、GAS のサービス(MailApp など)を置き換える簡易スタブを使って分岐の動きを確かめました。
この記事の結論
- 回答者に回答の控えを返すだけなら、フォームの設定で足ります(「メールアドレスを収集する」と「回答のコピーを回答者に送信」)。
- 文面を回答内容で変える・管理者にも知らせる・シートに結果を残すなら、GAS のフォーム送信時トリガーを使います。
- シートに記録も残したいならスプレッドシートに付けたトリガー(e.namedValues)、フォームだけで完結させたいならフォームに付けたトリガー(e.response)を選びます。
- 回答は必ず e から取り出します。 送信ごとにその回答が e に入って渡されるので、シートの最終行を読みにいく必要がありません。
- 1日に送れる宛先の数には上限があります(無料の Google アカウントは100、Google Workspace は1,500)。送る前に
MailApp.getRemainingDailyQuota()で残りを確かめます。
onFormSubmitの早見表:トリガーを付ける場所で e が変わる
| 比べる点 | スプレッドシートに付けたトリガー | フォームに付けたトリガー |
|---|---|---|
| スクリプトの置き場所 | 回答先のスプレッドシートに紐づくスクリプト | フォームに紐づくスクリプト |
| 回答の入り方 | e.namedValues:質問のタイトルをキー、回答の配列を値にしたオブジェクト |
e.response:送信された回答を表す FormResponse |
| ほかに受け取れる値 | e.values:回答の値を並び順で入れた配列/e.range:回答が書き込まれたセル範囲(Range)/e.authMode、e.triggerUid |
e.source:スクリプトが紐づくフォーム(Form)/e.triggerUid |
| 1つの回答の取り出し方 | e.namedValues['お名前'][0] |
e.response.getItemResponses() を回して getItem().getTitle() と getResponse() |
| 回答の行に結果を書く | e.range.getSheet() と e.range.getRow() で同じ行に書ける |
シートが無いので、実行ログやメールで知らせる |
| 送信日時 | シートに書き込まれた値を読む | e.response.getTimestamp() |
| 回答の編集用URL | ― | e.response.getEditResponseUrl() |
| コードで作るとき | ScriptApp.newTrigger('関数名').forSpreadsheet(ss).onFormSubmit().create() |
ScriptApp.newTrigger('関数名').forForm(form).onFormSubmit().create() |
| 向く使い方 | 回答シートに送信結果や担当者欄を足していく | 回答先のシートを作らずに、フォームだけで返信する |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「GASでGoogleフォーム自動返信メール|onFormSubmit早見表」
フォームの設定とGASのどちらを使うか
| 条件 | 選ぶもの |
|---|---|
| 回答の控えをそのまま回答者に送れば足りる | フォームの設定「回答のコピーを回答者に送信」 |
| 新しい回答が来たことを自分が知りたいだけ | フォームの「新しい回答についてのメール通知を受け取る」 |
| 回答内容に応じて案内文を変えたい | GAS |
| 件名・差出人名・返信先を決めたい | GAS |
| 回答者と管理者に別々の文面を送りたい | GAS |
| 回答の行に「送信済み」などの結果を残したい | GAS(スプレッドシートに付けたトリガー) |
フォームの設定では、「メールアドレスを収集する」に「確認済み」「回答者からの入力」、「回答のコピーを回答者に送信」に「リクエストされた場合」「常に表示」という選択肢があります(Google ドキュメント エディタ ヘルプの表記)。
例にするフォームと共通の関数
架空の「社内勉強会 参加申込フォーム」を例にします。質問は次の5つです。
| 質問のタイトル | 種類 | 必須 | 選択肢 |
|---|---|---|---|
| メールアドレス | 記述式(1行) | はい | ― |
| お名前 | 記述式(1行) | はい | ― |
| 参加形式 | ラジオボタン | はい | 会場、オンライン |
| 参加する回 | プルダウン | はい | 第1回(10月20日)、第2回(11月17日) |
| 質問・連絡事項 | 段落 | いいえ | ― |
メールアドレスは質問として置いています。こうすると、どちらの方式でも「メールアドレス」というタイトルで同じように取り出せます。フォームの設定でメールアドレスを収集している場合、フォームに付けたトリガーでは e.response.getRespondentEmail() でも取得できます(このメソッドは、メールアドレスの収集を有効にしているフォームで使えます)。
まず、2つの方式で共通に使う「文面を作る」「2通送る」関数です。回答を「質問のタイトル → 回答」のオブジェクトにそろえてから渡す形にしておくと、方式を変えても文面の部分はそのまま使えます。
// ===== 設定(自分の環境に合わせて書き換える)=====
const ADMIN_EMAIL = 'admin@example.com'; // 管理者の通知先
const SENDER_NAME = '勉強会事務局'; // 差出人として表示する名前
const EVENT_NAME = '社内勉強会';
// 質問のタイトル(フォームの質問文と一字一句そろえる)
const Q = {
email: 'メールアドレス',
name: 'お名前',
style: '参加形式',
session: '参加する回',
note: '質問・連絡事項',
};
/** 回答内容から、回答者あての件名と本文を作る */
function buildReply_(answers) {
const lines = [
answers[Q.name] + ' 様',
'',
EVENT_NAME + 'へのお申し込みを受け付けました。',
'',
'■ 参加する回:' + answers[Q.session],
'■ 参加形式:' + answers[Q.style],
'',
];
// 回答内容による文面の出し分け
if (answers[Q.style] === '会場') {
lines.push('当日は開始10分前までに、3階の会議室Aへお越しください。');
} else {
lines.push('接続用のURLは、開催日の前日までにこのメールアドレスへお送りします。');
}
if (answers[Q.note]) {
lines.push('', 'ご記入いただいた内容は、担当者が確認してあらためてご連絡します。');
}
lines.push('', '― ' + SENDER_NAME);
return {
subject: '【受付完了】' + EVENT_NAME + '(' + answers[Q.session] + ')',
body: lines.join('\n'),
};
}
/** 管理者あての件名と本文を作る */
function buildAdminNotice_(answers) {
const needsReply = Boolean(answers[Q.note]);
const body = Object.keys(answers)
.map(function (title) { return title + ':' + answers[title]; })
.join('\n');
return {
subject: (needsReply ? '【要返信】' : '【申込】') + answers[Q.name] + ' 様(' + answers[Q.session] + ')',
body: body,
};
}
/** 回答者と管理者の2通を送る。残りの送信数が足りなければ送らずに理由を返す */
function sendBoth_(answers) {
if (!answers[Q.email]) {
return 'メールアドレスが空のため保留';
}
if (MailApp.getRemainingDailyQuota() < 2) {
return '本日の送信数の上限のため保留';
}
const reply = buildReply_(answers);
MailApp.sendEmail(answers[Q.email], reply.subject, reply.body, {
name: SENDER_NAME,
replyTo: ADMIN_EMAIL,
});
const notice = buildAdminNotice_(answers);
MailApp.sendEmail(ADMIN_EMAIL, notice.subject, notice.body);
return '送信済み';
}
| 関数 | 役割 |
|---|---|
buildReply_ |
回答者あての件名と本文を作る。参加形式が「会場」か「オンライン」かで案内文を変え、質問欄に記入があれば一文を足す |
buildAdminNotice_ |
管理者あての件名と本文を作る。質問欄に記入があれば件名を【要返信】にする |
sendBoth_ |
宛先が空でないこと、残りの送信数が2件以上あることを確かめてから2通送り、結果を文字列で返す |
MailApp.sendEmail(recipient, subject, body, options) の options では、差出人の表示名(name)と返信先(replyTo)を指定しています。回答者がそのまま返信すると、管理者のアドレスに届きます。
方式A:スプレッドシートに付けたトリガーで自動返信する
フォームの回答先になっているスプレッドシートのスクリプトに、共通の関数と一緒に貼り付けます。
/**
* 方式A:スプレッドシートに付けたフォーム送信時トリガーで動く関数
* e.namedValues … { 質問のタイトル: [回答] } の形
* e.values … 回答の値を並び順で入れた配列
* e.range … 回答が書き込まれたセル範囲
*/
function onFormSubmitSheet(e) {
const sheet = e.range.getSheet();
const row = e.range.getRow();
const statusColumn = e.range.getLastColumn() + 1; // 回答の右隣の列に結果を書く
try {
// namedValues の値は配列なので、先頭の要素を取り出して普通の文字列にする
const answers = {};
Object.keys(e.namedValues).forEach(function (title) {
answers[title] = e.namedValues[title][0];
});
const result = sendBoth_(answers);
sheet.getRange(row, statusColumn).setValue(result);
} catch (err) {
// 止まった理由を同じ行に残し、実行ログにも出す
sheet.getRange(row, statusColumn).setValue('エラー:' + err.message);
console.error('onFormSubmitSheet row=' + row + ' ' + err.stack);
}
}
/** 方式Aのトリガーを作る(スクリプトエディタで1回だけ実行する) */
function createSheetTrigger() {
const exists = ScriptApp.getProjectTriggers().some(function (t) {
return t.getHandlerFunction() === 'onFormSubmitSheet';
});
if (exists) {
console.log('トリガーは作成済みです。');
return;
}
ScriptApp.newTrigger('onFormSubmitSheet')
.forSpreadsheet(SpreadsheetApp.getActive())
.onFormSubmit()
.create();
}
ポイントは3つです。
e.namedValuesの値は配列です。e.namedValues['お名前']は['山田 太郎']のような配列なので、[0]を付けて取り出します。コードでは最初にまとめて文字列へ詰め替えています。- 結果は
e.rangeと同じ行に書きます。e.range.getRow()が今回の回答の行番号なので、同時に複数の送信があっても、それぞれの行に自分の結果が残ります。 - try-catch で囲み、止まった理由を同じ行に残します。 あとからシートを見れば、どの回答に返信できていないかが一目で分かります。
トリガーの作り方
コードの createSheetTrigger をスクリプトエディタで1回実行すると、トリガーが作られます。同じ関数名のトリガーがすでにあれば作らないようにしてあるので、2回実行しても二重に送られません。
画面から作る場合は、公式ガイドの手順どおり、スクリプトエディタの左側で「トリガー」を開き、右下の「トリガーを追加」から、実行する関数に onFormSubmitSheet、イベントの種類に「フォーム送信時」を選んで保存します。
初回は承認の画面が表示されます。Apps Script はコードを調べて必要な権限を割り出し、足りない権限があると許可を求めます。
方式B:フォームに付けたトリガーで自動返信する
フォームに紐づくスクリプトに、共通の関数と一緒に貼り付けます。回答先のスプレッドシートが無くても動きます。
/**
* 方式B:フォームに付けたフォーム送信時トリガーで動く関数
* e.response … 送信された回答(FormResponse)
* e.source … スクリプトが紐づいているフォーム(Form)
*/
function onFormSubmitForm(e) {
try {
// 質問のタイトルをキーにしたオブジェクトへ詰め替える
const answers = {};
e.response.getItemResponses().forEach(function (itemResponse) {
const value = itemResponse.getResponse();
answers[itemResponse.getItem().getTitle()] =
Array.isArray(value) ? value.join('、') : value;
});
const result = sendBoth_(answers);
console.log(e.response.getId() + ' ' + result);
} catch (err) {
// フォーム側には書き込み先のシートが無いので、実行ログと管理者あてメールで知らせる
console.error('onFormSubmitForm ' + err.stack);
MailApp.sendEmail(
ADMIN_EMAIL,
'【要確認】自動返信が止まりました',
'回答ID:' + e.response.getId() + '\n' +
'送信日時:' + e.response.getTimestamp() + '\n' +
'内容:' + err.message
);
}
}
/** 方式Bのトリガーを作る(スクリプトエディタで1回だけ実行する) */
function createFormTrigger() {
const exists = ScriptApp.getProjectTriggers().some(function (t) {
return t.getHandlerFunction() === 'onFormSubmitForm';
});
if (exists) {
console.log('トリガーは作成済みです。');
return;
}
ScriptApp.newTrigger('onFormSubmitForm')
.forForm(FormApp.getActiveForm())
.onFormSubmit()
.create();
}
e.response.getItemResponses()は、フォームでの並び順どおりに1問ずつの回答(ItemResponse)を返します。getItem().getTitle()が質問のタイトル、getResponse()が回答です。getResponse()の型は質問の種類で変わります。 多くの質問は文字列、チェックボックスは文字列の配列です。コードでは配列なら「、」でつないで1つの文字列にしています。- フォーム側には結果を書く行が無いので、止まったときは実行ログに残し、管理者にもメールで知らせます。ログはスクリプトエディタの「実行数」で確認できます。
スタブで分岐を確かめた結果
GAS そのものは手元で動かせないため、MailApp やシートを置き換える簡易スタブを Node.js 22 で用意し、上のコードをそのまま読み込んで呼び出しました。Google のサーバーでメールを送った結果ではなく、分岐と文面の組み立てを確かめたものです。
| 渡した回答 | 回答の行に書かれた結果 | 送った通数 | 管理者あての件名 |
|---|---|---|---|
| 会場・質問欄は空 | 送信済み | 2 | 【申込】山田 太郎 様(第1回(10月20日)) |
| オンライン・質問欄に記入あり | 送信済み | 2 | 【要返信】山田 太郎 様(第1回(10月20日)) |
| 残りの送信数が1件 | 本日の送信数の上限のため保留 | 0 | ― |
| メールアドレスが空 | メールアドレスが空のため保留 | 0 | ― |
| 送信時に例外(Invalid email)を起こす | エラー:Invalid email | 0 | ― |
「会場・質問欄は空」のときに組み立てられた、回答者あての件名と本文です。
【受付完了】社内勉強会(第1回(10月20日))
山田 太郎 様
社内勉強会へのお申し込みを受け付けました。
■ 参加する回:第1回(10月20日)
■ 参加形式:会場
当日は開始10分前までに、3階の会議室Aへお越しください。
― 勉強会事務局
方式Bでも、同じ回答で2通が組み立てられること、例外が起きたときに管理者あての「【要確認】自動返信が止まりました」が1通だけ作られることを確かめました。トリガー作成の関数は、2回続けて呼んでも1つしか作られませんでした。
FormAppで質問項目を表から一括作成する
質問が多いフォームや、毎回似たフォームを作る場合は、スプレッドシートの表からまとめて作れます。「質問定義」という名前のシートに、次の表を用意します。
| 種類 | 質問のタイトル | 必須 | 選択肢 | 補足の説明 |
|---|---|---|---|---|
| テキスト | メールアドレス | TRUE | 確認メールの送り先 | |
| テキスト | お名前 | TRUE | ||
| ラジオ | 参加形式 | TRUE | 会場、オンライン | |
| プルダウン | 参加する回 | TRUE | 第1回(10月20日)、第2回(11月17日) | |
| 段落 | 質問・連絡事項 | FALSE |
シートの A1 に貼り付けられるタブ区切り版です。
種類 質問のタイトル 必須 選択肢 補足の説明
テキスト メールアドレス TRUE 確認メールの送り先
テキスト お名前 TRUE
ラジオ 参加形式 TRUE 会場、オンライン
プルダウン 参加する回 TRUE 第1回(10月20日)、第2回(11月17日)
段落 質問・連絡事項 FALSE
/**
* 「質問定義」シートの表からフォームを一括作成する
* A列:種類(テキスト/段落/ラジオ/プルダウン)
* B列:質問のタイトル C列:必須(TRUE/FALSE)
* D列:選択肢(読点「、」区切り) E列:補足の説明
*/
function createFormFromSheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const rows = ss.getSheetByName('質問定義').getDataRange().getValues();
rows.shift(); // 1行目(見出し)を除く
const form = FormApp.create('社内勉強会 参加申込フォーム');
form.setDescription('参加する回と形式を選んで送信してください。');
form.setConfirmationMessage('お申し込みを受け付けました。確認のメールをお送りします。');
rows.forEach(function (row, index) {
const type = row[0];
const title = row[1];
const required = row[2] === true;
const choices = String(row[3]).split('、').filter(function (c) { return c !== ''; });
const help = row[4];
let item;
if (type === 'テキスト') {
item = form.addTextItem();
} else if (type === '段落') {
item = form.addParagraphTextItem();
} else if (type === 'ラジオ') {
item = form.addMultipleChoiceItem().setChoiceValues(choices);
} else if (type === 'プルダウン') {
item = form.addListItem().setChoiceValues(choices);
} else {
throw new Error((index + 2) + '行目:種類「' + type + '」には対応していません');
}
item.setTitle(title).setRequired(required);
if (help) {
item.setHelpText(help);
}
});
// 回答の保存先を、このスプレッドシートにする
form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId());
console.log('回答用URL:' + form.getPublishedUrl());
console.log('編集用URL:' + form.getEditUrl());
}
スタブで呼び出したところ、表の5行から、テキスト2問・ラジオ1問・プルダウン1問・段落1問が表の順に作られ、必須と選択肢も表どおりに設定されました。
このコードで使っている FormApp まわりのメソッドです。
| メソッド | 戻り値 | 役割 |
|---|---|---|
FormApp.create(title) |
Form |
新しいフォームを作る。create(title, isPublished) で公開の状態を指定することもできる |
FormApp.getActiveForm() |
Form |
スクリプトが紐づいているフォームを返す |
FormApp.openById(id) |
Form |
ID を指定して既存のフォームを開く |
form.addTextItem() |
TextItem |
1行のテキストを入力する質問を足す |
form.addParagraphTextItem() |
ParagraphTextItem |
複数行のテキストを入力する質問を足す |
form.addMultipleChoiceItem() |
MultipleChoiceItem |
ラジオボタンで1つ選ぶ質問を足す |
form.addListItem() |
ListItem |
プルダウンで1つ選ぶ質問を足す |
item.setTitle(title) |
同じ種類の項目 | 質問のタイトルを決める |
item.setRequired(enabled) |
同じ種類の項目 | 必須かどうかを決める |
item.setHelpText(text) |
同じ種類の項目 | 補足の説明を付ける |
item.setChoiceValues(values) |
同じ種類の項目 | 選択肢を文字列の配列で決める(ラジオボタン、プルダウン) |
form.setDescription(description) |
Form |
フォームの説明文を決める |
form.setConfirmationMessage(message) |
Form |
送信後に表示するメッセージを決める |
form.setCollectEmail(collect) |
Form |
回答者のメールアドレスを収集するかどうかを決める |
form.setDestination(type, id) |
Form |
回答の保存先を決める。FormApp.DestinationType.SPREADSHEET とスプレッドシートの ID を渡す |
form.getPublishedUrl() |
String |
回答用のURLを返す |
form.getEditUrl() |
String |
編集用のURLを返す |
回答(FormResponse)と1問ずつの回答(ItemResponse)で使えるメソッドです。
| メソッド | 戻り値 | 役割 |
|---|---|---|
response.getItemResponses() |
ItemResponse[] |
すべての回答を、フォームでの並び順で返す |
response.getRespondentEmail() |
String |
回答者のメールアドレス(メールアドレスの収集が有効なフォーム) |
response.getTimestamp() |
Date |
送信日時 |
response.getId() |
String |
回答の ID |
response.getEditResponseUrl() |
String |
送信済みの回答を編集するためのURL |
response.getResponseForItem(item) |
ItemResponse |
指定した質問への回答 |
itemResponse.getItem() |
Item |
回答した質問。getTitle() でタイトル、getType() で種類が分かる |
itemResponse.getResponse() |
Object |
回答。多くは文字列、チェックボックスは文字列の配列、グリッドは行ごとの配列 |
MailAppとGmailAppの違い
自動返信のように「送るだけ」の用途には MailApp が合います。
| 比べる点 | MailApp | GmailApp |
|---|---|---|
| できること | メールの送信 | 送信に加えて、受信トレイのスレッドの取得・検索、ラベルの操作、下書きの作成など |
| 受信トレイへのアクセス | しない | する |
| 必要な権限(スコープ) | https://www.googleapis.com/auth/script.send_mail |
https://mail.google.com/ |
| 送信のメソッド | sendEmail(recipient, subject, body, options) ほか |
sendEmail(recipient, subject, body, options) |
| 残りの送信数の取得 | getRemainingDailyQuota() |
― |
| 主なオプション | name、replyTo、cc、bcc、htmlBody、attachments、inlineImages、noReply |
from、name、replyTo、cc、bcc、htmlBody、attachments、noReply |
| 選ぶ場面 | 自動返信・通知 | エイリアスのアドレスを差出人にしたい、送ったあとにラベルを付けたい |
公式リファレンスは MailApp について、Gmail の受信トレイにアクセスできない送信専用のサービスで、GmailApp を使うスクリプトより再承認を求められる頻度が低い、と説明しています。noReply は Google Workspace のアカウントで使えるオプションです。
1日の送信数の上限と必要な権限
上限(割り当て)
Apps Script の割り当てのページに載っている値です。
| 項目 | 無料の Google アカウント | Google Workspace |
|---|---|---|
| 1日にメールを送れる宛先の数 | 100 / 日 | 1,500 / 日 |
| 1通あたりの宛先の数 | 50 / 通 | 50 / 通 |
| メール本文の大きさ | 200 KB / 通 | 400 KB / 通 |
| 添付ファイルの数 | 250 / 通 | 250 / 通 |
| 添付ファイルの合計サイズ | 25 MB / 通 | 25 MB / 通 |
| トリガーの数 | 20 / ユーザー / スクリプト | 20 / ユーザー / スクリプト |
| トリガーの合計実行時間 | 90 分 / 日 | 6 時間 / 日 |
| 1回の実行時間 | 6 分 / 実行 | 6 分 / 実行 |
| ユーザーあたりの同時実行数 | 30 | 30 |
- 割り当てはユーザーごとで、最初のリクエストから24時間後にリセットされると書かれています。
- 数えるのは「宛先」の数です。 この記事のコードは1回の送信で回答者と管理者の2件を使うので、無料のアカウントでは1日50回答(100 ÷ 2)、Google Workspace では750回答(1,500 ÷ 2)が目安になります。ほかのスクリプトでもメールを送っていれば、その分も同じ枠から減ります。
MailApp.getRemainingDailyQuota()は、その日の残りで送れる宛先の数を返します。 コードでは2件に満たないときは送らず、回答の行に「本日の送信数の上限のため保留」と残しています。
権限と実行するアカウント
| 項目 | 内容 |
|---|---|
| 誰の権限で動くか | インストール型トリガーは、常に作成した人のアカウントで動く。メールもそのアカウントから送られる |
| 承認 | トリガーは作成したユーザーの承認が必要。コードを変えて新しい権限が要るようになったら、スクリプトエディタで手動実行して承認する |
| 権限の割り出し | Apps Script がコードを調べ、使っているサービスから必要な権限を割り出す |
| 権限を明示する | マニフェスト(appsscript.json)の oauthScopes に書く |
| ほかの人から見えるか | あるアカウントが作ったトリガーは、ほかのアカウントからは見えない |
| トリガーが動かない場面 | スクリプトの実行や API リクエストでは、原則としてトリガーは動かない |
| トリガーを扱うメソッドの権限 | https://www.googleapis.com/auth/script.scriptapp |
つまずきやすい点と対処
| 症状 | 原因 | 対処 |
|---|---|---|
e.namedValues['お名前'] をそのまま本文に入れると期待と違う |
値は配列で渡される | [0] を付けて取り出す |
値が undefined になる |
質問のタイトルとコードのキーが一字でも違う | フォームの質問文をコピーしてコードに貼る。空白の全角・半角もそろえる |
e.response が undefined |
スプレッドシートに付けたトリガーで動いている | e.namedValues を使う。または、フォームのスクリプトにトリガーを作り直す |
e.namedValues が undefined |
フォームに付けたトリガーで動いている | e.response.getItemResponses() を使う |
| エディタの実行ボタンで動かすと e が空で止まる | フォームの送信で呼ばれたときだけ e が渡される | フォームからテスト回答を送って確かめる |
| 1回の回答で2通届く | 同じ関数のトリガーが2つある | 「トリガー」の画面で1つにする。コードで作るときは getProjectTriggers() で有無を確かめる |
| 途中から送られなくなった | 1日の宛先数の上限に達した | getRemainingDailyQuota() で残りを確かめ、保留にした回答を翌日に送る |
| コードを変えたら動かなくなった | 新しい権限の承認が済んでいない | スクリプトエディタで関数を手動実行して承認する |
確かめて分かったこと
- フォームに付けたトリガーの e には、namedValues も values も range もありません。 公式のイベントオブジェクトの表では、フォーム側は
response・source・triggerUid、シート側はauthMode・namedValues・range・triggerUid・valuesと、項目がきれいに分かれています。 getResponse()の戻り値は文字列とは限りません。 チェックボックスは配列、グリッドは行ごとの配列です。本文に入れる前に配列かどうかを見て、文字列にそろえる処理を入れました。- 上限は「通数」ではなく「宛先の数」で数えます。 回答者と管理者に1通ずつ送ると、1回答で2件です。スタブで残りを1件にすると、コードは2通とも送らずに保留の記録を残しました。
- トリガーは作った人のアカウントで動きます。 回答者が誰であっても、メールの差出人はトリガーを作ったアカウントです。共有のフォームでは、誰のアカウントで作るかを先に決めておくと安心です。
forSpreadsheetもforFormも、公式の例ではオブジェクトを渡しています。SpreadsheetApp.getActive()やFormApp.getActiveForm()の戻り値をそのまま渡す形にそろえました。- MailApp と GmailApp では求められる権限の広さが違います。 送信だけなら MailApp の
script.send_mailで足ります。
よくある質問
Q. GASなしでGoogleフォームの自動返信はできる? A. 回答の控えを返すだけならできます。フォームの設定でメールアドレスを収集し、「回答のコピーを回答者に送信」を有効にします。文面を自分で決めたい場合は GAS を使います。
Q. e.namedValuesとe.valuesの違いは?
A. e.namedValues は質問のタイトルで取り出すオブジェクト、e.values は並び順で取り出す配列です。質問の順番を入れ替えても影響を受けにくいのは e.namedValues です。
Q. onFormSubmitが動かないときは? A. まず「トリガー」の画面に、その関数のフォーム送信時トリガーがあるかを確かめます。あれば「実行数」でエラーの内容を見て、権限の承認とイベントオブジェクトの種類(フォーム側かシート側か)を見直します。
Q. 自動返信メールの差出人は変えられる?
A. 表示名は name オプションで変えられます。差出人のアドレスはトリガーを作ったアカウントになり、返信先だけを変えたいときは replyTo を指定します。
Q. 同時に複数の回答が来ても大丈夫?
A. はい、e にはその送信の回答が入って渡されるので、回答どうしが混ざりません。シートに結果を書くときも、e.range.getRow() の行に書けば自分の行だけを更新できます。
まとめ
- 控えを返すだけならフォームの設定、文面を変える・通知する・記録するなら GAS のフォーム送信時トリガー。
- トリガーを付ける場所で e が変わる。シート側は
e.namedValues・e.values・e.range、フォーム側はe.response・e.source。 - 回答は「タイトル → 回答」のオブジェクトにそろえてから文面を作ると、方式を変えても同じ関数が使える。
- try-catch で囲み、結果を回答の行(または実行ログと管理者メール)に残す。
- 送る前に
getRemainingDailyQuota()。上限は宛先の数で、無料のアカウントは100、Google Workspace は1,500。 - 送るだけなら MailApp。必要な権限は
script.send_mailで、受信トレイにはアクセスしない。 - 質問の多いフォームは、FormApp で表から一括作成できる。
参考・出典
- Google Apps Script「Event Objects」 https://developers.google.com/apps-script/guides/triggers/events
- Google Apps Script「Installable Triggers」 https://developers.google.com/apps-script/guides/triggers/installable
- Google Apps Script「Quotas for Google Services」 https://developers.google.com/apps-script/guides/services/quotas
- Google Apps Script「Authorization for Google Services」 https://developers.google.com/apps-script/guides/services/authorization
- Google Apps Script「Logging」 https://developers.google.com/apps-script/guides/logging
- Google Apps Script リファレンス「MailApp」 https://developers.google.com/apps-script/reference/mail/mail-app
- Google Apps Script リファレンス「GmailApp」 https://developers.google.com/apps-script/reference/gmail/gmail-app
- Google Apps Script リファレンス「FormApp」「Form」「FormResponse」「ItemResponse」「Item」「TextItem」「ParagraphTextItem」「MultipleChoiceItem」「ListItem」 https://developers.google.com/apps-script/reference/forms/form-app
- Google Apps Script リファレンス「ScriptApp」「Trigger」「FormTriggerBuilder」「SpreadsheetTriggerBuilder」 https://developers.google.com/apps-script/reference/script/script-app
- Google Apps Script リファレンス「Spreadsheet」「Sheet」「Range」 https://developers.google.com/apps-script/reference/spreadsheet/range
- Google ドキュメント エディタ ヘルプ「フォームの回答を表示、管理する」 https://support.google.com/docs/answer/139706?hl=ja
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「GASでGoogleフォーム自動返信メール|onFormSubmit早見表」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。