田中力磨 › 一人塾のGAS自動化 実録 › 週次ダイジェスト

GASで週次レポートを自動メール送信——スプレッドシートの集計を毎週月曜に1通で届ける

日曜夜のシート巡回をやめ、月曜の朝に届くメールだけを読む形にした。集計と仕組みの生存確認を1通にまとめたGASの記録。

2026-10-26 公開 / 滋賀県彦根市の一人塾「総合学習教室ブリッジ」で実際に動いている仕組みの記録 / Google Apps Script(GAS)

この記事は、教室で動いているコードと運用の記録をもとに、AI(Claude)が書きました。

何を自動化したか

この教室では、自動化が実際に動いているかを点検するため、毎週日曜の夜に30〜45分かけてシートや画面を見て回るチェックリストを作った。GASエディタでトリガーの本数と直近7日のエラーを確かめ、自動化用のスプレッドシートで各シートの最終行の日付を見て、SNSに投稿が出ているかも目で追う内容だ。

2026-07-06に、負担を減らす見直しの一つとしてこの巡回を廃止し、毎週月曜の朝にGASが件名に「📊 週次ダイジェスト(これ1通でOK)」と入ったメールを1通送る形に置き換えた。最初に最小の計器盤として決めたのは3つ——体験申込とそのきっかけ、講習の申込と未入金、仕組みの健康状態(今はSNS自動投稿の欠け・データ整合性など)——で、設計目標は「このメールを読めば、シートは開かなくていい」。コードのコメントに書いた見積もりは「読むだけ(2〜3分)」で、実測値ではない。

要点は、健康状態の行を、異常が無くても毎週必ず出すことだ。正常なら✅や件数を、異常なら⚠と対処の手がかりを書く。毎週届く1通に同じ行が並ぶので、メールそのものが仕組みの生存確認を兼ねる。チェックリストは⚠が出たときのトラブルシュート用に残してある。

仕組み(データの流れ)

送信は毎週月曜の7時台。週次のディスパッチャー autoWeeklyMonday の最後の1行で autoWeeklyDigest を呼ぶ(トリガー上限20本の壁参照)。最後に置くのは、同じ月曜に動く他の処理が書いた結果(この記事では省いた面談リストの行など)まで読むためだ。

行読むもの数え方
体験生「体験生フォロー」シート「予約」の行数と累計。メモ欄の「きっかけ: 」を種類別に数える
講習申込「講習申込」シート直近7日の行数、累計、生徒名があって入金欄が空の行数
SNS自動投稿Threads・Xの投稿ログ直近7日の行数と必要な枠数
データ整合性スクリプトプロパティ毎晩のチェックが残した直近1回分の件数

体験生と講習申込のシートには公開の申込フォームが書き込み(LINE自動応答と申込フォーム)、選択式の質問「何をご覧になって教室を知りましたか」の答えはメモ欄(講習申込は備考欄)に「きっかけ: 〇〇」の形で残る。このほかにも、講習の種類別の内訳など教室の事情に合わせた行を同じ1通に載せている(この記事では省く)。本文は次の形になる(数字は伏せ字、一部の行は省略)。

■ 体験生: 現在「予約」ステータス n人(累計 N人)
  きっかけ内訳(フォーム計測開始 2026-07-05〜): 選択肢A a件 / 選択肢B b件
■ 講習申込: 今週 m件(累計 M件・未入金 u件)
…(中略)…
■ 仕組みの状態:
  ✅ SNS自動投稿(直近7日): Threads t/14・X 定時停止中(手動・告知分 x本)
  データ整合性: 異常i件(うち自動修復j件)

――このメールを読めば週次の確認は終わりです。シートを開いて回る必要はありません。
本文には件数や状態を載せ、生徒の名前や連絡先は入れない。個人情報を含むシートを集計するなら、本文に載せる範囲を先に決めておく。

コード(要点)

通知メールは共通の関数1つから送っている。宛先はスクリプトプロパティ NOTIFY_EMAIL、未設定なら実行ユーザーのアドレスだ。

/** 通知メール(NOTIFY_EMAIL プロパティ宛。未設定なら自分宛) */
function autoNotify(subject, body) {
  var to = PropertiesService.getScriptProperties().getProperty('NOTIFY_EMAIL')
        || Session.getActiveUser().getEmail();
  if (!to) { Logger.log('通知先が不明のためスキップ: ' + subject); return; }
  MailApp.sendEmail(to, '【ブリッジ自動化】' + subject, body);
}

Session.getActiveUser().getEmail() はセキュリティの設定によっては空文字を返す。空なら送らずログに残すだけなので、宛先は NOTIFY_EMAIL で明示する方が確実だ。

集計の本体は、体験生・講習申込のまとまりを try/catch で囲み、例外が出ても L.push('■ 体験生: 集計失敗(' + e.message + ')') のように失敗したこと自体を1行書く。健康状態は正常でも1行出す。

function autoWeeklyDigest() {
  var now = new Date();
  var weekAgo = new Date(now.getTime() - 7 * 24 * 3600 * 1000);
  var twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 3600 * 1000);
  var L = [];
  // …(省略)…
  // ⑤ 仕組みの健康状態
  var health = [];
  // …(省略)…
  try {
    // SNS自動投稿の充足(直近7日)。「無音で欠けた回」を毎週必ず可視化する計器盤(計画書M2)
    health.push(autoSnsPostFillLine());
  } catch (e) { /* 投稿ログ未作成なら黙ってスキップ */ }
  try {
    // データ整合性の夜間チェック(integration/DataRepair.gs の dataIntegrityNightly)。
    // 直近の夜間実行1回分のスナップショット。異常ゼロの日はメール単体では沈黙するが、
    // ここでは週次に必ず1行載せて「仕組みが動いているか」自体を可視化する
    var diProps = PropertiesService.getScriptProperties();
    var diCount = diProps.getProperty('DATA_INTEGRITY_LAST_COUNT');
    if (diCount !== null) {
      var diFixed = diProps.getProperty('DATA_INTEGRITY_LAST_FIXED') || '0';
      health.push('データ整合性: 異常' + diCount + '件(うち自動修復' + diFixed + '件)');
    } else {
      health.push('⚠ データ整合性チェックが未実行です(dataIntegrityNightlyが一度も走っていません)');
    }
  } catch (e) { /* プロパティ未設定等なら黙ってスキップ */ }
  L.push('■ 仕組みの状態:');
  L.push(health.length ? health.map(function(h) { return '  ' + h; }).join('\n') : '  ✅ 特記事項なし');
  // …(省略)…
  autoNotify('📊 週次ダイジェスト(これ1通でOK)', L.join('\n'));
}

毎晩のデータ整合性チェックは、異常ゼロの日は何も送らない。それでは「正常だから静か」と「止まっているから静か」を見分けにくいので、週1回ここで直近の実行が残した件数を出し、一度も記録が無ければ「未実行」と警告する。ただし載せるのは最後に動いた回の件数だけで、その日時は出していない。途中で止まると古い件数が出続けるため、止まったこと自体は失敗の記録とトリガーの自己点検(トリガー上限20本の壁)で拾っている。また、健康状態の行は例外が出ると黙って飛ばすため、全部が飛ぶと「✅ 特記事項なし」になる点も弱点として残っている。

SNS自動投稿の行は、失敗の記録ではなく、投稿ログに残った成功の行数を枠と比べる。

/** 直近7日のThreads/X投稿の充足数を1行で返す(週次ダイジェスト用。2026-09-13以降: Threads 2回×7日=14枠が正、Xの定時投稿は停止) */
function autoSnsPostFillLine() {
  var since = new Date(new Date().getTime() - 7 * 24 * 3600 * 1000);
  function countRows(sheet) {
    // …(省略)…
  }
  var th = countRows(getLogSheet());   // marketing/ThreadsMarketing.gs
  var x  = countRows(getXLogSheet());  // marketing/XMarketing.gs
  var props  = PropertiesService.getScriptProperties();
  var thNeed = props.getProperty('THREADS_NIGHT_SLOT') === 'on' ? 21 : 14;  // 08/12時のみ(告知期間の20時は加算されるだけ)
  var xOn    = props.getProperty('X_AUTOPOST') === 'on';
  var line = 'SNS自動投稿(直近7日): Threads ' + th + '/' + thNeed
           + '・X ' + (xOn ? (x + '/21') : ('定時停止中(手動・告知分 ' + x + '本)'));
  return (th >= thNeed && (!xOn || x >= 21)) ? ('✅ ' + line)
       : ('⚠ ' + line + ' — 欠けた枠は生成失敗かAPI失敗です。失敗通知メールと投稿ログを確認してください');
}

投稿ログには成功した回だけが書かれる。成功を数えるので、失敗の記録そのものが漏れた回(トリガーが消えて走らなかった回など)も「欠け」として表に出る。

ハマった点と回避策

「条件がそろわなければ静かにスキップ」が、故障に見えた

毎週月曜には、様子メモを材料にして筆者へ提案を届ける処理も動く。当初は直近2週間のメモが無いと何もせず終わる作りで、安全側のつもりだったが、受け取る側には故障にしか見えなかった(実際に「何も届いていない」と言われて発覚した)。メモが無い週も汎用の文面を筆者に届けるように直し、週次ダイジェストにはメモの件数の行を足した。

投稿が欠けても、誰も気づけなかった

Threads・Xの自動投稿には、生成がすべて失敗した回や投稿APIが失敗した回に、実行ログへ1行残すだけで投稿が消える経路があった。実行ログは誰も見ないので、欠けた回は無かったことになる。そこで失敗した回は必ず記録して毎晩のまとめ通知に載せ(同じ種類の失敗が続くときは、前回の通知から7日ごとにだけ通知する。詳しくはGemini APIの記事)、週次ダイジェストでは投稿数を枠と比べる2段構えにした。

運用を変えたら、計器の「正解」も同じ変更で直す

2026-09-13に、反応がほとんど無かったXの定時投稿を止め、Threadsも1日3回から2回に減らした。必要枠が21のままだと、正常なのに毎週⚠が出る。誤警報が続くと⚠が読まれなくなるので、同じ変更で計器を「Threads 14枠/X 定時停止中」に直した。必要枠は投稿を切り替えるのと同じスクリプトプロパティで決まるため、20時の回を戻せば枠も21に戻る。週次生成を止めたPR動画の未投稿警告を外したのも同じ理由だ。

導入手順

  1. 集計したいシートを1つのスプレッドシートにまとめ、1行目を見出しにする。シートは「無ければ見出し付きで作る」関数で取る
  2. スクリプトプロパティ NOTIFY_EMAIL に送信先(例 example@example.com)を入れる
  3. 送信用の関数を1つ作り、MailApp.sendEmail(宛先, 件名, 本文) で送る
  4. 集計関数を書く。まとまりごとに try/catch で囲み、失敗したら「集計失敗(理由)」をその行に書いて次へ進む
  5. 健康状態の行は、正常なら✅、異常なら⚠と直し方を書き、どちらでも必ず1行出す
  6. GASエディタから集計関数を一度手動で実行し、権限を承認して1通届くことを確かめる。appsscript.json で oauthScopes を明示しているなら https://www.googleapis.com/auth/script.send_mail を含める
  7. 週1回の時間主導型トリガーを作る(この教室では ScriptApp.newTrigger('autoWeeklyMonday').timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(7).create())。この1行は1回だけ実行する(実行するたびに同じトリガーが1本ずつ増え、毎週同じメールが何通も届く)。ディスパッチャーがあるなら、トリガーは作らずその最後に1行足す。atHour(7) はスクリプトのタイムゾーンで7時台のどこかで動く
  8. 数週間使ったら、読まない行を削り、止めた仕組みの行や警告を外す

よくある質問

Q. 異常が無い週までメールが来るのは、うるさくないですか

送るのは週1通に固定しているので、通知は増え続けない。「異常が無ければ送らない」にすると、届かないのが正常なのか止まっているのかを見分けられない。毎晩の失敗通知は異常があった日だけ、週次ダイジェストは毎週必ず、と役割を分けている。

Q. MailApp と GmailApp のどちらを使えばよいですか

送るだけなら MailApp で足りる。送信専用で受信箱を読めず、権限も送信用のスコープだけで済む(MailApp リファレンス)。下書きの作成や受信メールの検索が要るなら GmailApp を使う。

Q. ダイジェスト自体が止まったら、どう気づきますか

例外で落ちればその晩のまとめ通知に載り、GASごと止まれば外部のハートビート確認で気づく(トリガー上限20本の壁参照)。月曜の朝にこの1通が届かないこと自体も異変の合図になる。

筆者の本とnote(有料)で、まとめて読む