GASのトリガー:定期実行・フォーム送信時・編集時

公開 書いた人 大林 諒

GASのスクリプトは、作っただけでは誰かがボタンを押すまで動きません。決まった時刻に動かしたり、フォームが送信された瞬間に動かしたりするのがトリガーです。この記事では、トリガーの種類と、名前だけで動くシンプルトリガーと画面やコードで設定するインストール型トリガーの違い、設定の手順、時間主導型の時刻の指定の幅、トリガーが誰の権限で動くか、止まったときの見方を順に書きます。例は全記事で共通の、スタッフ6名ほどの予約制の小さな料理教室「サンプル料理教室」で、毎朝8時に今日の体験予約をGoogle Chatのスペースに送る処理と、フォームが送信されたら予約一覧の台帳へ転記する処理を作ります。前提として、スクリプトの作り方と初回の承認は「Google Apps Script(GAS)とは:できること・始め方」の記事の手順で一度通してあるものとします。

この記事の内容
  1. 先に決めること
  2. トリガーの種類
  3. サンプル料理教室で作るもの
  4. 手順1:毎朝8時にChatへ送る
  5. 手順2:フォーム送信時に台帳へ転記する
  6. 手順3:編集時に確定日時を記録する
  7. 実行数とエラー通知の見方
  8. 応用
  9. できないことと、その代わり
  10. よく詰まるところ
  11. 次にやること

先に決めること

トリガーは一度設定すると、誰も画面を見ていないところで動き続けます。設定する前に次の3つを決めておきます。

1つ目は、トリガーを誰のアカウントで作るかです。インストール型トリガーは、常にトリガーを作った人のアカウントの権限で動きます。受付の担当者がトリガーを作ると、確認メールは受付の個人のアドレスから送られ、受付が退職してアカウントが消えると、トリガーも動かなくなります。実際に運用していると、退職者のアカウントを削除した翌日から確認メールが止まっていた、ということが起きます。サンプル料理教室では、トリガーはすべて代表のアカウントで作ると決めています。

2つ目は、別の人が作ったトリガーは見えない、ということを前提にすることです。あるアカウントは、別のアカウントが作ったトリガーを見ることができません。同じスクリプトに、代表と受付がそれぞれトリガーを作ると、お互いに相手のトリガーが見えないまま二重に動きます。誰が作るかを1人に決めておけば、この事故は起きません。

3つ目は、何時に動けばよいかを幅で考えることです。時間主導型のトリガーは、ちょうど8時0分には動きません。8時台のどこかで動く、という指定になります。受付が8時半に出勤して見るなら8時台で足りますが、8時ちょうどに必ず届いていてほしいなら7時台に設定します。

トリガーの種類

トリガーには、シンプルトリガーとインストール型トリガーの2つの系統があります。

系統種類いつ動くか対象
シンプルonOpen編集権限のある人がファイルを開いたときスプレッドシート・ドキュメント・スライド・フォーム
シンプルonEdit人がスプレッドシートの値を変えたときスプレッドシート
シンプルonSelectionChange人がスプレッドシートの選択を変えたときスプレッドシート
シンプルdoGet・doPostWebアプリが開かれたとき、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を取る

  1. パソコンのブラウザでGoogle Chatを開きます。WebhookはChatのモバイルアプリからは設定できません。
  2. スペース「受付」を開き、スペース名の横の下向き矢印をクリックします。
  3. 「アプリとインテグレーション」をクリックします。
  4. 「Webhook を追加」をクリックします。
  5. 名前に「体験予約のお知らせ」と入れて保存します。
  6. 作られたWebhookの「その他」から「リンクをコピー」をクリックします。
Chatのスペースの「アプリとインテグレーション」から「Webhook を追加」を開いたところ
Chatのスペースの「アプリとインテグレーション」から「Webhook を追加」を開いたところ

コピーしたURLには、そのスペースに投稿できる鍵が入っています。URLを知っている人なら誰でもスペースに書き込めるので、コードに直接書かずに、スクリプトのプロパティにしまいます。組織によっては、管理者がWebhookを使えるようにしていないと、この画面に項目が出ません。

URLをスクリプトのプロパティに入れる

  1. 体験予約のスプレッドシートで「拡張機能」→「Apps Script」を開きます。
  2. 左側の「プロジェクトの設定」(歯車)をクリックします。
  3. 「スクリプト プロパティ」の「スクリプト プロパティを追加」をクリックします。
  4. プロパティに CHAT_WEBHOOK_URL、値にコピーしたURLを貼ります。
  5. 「スクリプト プロパティを保存」をクリックします。

同じ画面の「タイムゾーン」が「(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件なのかを区別するためです。何も来ない日があると、受付はトリガーが止まったことに気づけません。

画面からトリガーを設定する

  1. エディタの左側の「トリガー」(時計の形)をクリックします。
  2. 右下の「トリガーを追加」をクリックします。
  3. 「実行する関数を選択」で sendTodayTrials を選びます。
  4. 「実行するデプロイを選択」は「Head」のままにします。
  5. 「イベントのソースを選択」で「時間主導型」を選びます。
  6. 「時間ベースのトリガーのタイプを選択」で「日付ベースのタイマー」を選びます。
  7. 「時刻を選択」で「午前7時~8時」を選びます。
  8. 「エラー通知設定」で「今すぐ通知を受け取る」を選びます。
  9. 「保存」をクリックします。
「トリガーを追加」のダイアログで、時間主導型・日付ベースのタイマー・午前7時~8時を選んだところ
「トリガーを追加」のダイアログで、時間主導型・日付ベースのタイマー・午前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本だけ並んでいることを確かめます。

エディタの「トリガー」画面に sendTodayTrials と copyToLedger の2本が並んでいるところ
エディタの「トリガー」画面に sendTodayTrials と copyToLedger の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 なので、トリガーの設定は要りません。保存した時点から、受付が対応状況を変えるたびに動きます。

予約一覧で対応状況のプルダウンを「確定」にすると、H列に確定した日時が入ったところ
予約一覧で対応状況のプルダウンを「確定」にすると、H列に確定した日時が入ったところ

最初の2行で、別のシートや別の列の編集なら何もせずに終わるようにしています。onEdit はファイルのどこを編集しても呼ばれるので、この絞り込みが無いと、無関係な編集のたびに処理が走ります。e.value は1つのセルを編集したときだけ入ってきます。何行かまとめて貼り付けたときは空になるので、まとめて「確定」に貼り替えた行には日時が入りません。受付には1行ずつ選ぶよう伝えておきます。

確定したらメールも送りたくなったら、関数名を handleEdit に変え、インストール型の「編集時」トリガーを付けます。関数名を onEdit のままインストール型のトリガーも付けると、1回の編集でシンプルとインストール型の両方が動き、メールが2通送られます。

実行数とエラー通知の見方

実行数で動いたかを見る

  1. エディタの左側の「実行数」をクリックします。
  2. 関数名・種類(「時間主導型」「トリガー」「エディタ」など)・開始時刻・実行時間・ステータスが並びます。
  3. 失敗だけを見たいときは「フィルタを追加」→「ステータス」で「失敗」と「タイムアウト」にチェックを入れます。
  4. 行をクリックすると、その回の console.log の中身とエラーの内容が出ます。
「実行数」で sendTodayTrials の実行が並び、ステータスが「完了」になっているところ
「実行数」で sendTodayTrials の実行が並び、ステータスが「完了」になっているところ

script.google.com を開いて左側の「マイ トリガー」を見ると、自分のアカウントで作ったトリガーが、全部のスクリプトをまたいで一覧になります。どのファイルにトリガーが仕掛けてあるか分からなくなったときは、ここから探します。

エラー通知のメール

インストール型トリガーで関数が失敗しても、画面には何も出ません。かわりに、トリガーを作った人のGmailに、失敗のまとめのメールが届きます。メールのリンクから、トリガーの無効化やスクリプトの修正の画面に飛べます。届く頻度は、トリガーの「エラー通知設定」で選びます。毎朝の通知のように1日1回動くものは「今すぐ通知を受け取る」、5分ごとに動くものは毎日のまとめにしておくと、通知のメールで受信トレイが埋まりません。

失敗のメールの仕組みが公式に書かれているのは、インストール型のトリガーだけです。シンプルトリガーの onEdit や onOpen は、失敗したら「実行数」を見に行かないと分からない前提で扱います。止まっても気づきにくいので、大事な処理はシンプルトリガーに乗せないでおきます。

Apps Scriptから届いた、トリガーの失敗のまとめのメール
Apps Scriptから届いた、トリガーの失敗のまとめのメール

トリガーを作った人が、このメールを読まない設定にしていたり、そもそも退職していたりすると、誰も失敗に気づきません。サンプル料理教室では、トリガーを作る代表のGmailで、Apps Scriptからの通知メールを受付と共有するグループへ転送するフィルタを作っています。

応用

トリガーの合計時間を意識する

Google Workspaceのアカウントでは、トリガーで動いた時間の合計が1日6時間まで(無料のGoogleアカウントは90分まで)です。1人・1スクリプトあたりのトリガーは20本までです(いずれも2026年10月時点)。5分ごとのトリガーは1日288回動くので、1回に1分かかる処理なら、それだけで1日4時間以上を使います。数分ごとに見張りたい処理は、間隔を延ばせないか、そもそも外のサービスからWebhookで知らせてもらえないかを先に考えます。

退職や担当替えの前にやること

トリガーの持ち主が辞めるときは、アカウントを消す前に次の順で移します。

  1. 持ち主のアカウントで script.google.com の「マイ トリガー」を開き、トリガーの一覧を控えます。
  2. 引き継ぐ人のアカウントでスクリプトを開き、setupTriggers を実行してトリガーを作り直します。
  3. 「実行数」で、新しいトリガーが引き継ぐ人の名前で動いたことを確かめます。
  4. 元の持ち主のトリガーを削除します。

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件の日も「ありません」と送る作りにしておくと、通知が来なかった朝に受付が気づけます。

次にやること

あわせて読む