GASのスクリプトは、作っただけでは誰かがボタンを押すまで動きません。決まった時刻に動かしたり、フォームが送信された瞬間に動かしたりするのがトリガーです。この記事では、トリガーの種類と、名前だけで動くシンプルトリガーと画面やコードで設定するインストール型トリガーの違い、設定の手順、時間主導型の時刻の指定の幅、トリガーが誰の権限で動くか、止まったときの見方を順に書きます。例は全記事で共通の、スタッフ6名ほどの予約制の小さな料理教室「サンプル料理教室」で、毎朝8時に今日の体験予約をGoogle Chatのスペースに送る処理と、フォームが送信されたら予約一覧の台帳へ転記する処理を作ります。前提として、スクリプトの作り方と初回の承認は「Google Apps Script(GAS)とは:できること・始め方」の記事の手順で一度通してあるものとします。
この記事の内容
先に決めること
トリガーは一度設定すると、誰も画面を見ていないところで動き続けます。設定する前に次の3つを決めておきます。
1つ目は、トリガーを誰のアカウントで作るかです。インストール型トリガーは、常にトリガーを作った人のアカウントの権限で動きます。受付の担当者がトリガーを作ると、確認メールは受付の個人のアドレスから送られ、受付が退職してアカウントが消えると、トリガーも動かなくなります。実際に運用していると、退職者のアカウントを削除した翌日から確認メールが止まっていた、ということが起きます。サンプル料理教室では、トリガーはすべて代表のアカウントで作ると決めています。
2つ目は、別の人が作ったトリガーは見えない、ということを前提にすることです。あるアカウントは、別のアカウントが作ったトリガーを見ることができません。同じスクリプトに、代表と受付がそれぞれトリガーを作ると、お互いに相手のトリガーが見えないまま二重に動きます。誰が作るかを1人に決めておけば、この事故は起きません。
3つ目は、何時に動けばよいかを幅で考えることです。時間主導型のトリガーは、ちょうど8時0分には動きません。8時台のどこかで動く、という指定になります。受付が8時半に出勤して見るなら8時台で足りますが、8時ちょうどに必ず届いていてほしいなら7時台に設定します。
トリガーの種類
トリガーには、シンプルトリガーとインストール型トリガーの2つの系統があります。
| 系統 | 種類 | いつ動くか | 対象 |
|---|---|---|---|
| シンプル | onOpen | 編集権限のある人がファイルを開いたとき | スプレッドシート・ドキュメント・スライド・フォーム |
| シンプル | onEdit | 人がスプレッドシートの値を変えたとき | スプレッドシート |
| シンプル | onSelectionChange | 人がスプレッドシートの選択を変えたとき | スプレッドシート |
| シンプル | doGet・doPost | Webアプリが開かれたとき、HTTPで送られたとき | Webアプリ |
| インストール型 | 時間主導型 | 決めた日時、または一定の間隔 | すべて |
| インストール型 | 起動時 | 編集権限のある人がファイルを開いたとき | スプレッドシート・ドキュメント・フォーム |
| インストール型 | 編集時 | スプレッドシートの値が変わったとき | スプレッドシート |
| インストール型 | 変更時 | シートの追加や列の削除など、構造が変わったとき | スプレッドシート |
| インストール型 | フォーム送信時 | フォームに回答が送信されたとき | フォーム、または回答先のスプレッドシート |
| インストール型 | カレンダー更新時 | カレンダーの予定が作成・編集・削除されたとき | カレンダー |
シンプルトリガーとインストール型トリガーの違い
シンプルトリガーは、関数の名前を onOpen や onEdit にするだけで動きます。設定の画面は要りません。そのかわり、できることに制限があります。
| シンプルトリガー | インストール型トリガー | |
|---|---|---|
| 作り方 | 関数名を onEdit などにする | エディタの「トリガー」画面か、ScriptApp.newTrigger で作る |
| 誰の権限で動くか | 操作した人 | トリガーを作った人 |
| 承認が要るサービス(Gmailの送信、他のファイルを開く、UrlFetchなど) | 使えない | 使える |
| 実行時間 | 30秒まで | 通常の上限(6分) |
| 閲覧・コメントだけの人が開いたとき | 動かない | 動かない |
| スクリプトやAPIからの書き込み | 動かない | 動かない |
| 時間主導型・フォーム送信時・カレンダー | 無い | ある |
| 失敗したときの通知 | 公式に記載なし(実行数で見に行く前提) | メールで来る |
| 止め方 | 関数を消すか名前を変える | トリガーを削除する |
業務で困るのは、シンプルトリガーの onEdit からメールが送れないことです。「対応状況を確定にしたら会員にメールを送る」を onEdit で書いても、メールの送信には承認が要るので動きません。この場合は、関数名を onEdit 以外(たとえば handleEdit)にして、インストール型の「編集時」のトリガーをつけます。
もう1つ、スクリプトやAPIからの書き込みでは、シンプルトリガーもインストール型の編集時トリガーも動きません。GASの setValue や appendRow でセルを書き換えても onEdit は呼ばれません。フォームの回答が回答先のシートに入るのは「編集」ではなく「フォーム送信」なので、編集時のトリガーではなくフォーム送信時のトリガーで受けます。AppSheetやn8nからスプレッドシートに書き込んだ場合も、編集時のトリガーは動かない前提で設計します。
サンプル料理教室で作るもの
| 処理 | 関数名 | トリガー | 置き場所 |
|---|---|---|---|
| 毎朝、今日の体験予約をChatのスペース「受付」に送る | sendTodayTrials | 時間主導型・毎日・午前7時〜8時 | 体験予約のスプレッドシート |
| フォームの回答を「予約一覧」シートへ転記し、対応状況を「未対応」にする | copyToLedger | フォーム送信時(スプレッドシートから) | 同上 |
| 対応状況を「確定」にしたら、確定した日時を記録する | onEdit | シンプルトリガー | 同上 |
体験予約のスプレッドシートには、フォームの回答が入る「フォームの回答 1」シートと、受付が対応状況を付ける「予約一覧」シートがあります。フォームの回答のシートに直接列を足して書き込むと、フォームの質問を変えたときに列がずれます。回答はそのまま置いておき、作業用の「予約一覧」へ転記する形にしています。
手順1:毎朝8時にChatへ送る
ChatのWebhookのURLを取る
- パソコンのブラウザでGoogle Chatを開きます。WebhookはChatのモバイルアプリからは設定できません。
- スペース「受付」を開き、スペース名の横の下向き矢印をクリックします。
- 「アプリとインテグレーション」をクリックします。
- 「Webhook を追加」をクリックします。
- 名前に「体験予約のお知らせ」と入れて保存します。
- 作られたWebhookの「その他」から「リンクをコピー」をクリックします。

コピーしたURLには、そのスペースに投稿できる鍵が入っています。URLを知っている人なら誰でもスペースに書き込めるので、コードに直接書かずに、スクリプトのプロパティにしまいます。組織によっては、管理者がWebhookを使えるようにしていないと、この画面に項目が出ません。
URLをスクリプトのプロパティに入れる
- 体験予約のスプレッドシートで「拡張機能」→「Apps Script」を開きます。
- 左側の「プロジェクトの設定」(歯車)をクリックします。
- 「スクリプト プロパティ」の「スクリプト プロパティを追加」をクリックします。
- プロパティに
CHAT_WEBHOOK_URL、値にコピーしたURLを貼ります。 - 「スクリプト プロパティを保存」をクリックします。
同じ画面の「タイムゾーン」が「(GMT+09:00) 日本標準時」になっていることも確かめます。ここがずれていると、トリガーの時刻もずれます。
コードを書く
左側の「エディタ」に戻り、次のコードを貼って保存します。
function sendTodayTrials() {
const url = PropertiesService.getScriptProperties().getProperty('CHAT_WEBHOOK_URL');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('予約一覧');
const values = sheet.getDataRange().getValues();
const header = values.shift();
const nameCol = header.indexOf('氏名');
const dateCol = header.indexOf('希望日');
const courseCol = header.indexOf('コース');
const statusCol = header.indexOf('対応状況');
const tz = 'Asia/Tokyo';
const today = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd');
const rows = values.filter(function (r) {
return r[dateCol] instanceof Date
&& Utilities.formatDate(r[dateCol], tz, 'yyyy-MM-dd') === today
&& r[statusCol] !== 'キャンセル';
});
rows.sort(function (a, b) { return a[dateCol] - b[dateCol]; });
const lines = rows.map(function (r) {
return '・' + Utilities.formatDate(r[dateCol], tz, 'HH:mm') + ' ' + r[nameCol] + ' 様(' + r[courseCol] + ')';
});
const text = rows.length > 0
? '今日の体験レッスンは ' + rows.length + ' 件です\n' + lines.join('\n')
: '今日の体験レッスンの予約はありません。';
UrlFetchApp.fetch(url, {
method: 'post',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
payload: JSON.stringify({ text: text })
});
console.log(text);
}
関数の選択欄で sendTodayTrials を選んで「実行」をクリックします。外部への送信の権限を求められるので承認します。Chatの「受付」スペースに今日の予約が出れば、処理はできています。
予約が0件の日も「ありません」と送るようにしているのは、届かなかったのか0件なのかを区別するためです。何も来ない日があると、受付はトリガーが止まったことに気づけません。
画面からトリガーを設定する
- エディタの左側の「トリガー」(時計の形)をクリックします。
- 右下の「トリガーを追加」をクリックします。
- 「実行する関数を選択」で
sendTodayTrialsを選びます。 - 「実行するデプロイを選択」は「Head」のままにします。
- 「イベントのソースを選択」で「時間主導型」を選びます。
- 「時間ベースのトリガーのタイプを選択」で「日付ベースのタイマー」を選びます。
- 「時刻を選択」で「午前7時~8時」を選びます。
- 「エラー通知設定」で「今すぐ通知を受け取る」を選びます。
- 「保存」をクリックします。

時刻は「午前7時~8時」のように1時間の幅で選びます。この幅の中のどこで動くかは決められず、日によって変わります。公式の説明でも、毎朝9時に設定したトリガーが9時から10時の間のどこかで動く場合がある、とされています。8時の出勤前に届けたいので、7時台にしています。
手順2:フォーム送信時に台帳へ転記する
コードを書く
同じエディタに、次の関数を足して保存します。フォームの質問名(「お名前」「メールアドレス」「希望日時」「コース」「知ったきっかけ」)は、自分のフォームの質問の文言に合わせて書き換えます。
function copyToLedger(e) {
const lock = LockService.getScriptLock();
lock.waitLock(30000);
try {
const src = e.range.getSheet();
const header = src.getRange(1, 1, 1, src.getLastColumn()).getValues()[0];
const row = e.range.getValues()[0];
const get = function (name) { return row[header.indexOf(name)]; };
const ledger = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('予約一覧');
ledger.appendRow([
get('タイムスタンプ'),
get('お名前'),
get('メールアドレス'),
get('希望日時'),
get('コース'),
get('知ったきっかけ'),
'未対応',
''
]);
} finally {
lock.releaseLock();
}
}
フォーム送信時のイベントには、質問名と値の組(e.namedValues)、シートと同じ順の値(e.values)、書き込まれた範囲(e.range)が入ってきます。e.namedValues と e.values は値が文字になって入ってくるので、日付として扱いたい希望日時は、e.range からシートに書き込まれた値を読み直しています。LockService は、2件の申込がほぼ同時に来たときに、転記が重なって行がずれないようにするためのものです。
この関数はエディタの「実行」で動かすと、e が無いのでエラーになります。テストは、フォームから実際に1件送って行います。
コードでトリガーを作る
フォーム送信時のトリガーは画面からも作れますが(「イベントのソースを選択」で「スプレッドシートから」、「イベントの種類を選択」で「フォーム送信時」)、ここではコードで作る方法を書きます。複数のスプレッドシートに同じ仕組みを入れるときや、作り直すときに、同じ手順を確実に再現できます。
次の関数を足して保存し、関数の選択欄で setupTriggers を選んで1回だけ実行します。
function setupTriggers() {
const targets = ['sendTodayTrials', 'copyToLedger'];
ScriptApp.getProjectTriggers().forEach(function (t) {
if (targets.indexOf(t.getHandlerFunction()) !== -1) {
ScriptApp.deleteTrigger(t);
}
});
ScriptApp.newTrigger('sendTodayTrials')
.timeBased()
.atHour(7)
.everyDays(1)
.inTimezone('Asia/Tokyo')
.create();
ScriptApp.newTrigger('copyToLedger')
.forSpreadsheet(SpreadsheetApp.getActiveSpreadsheet())
.onFormSubmit()
.create();
}
最初の forEach で、同じ関数のトリガーが既にあれば消してから作っています。これを入れずに setupTriggers を2回実行すると、トリガーが2本ずつになり、毎朝のChatが2通、転記が2行ずつになります。トリガーの設定を変えたいときも、既存のトリガーを直接書き換えることはできず、消して作り直します。
実行したら、左側の「トリガー」を開き、2本だけ並んでいることを確かめます。

時間主導型の指定のしかた
コードで時間主導型を作るときの指定は次のとおりです。
| 書き方 | 意味 | 注意 |
|---|---|---|
.everyMinutes(5) | 5分ごと | 指定できるのは1・5・10・15・30のどれか |
.everyHours(1) | 1時間ごと | |
.everyDays(1).atHour(7) | 毎日7時台 | 分は指定しないとランダム |
.everyDays(1).atHour(7).nearMinute(30) | 毎日7時30分ごろ | 前後15分の幅がある |
.onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(9) | 毎週月曜の9時台 | |
.onMonthDay(1).atHour(9) | 毎月1日の9時台 | 月末は下のコードのように関数の中で判定する |
.at(new Date('2026-11-01T09:00:00+09:00')) | その日時に1回だけ | |
.after(10 * 60 * 1000) | 今から10分以上あとに1回だけ | 長い処理を分けて続きを動かすときに使う |
.inTimezone('Asia/Tokyo') | 時刻を日本時間で解釈する | 書かないとスクリプトのタイムゾーン |
月末に動かしたいときは、毎日動かして、関数の中で「明日が1日なら処理する」と判定します。
function monthEndJob() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
if (tomorrow.getDate() !== 1) return;
// ここに月末の処理を書く(請求書の作成など)
}
手順3:編集時に確定日時を記録する
予約一覧の対応状況(G列)を受付が「確定」にしたら、H列に確定した日時を自動で入れます。同じスプレッドシートの中だけで完結し、メールも送らないので、シンプルトリガーで足ります。
function onEdit(e) {
const sheet = e.range.getSheet();
if (sheet.getName() !== '予約一覧') return;
if (e.range.getColumn() !== 7 || e.range.getRow() === 1) return;
if (e.value === '確定' && sheet.getRange(e.range.getRow(), 8).getValue() === '') {
sheet.getRange(e.range.getRow(), 8).setValue(new Date());
}
}
関数名が onEdit なので、トリガーの設定は要りません。保存した時点から、受付が対応状況を変えるたびに動きます。

最初の2行で、別のシートや別の列の編集なら何もせずに終わるようにしています。onEdit はファイルのどこを編集しても呼ばれるので、この絞り込みが無いと、無関係な編集のたびに処理が走ります。e.value は1つのセルを編集したときだけ入ってきます。何行かまとめて貼り付けたときは空になるので、まとめて「確定」に貼り替えた行には日時が入りません。受付には1行ずつ選ぶよう伝えておきます。
確定したらメールも送りたくなったら、関数名を handleEdit に変え、インストール型の「編集時」トリガーを付けます。関数名を onEdit のままインストール型のトリガーも付けると、1回の編集でシンプルとインストール型の両方が動き、メールが2通送られます。
実行数とエラー通知の見方
実行数で動いたかを見る
- エディタの左側の「実行数」をクリックします。
- 関数名・種類(「時間主導型」「トリガー」「エディタ」など)・開始時刻・実行時間・ステータスが並びます。
- 失敗だけを見たいときは「フィルタを追加」→「ステータス」で「失敗」と「タイムアウト」にチェックを入れます。
- 行をクリックすると、その回の
console.logの中身とエラーの内容が出ます。

script.google.com を開いて左側の「マイ トリガー」を見ると、自分のアカウントで作ったトリガーが、全部のスクリプトをまたいで一覧になります。どのファイルにトリガーが仕掛けてあるか分からなくなったときは、ここから探します。
エラー通知のメール
インストール型トリガーで関数が失敗しても、画面には何も出ません。かわりに、トリガーを作った人のGmailに、失敗のまとめのメールが届きます。メールのリンクから、トリガーの無効化やスクリプトの修正の画面に飛べます。届く頻度は、トリガーの「エラー通知設定」で選びます。毎朝の通知のように1日1回動くものは「今すぐ通知を受け取る」、5分ごとに動くものは毎日のまとめにしておくと、通知のメールで受信トレイが埋まりません。
失敗のメールの仕組みが公式に書かれているのは、インストール型のトリガーだけです。シンプルトリガーの onEdit や onOpen は、失敗したら「実行数」を見に行かないと分からない前提で扱います。止まっても気づきにくいので、大事な処理はシンプルトリガーに乗せないでおきます。

トリガーを作った人が、このメールを読まない設定にしていたり、そもそも退職していたりすると、誰も失敗に気づきません。サンプル料理教室では、トリガーを作る代表のGmailで、Apps Scriptからの通知メールを受付と共有するグループへ転送するフィルタを作っています。
応用
トリガーの合計時間を意識する
Google Workspaceのアカウントでは、トリガーで動いた時間の合計が1日6時間まで(無料のGoogleアカウントは90分まで)です。1人・1スクリプトあたりのトリガーは20本までです(いずれも2026年10月時点)。5分ごとのトリガーは1日288回動くので、1回に1分かかる処理なら、それだけで1日4時間以上を使います。数分ごとに見張りたい処理は、間隔を延ばせないか、そもそも外のサービスからWebhookで知らせてもらえないかを先に考えます。
退職や担当替えの前にやること
トリガーの持ち主が辞めるときは、アカウントを消す前に次の順で移します。
- 持ち主のアカウントで
script.google.comの「マイ トリガー」を開き、トリガーの一覧を控えます。 - 引き継ぐ人のアカウントでスクリプトを開き、
setupTriggersを実行してトリガーを作り直します。 - 「実行数」で、新しいトリガーが引き継ぐ人の名前で動いたことを確かめます。
- 元の持ち主のトリガーを削除します。
setupTriggers のように、トリガーをコードで作る関数を用意しておくと、この引き継ぎが1回の実行で済みます。
できないことと、その代わり
| やりたいこと | トリガーでは | 代わりの手 |
|---|---|---|
| 毎朝8時0分ちょうどに動かす | 1時間の幅か、前後15分の幅になる | 前の時間帯に設定する。厳密な時刻が要る処理は外のスケジューラーやn8nで |
| 1分より細かい間隔 | 最短1分 | 外のサービスから呼ぶ |
| GASやAppSheetからの書き込みで編集時トリガーを動かす | 動かない | 書き込んだ側の処理の中で続きの処理を呼ぶ |
| 他の人が作ったトリガーを見る・止める | 見えない | 作る人を1人に決める。作った本人に「マイ トリガー」から止めてもらう |
| シンプルトリガーでメールを送る | 送れない | インストール型の編集時トリガーにする |
| 失敗したら自動でやり直す | やり直さない | 処理済みの印を持たせて、次の回で続きから処理する |
| Gmailにメールが届いたら即座に動かす | Gmailの受信のトリガーは無い | 時間主導型で数分おきに検索するか、n8nのGmailのトリガーを使う |
よく詰まるところ
| 症状 | 原因 | 直し方 |
|---|---|---|
| 毎朝のChatが2通来る | 同じ関数のトリガーが2本ある。setupTriggers を2回実行した、または別の人もトリガーを作った | 「トリガー」画面で1本だけ残す。作った人ごとに確かめる |
| 確定のメールが2通送られる | 関数名が onEdit のまま、インストール型の編集時トリガーも付けている | 関数名を handleEdit などに変える |
onEdit でメールを送ろうとすると動かない | シンプルトリガーは承認の要るサービスを使えない | インストール型の編集時トリガーにする |
| GASで書き込んだ行で編集時トリガーが動かない | スクリプトやAPIによる変更では動かない | 書き込む側の関数から続きの処理を直接呼ぶ |
| フォームの回答が入っても編集時トリガーが動かない | 回答の書き込みは編集ではない | フォーム送信時のトリガーで受ける |
copyToLedger をエディタで実行すると Cannot read properties of undefined | エディタからの実行ではイベント e が渡されない | フォームから実際に送信してテストする |
| 8時に設定したのに8時40分に届く | 時間主導型は1時間の幅の中のどこかで動く | 7時台に設定する |
| 時刻が9時間ずれる | プロジェクトのタイムゾーンが日本になっていない | 「プロジェクトの設定」でタイムゾーンを直し、inTimezone('Asia/Tokyo') を付ける |
| ある日から全部のトリガーが止まった | トリガーを作った人のアカウントが削除された、または管理コンソールでGASがオフにされた | 動かすアカウントで作り直す。管理者に設定を確かめる |
| 失敗しても誰も気づかない | 通知は作った人のGmailにしか来ない。シンプルトリガーは通知の仕組みが公式に書かれていない | 通知を共有のグループへ転送する。0件の日も送る作りにする |
| トリガーの画面に、あるはずのトリガーが無い | 別のアカウントで作ったトリガーは見えない | 作った人のアカウントで「マイ トリガー」を見る |
| 何行かまとめて貼り付けると確定日時が入らない | 複数のセルの編集では e.value が入らない | 1行ずつ選ばせるか、e.range の全行を読む書き方にする |
| 「その操作を実行するには承認が必要です。」で止まる | 新しいサービスを足したあと、トリガーの持ち主が承認し直していない | 持ち主のアカウントでエディタから一度実行して承認する |
いちばん気づきにくいのは「ある日から全部止まった」です。トリガーはエラーで止まるのではなく、黙って動かなくなります。0件の日も「ありません」と送る作りにしておくと、通知が来なかった朝に受付が気づけます。
次にやること
- Google Apps Script(GAS)とは:できること・始め方
- GASでメールを自動送信する:スプレッドシートの宛先に1通ずつ差し込む
- GASが止まったとき:エラー・実行時間・上限の見方
- Google Chatの使い方:スペースで社内連絡をまとめる