ClaudeでGASのフォーム送信トリガーを作る — 通知・シート転記・二重発火対策
Googleフォームの送信をGASで受け、メール通知と別シートへの転記を行う処理をClaudeに書かせる手順です。イベントオブジェクトの渡し方と、二重発火を防ぐロックの入れ方を扱います。
GoogleフォームにGAS(Google Apps Script)の送信トリガーを付ければ、回答が届いた瞬間にメール通知と別シートへの転記を自動化できます。Claudeに書かせるときの要点は2つです。トリガーの種類(フォーム用かスプレッドシート用か)を先に決めて伝えること。そして、二重に動いても結果が変わらない書き方を最初から求めることです。
GASをClaude Codeで進める土台となるclaspの準備はClaude CodeでGAS開発を進める手順にまとめています。この記事は、その上で「フォーム送信」という1つのイベントに絞って、依頼文・コード・事故の切り分けまで通します。
フォーム用とスプレッドシート用、どちらの送信トリガーか
フォーム送信トリガーには2種類あります。Google Formsそのものに付けるものと、回答の保存先スプレッドシートに付けるものです。どちらも「回答が送られたとき」に動きますが、関数に渡されるイベントオブジェクトの中身が違います。
2種類の送信トリガーで渡される値
スプレッドシート側
namedValues(質問名ごとの値)、values(シートの列順の配列)、range(回答が入った範囲)などが渡されます。シートへ転記したいときに向きます。
フォーム側
response(FormResponseオブジェクト)とsource(Formオブジェクト)が渡されます。回答者のメールアドレスや編集用URLをフォームのAPIから取りたいときに向きます。
イベントオブジェクトのリファレンスには、どちらの送信トリガーにも「対応するビルダーで使うこと」という注意書きがあります。スプレッドシート側はSpreadsheetTriggerBuilder、フォーム側はFormTriggerBuilderです。取り違えると、期待したフィールドが渡りません。
フォーム側のFormResponseには、回答者のメールアドレスを返すgetRespondentEmail()や、回答の編集用URLを返すgetEditResponseUrl()、回答時刻のgetTimestamp()があります。通知に編集用URLを添えたいなら、フォーム側のトリガーが候補です。
今回の「通知とシート転記」はスプレッドシート側で組みます。e.rangeからシートを辿れて、e.valuesで列順の値がそのまま取れるためです。
Claudeに渡す材料はイベントオブジェクトの形
Claudeが最もつまずくのは、eの中身を推測で書くことです。GASは手元で実行してeを覗けないので、リファレンスにある形をそのまま貼って渡します。スプレッドシート側の送信トリガーでは、次のフィールドが渡されます。
| フィールド | 中身 |
|---|---|
namedValues | 中身質問名をキー、回答を配列で持つオブジェクト |
values | 中身シートの列順に並んだ値の配列 |
range | 中身回答が書かれた範囲のRange |
authMode | 中身ScriptApp.AuthModeの値(送信トリガーではFULL) |
triggerUid | 中身このイベントを起こしたトリガーのID |
リファレンスの例では、namedValuesは{'First Name': ['Jane'], 'Last Name': ['Doe']}のように、値が配列で入ります。1つの質問に複数の回答(チェックボックス等)が付く場合があるためです。[0]だけ読むコードは、複数選択の質問で値を落とします。
質問名の表記はフォームの質問文そのものです。日本語のフォームなら日本語がキーになります。コードに質問名を埋め込む前に、実際のキーを一度ログに出して確かめる工程を依頼文に含めると、綴りの食い違いを避けられます。
依頼文とCLAUDE.mdに書くこと
次の依頼文は、イベントオブジェクトの形、転記先、通知先、冪等性(同じ回答を2回処理しても結果が変わらないこと)を最初に全部渡す例です。
フォーム回答用スプレッドシートに紐づくGASを書いてください。
- スプレッドシート側のフォーム送信トリガー(onFormSubmit)で動く
- 渡されるeは namedValues / values / range を持つ(値は配列)
- 回答を「管理」シートの末尾に1行追加し、全項目をメール本文にして通知
- 同じ回答行を2回処理しないこと(行を一意キーにして判定)
- 処理全体をLockServiceのスクリプトロックで直列化する
- トリガー登録は関数にし、同じ関数のトリガーが既にあれば作らない
- eを使う部分と処理本体を分け、本体は模擬データで呼べるようにする毎回同じ前提を打ちたくないときは、プロジェクトのCLAUDE.mdに規約として置きます。
## GASのトリガー規約
- イベントオブジェクト由来の処理は、eを展開した引数だけを受ける関数に分ける
- 送信トリガーの処理は、行番号などの一意キーで重複判定する
- 書き込みを伴う処理は LockService.getScriptLock() で囲む
- トリガー登録関数は、登録前に getProjectTriggers() で既存を確認する最後の2行が、二重発火の2大原因(重複登録と並行実行)に対応しています。理由はあとの節で説明します。
生成コードの完成形
次のコードは、公式リファレンスのAPIに沿って組んだ例です。Claudeの実出力ではなく、依頼文に対して期待する形の一例として読んでください。
const NOTIFY_TO = 'team@example.com';
const LOG_SHEET = '管理';
function handleFormSubmit(e) {
const sheet = e.range.getSheet();
const key = sheet.getName() + '!' + e.range.getRow();
processResponse_(sheet.getParent(), key, e.values, e.namedValues);
}
function processResponse_(ss, key, values, named) {
const lock = LockService.getScriptLock();
lock.waitLock(30000); // 取れなければ例外で止まる
try {
const log = ss.getSheetByName(LOG_SHEET);
const last = log.getLastRow();
const done = last > 0
? log.getRange(1, 1, last, 1).getValues().flat()
: [];
if (done.includes(key)) return 'skipped';
log.appendRow([key, new Date()].concat(values));
SpreadsheetApp.flush(); // ロック解放の前に書き込みを確定
notify_(named);
return 'done';
} finally {
lock.releaseLock();
}
}
function notify_(named) {
const body = Object.keys(named)
.map(k => k + ': ' + named[k].join(', '))
.join('\n');
MailApp.sendEmail(NOTIFY_TO, '[フォーム] 新しい回答', body);
}ポイントは3つあります。
- 重複判定のキーは、回答シート名と行番号です。回答は1行ずつ追加されるので、同じ回答が再処理されても同じキーになります
waitLock(30000)は、30秒待っても取れないと例外を投げます。tryLockは例外ではなく真偽値を返すので、黙って落とす用途には向きませんSpreadsheetApp.flush()をreleaseLock()の前に置いています。リファレンスが、スプレッドシートを扱うときはロック解放前に保留中の変更を確定させるよう求めているためです
通知を先に送るか、記録を先に書くかは設計の分かれ目です。上の例は記録を先に書くので、メール送信が例外で落ちた場合、次回の再実行では「処理済み」と判定され通知は再送されません。通知の取りこぼしを避けたいなら、順番を逆にして送信成功後に記録します。その代わり、記録に失敗すると通知が重複します。どちらを許容するかは、Claudeに依頼文で明示しておく項目です。
模擬データで本体だけ確かめる
eはトリガーが発火するときに渡されるので、エディタから関数を直接実行しても中身は入りません。そこでeの展開部分と処理本体を分けておき、本体を模擬データで呼ぶテスト関数を作ります。
function testProcessResponse() {
const ss = SpreadsheetApp.getActive();
const values = ['2026/10/28 10:00:00', '山田', '問い合わせ'];
const named = {
'タイムスタンプ': ['2026/10/28 10:00:00'],
'お名前': ['山田'],
'種別': ['問い合わせ'],
};
Logger.log(processResponse_(ss, 'テスト!2', values, named)); // done
Logger.log(processResponse_(ss, 'テスト!2', values, named)); // skipped
}2回続けて呼び、1回目がdone、2回目がskippedになれば、重複判定が効いています。実行ログはスクリプトエディタで確認します。clasp pushでローカルの変更を反映したあと、エディタでこの関数を選んで実行する流れです。
トリガーは1本だけ登録する
二重発火で最も多いのは、同じ関数を指すトリガーが2本ある状態です。エディタの「トリガー」画面から手で追加し、さらにコードのScriptApp.newTriggerでも作ると、送信のたびに関数が2回呼ばれます。
登録関数は、既存を確かめてから作ります。
function installTrigger() {
const exists = ScriptApp.getProjectTriggers().some(t =>
t.getHandlerFunction() === 'handleFormSubmit');
if (exists) return;
ScriptApp.newTrigger('handleFormSubmit')
.forSpreadsheet(SpreadsheetApp.getActive())
.onFormSubmit()
.create();
}すでに重複しているときは、getProjectTriggers()で取れるトリガーのうち、getHandlerFunction()が同じものをScriptApp.deleteTriggerで1本残して消します。トリガーのIDはgetUniqueId()で区別できます。
もう1つの重複源は、フォーム側とスプレッドシート側に別々のトリガーを張ることです。リファレンスは、送信トリガーに「フォーム用」と「スプレッドシート用」の2系統があると説明しています。両方に同じ処理を登録すれば、1回の送信で2回動きます。どちらか一方に統一します。
二重発火を切り分ける
重複が起きたときは、次の順で確かめます。
二重に動くときの切り分け
- Qトリガー画面に、同じ関数を指す行が2本以上ありますか?答えがいいえのとき答えがはい
1本を残して削除します。手動登録とコード登録の併用が典型です。
- Qフォーム側とスプレッドシート側の両方にトリガーがありますか?答えがいいえのとき答えがはい
片方を削除します。通知と転記はスプレッドシート側に寄せます。
- Qトリガーは1本なのに、同じ行の処理が2回記録されますか?答えがいいえのとき答えがはい
並行実行を疑います。ロックと行番号キーの重複判定を入れます。
それでも重複するとき
実行ログでtriggerUidと行番号を比べ、同じトリガー・同じ行で2回動いているのか、別のトリガーが動いているのかを見分けます。
イベントオブジェクトのtriggerUidは、そのイベントを起こしたトリガーのIDです。ログに出しておけば、2回の実行が同じトリガーから来たのか、別々の2本から来たのかを区別できます。
動かない・想定外に動くケース
重複と逆に、動くはずなのに動かないケースもあります。インストール型トリガーの制約から、押さえておく点は次のとおりです。
- トリガーは、トリガーを作ったアカウントの権限で動きます。通知メールの送信元も、作成者のアカウントです
- ファイルを閲覧のみ・コメントのみのモードで開いている場合は動きません
- スクリプトやAPIリクエストによる送信ではトリガーが発火しません。たとえば
FormResponse.submit()で回答を作っても、フォームの送信トリガーは動きません。テストデータを作る目的でスクリプトから回答を送っても、検証にはならない点に注意してください
最後の点は、Claudeに「テスト用に回答を自動投入するスクリプトも書いて」と頼むときにも関わります。投入したデータでトリガーを試すことはできず、実際にフォームの画面から1件送って確かめる必要があります。
通知を増やす前に見る上限
メール通知を使うので、クォータも見ておきます。メール受信者数は、一般のGoogleアカウントで1日100人、Google Workspaceで1日1,500人が上限です。回答数が多いフォームで1件ごとに通知する設計は、上限に当たりやすくなります。
トリガーの合計実行時間にも上限があります。一般アカウントで1日90分、Workspaceで6時間です。トリガーの数は1ユーザー・1スクリプトあたり20本までです。1回の実行は6分で打ち切られます。長い処理が必要になった場合の対処はClaudeでGASの6分制限を分割実行に直す依頼の出し方にあります。
通知の中身をClaudeに要約させたいときは、GASからAPIを呼ぶ手順をGASからClaude APIをUrlFetchAppで呼ぶ記事で扱っています。ただし、その分だけ1回の実行時間と外部呼び出しが増えるため、ロックを保持する時間も延びます。ロックの外で要約し、ロック内では記録だけにする分け方が現実的です。
まとめ
送信トリガーの依頼では、eの形・冪等性・登録の一意性の3点を最初に文章へ入れておくと、後からの手戻りが減ります。通知先と順序(通知が先か記録が先か)は、取りこぼしと重複のどちらを許容するかの選択なので、コードを読んで自分で決めるところです。動作確認は模擬データで本体を、実際の1件送信でトリガー配線を、それぞれ別に確かめます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。