Apps Scriptで期限到来の案件をメール通知する方法|未対応行を知らせる

日付記号とチェック欄のある表から封筒への矢印、ベル、パソコンを使う人のイラスト デジタルツール
一覧メールで知らせる流れのイラストです。実際の送信・受信・作業完了を示すものではありません。

期限を過ぎた案件をメールで知らせるには、期限、対応の状態、通知済みの記録を分けて持たせます。まずは本番と分けたテスト用ブックで自分宛てに送り、対象行と日時の記録を照合しましょう。1日1回、該当行を1通にまとめる運用から試せます。ただし、通知が届くことと作業が終わることは別です。受信後に誰が案件を開き、何を済ませて状態を変えるかまで決める手順を説明します。

この例は、期限が当日以前で、状態が「未確認」または空欄の行を知らせる基本形です。空欄は完了と判断できないため対象に含めますが、それだけで作業が未実施だったとは断定しません。少量の管理表を同じプロジェクトから動かす用途を想定し、実行中は人や別処理による編集・並べ替え・行追加削除を止めます。この運用ができない共同編集表へ、そのまま本番適用しないでください。

状況判断最初の対応続けて点検すること
期限を過ぎてから気付く期限が当日以前で未確認の行だけを通知する6列の管理表を作る期限が日付として入力されているか
同じメールが何度も届く通知済み日時と再送の扱いを決める送信後に日時を書き込む再通知する案件と根拠の記録
いきなり担当者へ送るのが不安本番通知前にテストする自分のメールアドレスを設定する2回目に再送されないか
担当者ごとに案件が異なる通知先ごとにメールを分ける通知先列を入力する他担当の案件が混ざらないか

通知を作る前に決める4つの条件

先に、何を終えたら状態を変えるのかを決めます。1行へ複数案件をまとめると、一部だけ終わった際に残件を追いにくいため、基本は「1行1件」です。下の4条件を担当間で合わせましょう。状態欄の「確認済み」は対象の照合作業を終えた印として使い、別に残る承認、手直し、出荷などまで完了した意味にしないようにします。「不要」は対象外と判断した根拠を残して選びます。

  • 期限:対象の照合・点検をいつまでに行うか
  • 確認状況:未確認、確認済み、不要のどれか
  • 通知済み日時:メール処理の後に日時を書き込めた印。受信や作業完了とは別
  • 通知先:最初に知らせる担当者のメールアドレス

期限が日付でない行や、通知先が空欄の行は、このコードのメールには出ません。そのため、通知が来なかったことを対象なしの証拠にせず、実行ログと表の空欄を担当者が点検します。期限未定には決める担当と回答期限を別途残してください。テストは架空の行を入れた複製ブックで行います。テスト宛先へ送っても日時を書き込むので、本番行を混ぜて試すと後の通知を止めてしまいます。

通知用スプレッドシートへ6列を設ける

1行目へ下の6列を置きます。列順は変えられますが、見出しの文字や末尾の空白まで一致させ、同名の見出しを重ねません。管理番号は空欄にせず重複しない値にします。表の番号とメールの番号を同じ案件へ結び付けるためです。日付、状態、通知先は入力する人が決め、通知済み日時は処理が書く欄として分けてください。

列名入力・更新する人入力ルール記入例
管理番号登録担当者1行を識別できる番号または案件名を必須にする記入例:NC-024
確認内容登録担当者メールだけで作業が分かる短い説明を入れる記入例:図面改訂の照合
期限登録担当者文字列ではなく日付セルとして入力する記入例:2026/04/15
確認状況確認担当者未確認、確認済み、不要に統一する記入例:未確認
通知先管理担当者担当者ごとの通知先を入力する記入例:manager@example.com
通知済み日時スクリプト通常は手入力せず、送信処理の後に自動記録する記入例:2026/04/15 09:10

状態はプルダウンで選ぶと、コードが扱わない表記を減らせます。例のコードは状態の前後空白を除いて比較しますが、別の語を入力するとエラーにして送信を止めます。スプレッドシートの入力規則で品番ミスを減らす設定と運用も参考に、候補の追加時は通知条件も合わせて直しましょう。期限は文字列でなく日付として保存し、シートのタイムゾーンを業務の日付に合わせます。

未確認かつ期限到来の行だけをメールで送るApps Script

複製したテスト用ブックで[拡張機能]からApps Scriptを開き、次の例を保存します。SHEET_NAMEは対象シート名、TEST_EMAILは自分の受信先へ変えます。例示のアドレスのままでは送信を止めます。本番へ移すのは宛先分離と日時更新の試験後とし、その際にTEST_EMAILを空文字にします。ロックは同じプロジェクトの重複実行を抑えますが、人の編集や別プロジェクトの処理は止めないため、実行時間帯を分けてください。

const CONFIG = {
  SHEET_NAME: '確認管理',
  TEST_EMAIL: 'your-address@example.com', // 複製したテスト用ブックで自分の宛先に変更。本番では ''
  SUBJECT: '【要確認】期限到来・未確認の一覧'
};

function sendUnconfirmedAlerts() {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(5000)) throw new Error('別の通知処理が実行中です。');
  try {
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    if (!spreadsheet) throw new Error('対象ブックに紐付けて実行してください。');
    const sheet = spreadsheet.getSheetByName(CONFIG.SHEET_NAME);
    if (!sheet) throw new Error('指定したシート名が見つかりません。');
    const values = sheet.getDataRange().getValues();
    if (values.length < 2) return;
    const headers = values[0].map(String);
    const required = ['管理番号', '確認内容', '期限', '確認状況', '通知先', '通知済み日時'];
    const col = {};
    required.forEach(name => {
      const index = headers.indexOf(name);
      if (index === -1 || index !== headers.lastIndexOf(name)) {
        throw new Error('見出し「' + name + '」は重複させず正確に1つ置いてください。');
      }
      col[name] = index;
    });
    const oneAddress = /^[^\s,;<>@]+@[^\s,;<>@]+\.[^\s,;<>@]+$/;
    const testAddress = String(CONFIG.TEST_EMAIL || '').trim();
    if (testAddress === 'your-address@example.com' || (testAddress && !oneAddress.test(testAddress))) {
      throw new Error('テスト用の宛先を自分のメールアドレスへ変更してください。');
    }
    const timezone = spreadsheet.getSpreadsheetTimeZone();
    const today = Utilities.formatDate(new Date(), timezone, 'yyyyMMdd');
    const targets = [];
    const ids = new Set();
    values.slice(1).forEach((row, index) => {
      if (required.every(name => row[col[name]] === '')) return;
      const id = String(row[col['管理番号']] || '').trim();
      if (!id || ids.has(id)) throw new Error('管理番号が空欄または重複しています。行:' + (index + 2));
      ids.add(id);
      const due = row[col['期限']];
      const status = String(row[col['確認状況']] || '').trim();
      const recipient = String(row[col['通知先']] || '').trim();
      const notified = row[col['通知済み日時']];
      if (!['', '未確認', '確認済み', '不要'].includes(status)) {
        throw new Error('状態の選択肢が一致しません。行:' + (index + 2));
      }
      if (!(due instanceof Date) || isNaN(due) || !recipient) {
        console.warn('期限または通知先が未登録のため対象外。行:' + (index + 2));
        return;
      }
      if (!oneAddress.test(recipient)) throw new Error('通知先は単一アドレスで記入してください。行:' + (index + 2));
      const dueDate = Utilities.formatDate(due, timezone, 'yyyyMMdd');
      if (dueDate <= today && (status === '' || status === '未確認') && notified === '') {
        targets.push({ rowNumber: index + 2, row: row, recipient: recipient });
      }
    });
    if (targets.length === 0) return;
    const groups = new Map();
    targets.forEach(item => {
      const address = testAddress || item.recipient;
      if (!groups.has(address)) groups.set(address, []);
      groups.get(address).push(item);
    });
    if (MailApp.getRemainingDailyQuota() < groups.size) {
      throw new Error('メール送信可能数が不足しています。');
    }
    groups.forEach((items, address) => {
      const lines = items.map(item => {
        const row = item.row;
        const dueText = Utilities.formatDate(row[col['期限']], timezone, 'yyyy/MM/dd');
        return '・' + row[col['管理番号']] + '|' + row[col['確認内容']] + '|期限:' + dueText;
      });
      const body = '期限が到来し、確認状況が未確認または空欄の行です。\n\n' +
        lines.join('\n') + '\n\n対象件数:' + items.length + '件\n' +
        '対象作業を終えたら、シートの「確認状況」と対応記録を更新してください。\n' + spreadsheet.getUrl();
      MailApp.sendEmail({ to: address, subject: CONFIG.SUBJECT, body: body });
      // 受信・既読・業務完了の記録ではない。実行中は人や別処理による編集を停止する。
      items.forEach(item => {
        sheet.getRange(item.rowNumber, col['通知済み日時'] + 1).setValue(new Date());
      });
      SpreadsheetApp.flush();
    });
  } finally {
    lock.releaseLock();
  }
}

期限の判定はシートのタイムゾーンで日付だけを比べ、当日以前、状態が空欄または「未確認」、通知済み日時が空欄の行を対象にします。時刻付きの締切を管理する例ではありません。本番は単一の通知先ごとに1通へまとめ、テスト中は自分宛てへ全対象をまとめます。メール処理が戻ってから日時を書きますが、これは受信・既読・業務完了の証明ではありません。対象がなければメールは送りません。

期限前の予告と期限超過の通知を分けて考える

最初は当日以前の通知で、どの行が選ばれ、受信者がどこへ記録するかを試します。期限前の予告も加えるときは、予告と期限超過を別の通知として設計しましょう。同じ日時欄を使うと、予告を送ったために期限超過後の通知が止まるおそれがあるためです。掲載コードは予告や毎日の再送を実装していません。

予告を加える案では「予告通知済み日時」と「期限超過通知済み日時」を分け、作業の準備日数から送信日を決めます。毎日の再送なら最終通知日をシート基準の日付と比較する処理へ改修します。列名を変えるだけでは動きません。改修後は初回、同日、翌日、状態変更後をテストし、未処置が続いた場合の引き継ぎ先も決めます。通知回数を増やすだけで対応が進んだ扱いにはしません。

時間主導型トリガーで毎日自動チェックする

自動実行に移す場合はApps Scriptのトリガー画面で、関数にsendUnconfirmedAlerts、イベントに時間主導型、頻度に日次を選びます。コードを保存するだけでは定期実行は始まりません。既存トリガーの有無を調べ、同じ処理を重複登録しないようにします。実行中の編集を止められ、担当が結果を読める時間帯を選んでください。

初回は手動実行し、要求される権限と対象ブック・送信先を読んで承認します。インストール型トリガーは作成者のアカウントで動くため、担当交代では旧担当のトリガー停止と新担当の設定を一緒に扱います。時間主導型の実行時刻には幅があるので、分単位の緊急連絡には使いません。日々の実行結果を読む担当と、動かなかった場合の手作業による連絡も決めましょう。

送信前の試験と不具合の調査を順に行う

テスト用ブックには、対象行だけでなく未来日、処理済み、空欄、重複番号などの行も用意します。SHEET_NAMEをテスト用シートへ合わせ、架空の案件だけにしてください。手動実行する関数はsendUnconfirmedAlertsです。まず1行の対象で送信先と記録を読み、次に複数宛先でも他担当の案件が混ざらないかを試します。本番と同じ宛先分離の試験も、自分で管理する試験用アドレスだけで行います。

  • 期限を過去日、確認状況を未確認、通知済み日時を空欄にした行がメールに出る
  • メールの管理番号、確認内容、期限、対象件数、シートURLが正しい
  • 送信後に、対象行だけの通知済み日時へ実行時刻が記録される
  • 同じまま2回目を実行しても、その行が再送されない
  • 確認済みの行、未来日の行、期限が空欄の行はメールに出ない

届かない場合は、実行履歴の成功・失敗、エラー、対象外の警告を先に読みます。次にシート名と6つの見出し、管理番号、日付、状態、通知先、通知済み日時を元の行へ照合します。メール送信とセル更新は一体の処理ではないため、送信後の書込み失敗では再送される場合があります。途中エラーでは定期実行をいったん止め、送信・受信・記録を照合してから再実行を判断し、日時を一括消去しないでください。

メール通知とシートの見える化を役割分担する

受信した人は、管理番号で対象を開き、所定の作業を行って結果と実施日時を記録します。メールを読んだだけで状態を完了側へ変えないようにしましょう。別の処置や承認が残るなら、その担当と期限も追える欄を加えます。通知後も状態が変わらない行、送信先不明の行、通知されなかった行を管理担当が追い、危険や緊急案件はこの日次メールを待たず所定の連絡手順を使います。

表を開いたときの手掛かりには、条件付き書式で納期遅れを見える化する方法|行全体を自動色分けの色分けも使えます。ただし、色と通知は記録値からの判定で、現場の実施状況そのものではありません。Googleフォームで設備点検を記録し一覧化する方法と組み合わせる場合は、回答のない設備も追える予定表と照合します。回答が来た行だけを通知対象にしても、未回答の点検は拾えません。

よくある質問

期限を過ぎても未確認の行に、毎日1回だけ再通知するにはどう変更しますか?

掲載例は、通知済み日時を1回限りの記録として使います。毎日1回に変えるなら、最終通知日と今日をシートのタイムゾーンで比較し、送信後に日付を更新する処理へ改修します。同日2回目には送らず、翌日の未処置分には送る試験を行ってください。再送を続ける期間と責任者への引き継ぎ条件を決め、期限を延ばすだけで未処置を消さないようにします。

担当者ごとに対象行を分けて、別のメールで送れますか?

本番設定では、通知先ごとに対象をまとめて別メールを送ります。1行につき最初に扱う担当を1人決め、通知先にはカンマ区切りなどを使わず単一のアドレスを入れます。これは複数宛先を受け付けない例です。共同メールボックスを使う場合も閲覧できる人を管理し、誰が案件を引き取るかを決めます。受信者が対象表を開ける権限も試験してください。

通知済み日時を消して再通知しても、過去の対応履歴は残せますか?

対応メモ、実施日時、承認の根拠は通知済み日時とは別に保存します。再通知を認める場合は、過去の送信日時と再送理由を別記録へ残してから対象行の通知済み日時を消します。期限や担当を変えても、この欄が埋まっている限り掲載コードは再送しません。変更を誰へ伝えるかを判断し、以前の日時を消すだけで過去の処置まで失わないようにします。

シートの編集時だけ通知し、毎日の定期チェックを行わない運用にできますか?

編集を契機とする構成も作れますが、日付が変わるだけでは編集イベントは起きません。そのため、期限到来を拾う用途では時間主導型と結果の点検を基本にします。メール送信には権限を持つインストール型などの設計が要り、単純な編集トリガーへ置くだけでは同じ動作になりません。スクリプトなどによる更新では動かない場合も含め、実際の入力経路で試してください。

参考にした公式情報

下の公式資料は、実行権限、実行時刻の幅、失敗時の調査方法を読むための参照先です。特にトリガーは作成者に依存するため、共有した相手へ自動的に運用が引き継がれるとは扱いません。また、同じプロジェクトの実行を排他するロックの範囲は公式リファレンス(https://developers.google.com/apps-script/reference/lock/lock-service)で照合できます。表への人の編集まで止める機能とは区別します。

コード内のMailApp.sendEmailは送信処理、MailApp.getRemainingDailyQuotaは残りの送信可能な受信者数を読む処理です。上限はアカウントや利用状況で変わるため、固定の送信保証にはしません。getValuesは値を取得し、setValueはセルを書き換えます。複数セルを扱うsetValuesも含め、公式仕様へ照合して改修してください。手動で動いた場合も、利用先の権限、実際の受信、定期実行が成立したかは別に試します。

まとめ

まず「管理番号、確認内容、期限、確認状況、通知先、通知済み日時」の6列を複製したテスト用ブックへ用意し、送る行と送らない行を試しましょう。次に、受信後の作業、結果を書く人、未処置を追う人を決めます。宛先分離と記録が意図どおり動き、実行中の編集を避けられる状態になってから定期実行へ移します。

運用後は、実行失敗、日時の書込み漏れ、期限・宛先の空欄、通知後に残った案件を点検します。送信が成功しても届かない場合や、届いても対応が進まない場合を分けて調べるためです。条件や担当を変えたら既存記録の扱いを決め、試験用の行で再度動作を照合してください。通知を増やすことより、知らせた案件が次の担当へ渡ったことまで追える運用を目指しましょう。

タイトルとURLをコピーしました