朝、出勤してPCを立ち上げて、まずスプレッドシートを開く。Googleフォームの回答が何件たまったか、いちばん下までスクロールして行番号を見る。昨日の最後が何行目だったか思い出せなくて、タイムスタンプを目でさかのぼる。
わたしはずっとそうでした。場所は職員室。会議の直前に「で、いま何件?」と聞かれて、慌ててもう一度シートを開いて数え直すところまで、毎朝セットです。数字を知りたいだけなのに、自分が「人間カウンター」になっている。
しかも、です。前回(記事02:フォーム回答をLINEに即時通知)で「回答が来た瞬間に通知」をつくったら、今度は新しい本音が顔を出しました。通知は来る。来たことは分かる。でも「結局、全部で何件?」「昨日は何件増えた?」という全体像は、シートを開かないと分からない。
というわけで今回は、記事02の末尾で予告した「たまっていく回答を“見る・まとめる”側」です。ゴールはこれ。
毎朝決まった時間帯に、「累計件数・昨日の新着数・選んだ質問の内訳」が1通のLINEになって、勝手に届く。
例によってコードは全部AIに書かせました(この方針のいきさつは創刊号に)。わたしがやったのは、貼って、押して、寝ただけです。
完成形:毎朝、フォーム回答の集計レポートがLINEに届く

これは、わたしのLINEに実際に残っているログです。
- 前夜23:21:「テスト送信」を手で実行 →「☀️ フォーム回答レポート(6/10)・累計:2件(昨日 +0件)」
- 翌朝8:48:何もしていないのに自動で着信 →「☀️ フォーム回答レポート(6/11)・累計:2件(昨日 +1件)」
翌朝この「+1」を見て、ちょっと笑ってしまいました。わたしが寝ている間に、ちゃんと「昨日」を数えている。6/10の朝に送っておいたテスト回答1件を、翌朝のレポートが「昨日の新着」として正しく数えてくれた。つまり、日付をまたぐ集計がちゃんと動いている証拠です。
もうひとつ。翌朝のスクショ、電波表示が4Gなんです。自宅のWi-Fiの外、出かけた先のポケットの中に届いていました。シートを開かないどころか、家にすらいないのに、数字のほうから届く。
正直に書いておきます。「数を数えるだけ」なら、COUNTIFのような関数でもできます。この仕組みの本当の価値は集計そのものではなく、「こちらが開かなくても、毎朝決まった時間帯に、1通に整形されて、向こうから届く」ことです。シートを開く習慣ごと、手放せます。
作戦会議:時間主導型トリガー=“目覚まし時計”を仕掛ける
これまでは「玄関チャイム」型でした
このシリーズでこれまで使ってきた仕掛け(フォーム回答をメール通知(記事01)、フォーム回答をLINEに即時通知(記事02))は、どちらも「フォームが送信された瞬間に動く」タイプでした。来客があったら鳴る、玄関チャイム型です。
今回はシリーズ初登場の「時間主導型トリガー」を使います(トリガー=プログラムを自動で動かす「きっかけ」の予約。Apps Scriptの画面上では「時間ベース」と表示されます)。誰も何もしなくても、「毎日この時間帯になったら動け」と命令しておけるもの。つまり目覚まし時計型です。チャイムと目覚まし時計、両方そろえば「来た瞬間に気づく」+「毎朝の定点観測」の二段構えになります。
正直ポイント:時間トリガーは“8時台のどこか”に動く
コードで「8時」と指定しても、届くのは「8時台のどこか」です。これはGoogle側の仕様で、分単位でぴったり指定することはできません。わたしの実測では8:48に届きました。「8:00きっかりに会議で読み上げたい」みたいな使い方には向きません。でも「朝のうちに数字が手元にある」が目的なら、まったく問題ありません。
記事02の「合鍵」をそのまま使い回します(メール派もOK)
LINE側の準備は、追加でやることがゼロです。記事02で作ったLINE公式アカウント(ボット)と合鍵(チャネルアクセストークン)を、そのまま再利用します。まだ作っていない方は記事02の前半をどうぞ。
そして、LINEはまだいいかな……という方も置いていきません。合鍵を空欄のままにすると、同じレポートが自分宛てのメールで届きます。記事01のメール派の方も、同じコードでそのまま使えます。
費用はゼロ円
GAS(Google Apps Script)の時間トリガーは無料枠の範囲内。LINEの無料枠は月200通で、毎朝1通なら月30通。余裕です。
手順:コードを貼って押すだけ(7ステップ)
手順1:回答先のスプレッドシートからApps Scriptを開く
フォームの回答先スプレッドシートを開き、メニューの「拡張機能」→「Apps Script」。開き方の基礎は記事01にあります。
手順2:コードを丸ごと貼り替える
もとから入っているコードを全部消して、この記事のコードを丸ごと貼り付けます。コードはこの少し下、「貼り付けるコード」の章にあります。
※記事02のコードを使っている方は、消す前に CHANNEL_ACCESS_TOKEN = '...' の '' の中身(合鍵)をコピーして、メモ帳などに控えておいてください。次の手順ですぐ使います。
手順3:設定4行を自分用に書き換える
コードのいちばん上に、設定が4行あります。その1つ目、CHANNEL_ACCESS_TOKEN = '' と書かれた行の '' の間に、記事02で発行した合鍵を貼ります(メール派は空欄のままでOK)。
ついでに 内訳を出す列 = ['ご用件'] の「ご用件」を、自分のフォームの質問名に書き換えてください。質問名が一字でも違うと、エラーは出ずに内訳だけ表示されません。書き方の詳細は、コードの章にまとめてあります。
手順4:保存する
フロッピーのアイコン(プロジェクトを保存)を押します(Ctrl+S / ⌘SでもOK)。保存するまでは、次の手順の「テスト送信」が関数メニューに出てこないので、ここを飛ばすと詰まります。
手順5:「テスト送信」を実行する
画面上部、「実行」「デバッグ」ボタンの並びにある関数名のメニューで「テスト送信」を選び、「実行」を押します。初回は「承認が必要です」の許可画面が出ます。進み方は記事01・02とまったく同じ「すべて選択→続行」です。

LINEに「今すぐ」朝レポが1通届けば成功。合鍵を空欄にした方は、自分宛てのメールが届いていれば成功です。

手順6:「初期設定」を実行する
関数を「初期設定」に切り替えて、1回だけ実行します。スプレッドシート側に「セットアップ完了!毎朝8時ごろにレポートが届きます。」と表示されたら、毎朝のスイッチONです(時刻の数字は、設定に合わせて表示されます)。

手順7:あとは寝るだけ
翌朝、自動で届きます。ちゃんと予約されたか不安な方は、Apps Script左メニューの時計アイコン(トリガー)を開いてみてください。「毎朝レポート/時間ベース」の行が見えていれば成功です。

貼り付けるコード(完全版・即時通知も同梱)
あなたが触るのは、いちばん上の設定4行だけです。
- CHANNEL_ACCESS_TOKEN:記事02で発行したLINEの合鍵。空欄のままなら自分宛てメールに切り替わります
- 通知時刻:レポートが届く時間帯(0〜23)。初期値は8(=朝8時台のどこか)
- 内訳を出す列:内訳を数えたい質問名。フォームの質問の見出しと一字一句同じ文字にしてください。書き換えるのは
''の中身だけで、['ご用件']→['お名前']のように。複数の質問を数えたいときは['ご用件', '学年']とカンマ区切りに。内訳が不要なら[]だけに。''や[]の記号は消さずにそのまま使ってください - 対象シート名:通常は空欄でOK。ひとつのスプレッドシートに複数フォームをつないでいる場合だけ、シート名(=スプレッドシート画面の下に並ぶタブの名前)を指定
// === 設定(ここだけ変えればOK)===
const CHANNEL_ACCESS_TOKEN = ''; // 記事02で発行したLINEの合鍵。空のままなら自分にメールで届きます
const 通知時刻 = 8; // 毎朝レポートが届く時間帯(0〜23。例: 8 → 朝8時台のどこか)
const 内訳を出す列 = ['ご用件']; // 内訳を数えたい質問名(フォームの見出しと同じ文字で。不要なら [] に)
const 対象シート名 = ''; // 通常は空でOK。複数フォームがつながっている場合だけシート名を指定
// 毎朝“自動で”動く本体(テスト送信からも呼ばれる)
function 毎朝レポート() {
const ss = SpreadsheetApp.getActive();
const sheet = (対象シート名 && ss.getSheetByName(対象シート名))
|| ss.getSheets().find(s => s.getName().startsWith('フォームの回答'))
|| ss.getSheets()[0];
const data = sheet.getDataRange().getValues();
const headers = data[0];
const rows = data.slice(1).filter(r => r[0]); // 空行は除く
const total = rows.length;
// 昨日ぶんの新着(タイムスタンプ列で判定)
const now = new Date();
const today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday0 = new Date(today0.getTime() - 24 * 60 * 60 * 1000);
const 昨日 = rows.filter(r => r[0] >= yesterday0 && r[0] < today0).length;
// 選んだ列の内訳(累計・多い順)
let 内訳 = '';
for (const 列名 of 内訳を出す列) {
const i = headers.indexOf(列名);
if (i < 0) continue;
const counts = {};
rows.forEach(r => { const v = String(r[i] || '(未記入)'); counts[v] = (counts[v] || 0) + 1; });
const lines = Object.entries(counts).sort((a, b) => b[1] - a[1])
.map(([v, c]) => ` ${v}:${c}件`).join('\n');
内訳 += `\n■「${列名}」の内訳\n${lines}`;
}
const 日付 = Utilities.formatDate(now, 'Asia/Tokyo', 'M/d');
const text = `☀️ フォーム回答レポート(${日付})\n・累計:${total}件(昨日 +${昨日}件)${内訳}`;
sendLineBroadcast(text, `【朝レポ】フォーム回答 累計${total}件(昨日+${昨日})`);
}
// 回答が来た“瞬間”の通知(記事02と同じもの・同梱)
function onFormSubmit(e) {
const sheet = e.range.getSheet();
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const values = e.values;
const total = sheet.getLastRow() - 1;
const lines = headers.map((h, i) => `${h}:${values[i] || ''}`);
sendLineBroadcast(`🔔 新しい回答が届きました(これで ${total} 件目)\n\n` + lines.join('\n'),
`【新着】フォーム回答が届きました(${total}件目)`);
}
// LINEへ送る(合鍵が空ならメールで送る)
function sendLineBroadcast(text, mailSubject) {
if (CHANNEL_ACCESS_TOKEN) {
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/broadcast', {
method: 'post',
contentType: 'application/json',
headers: { 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN },
payload: JSON.stringify({ messages: [{ type: 'text', text: text }] }),
muteHttpExceptions: true
});
} else {
MailApp.sendEmail(Session.getActiveUser().getEmail(), mailSubject || 'フォーム通知', text);
}
}
// 動作テスト:これを実行すると“今すぐ”朝レポが1通届く
function テスト送信() { 毎朝レポート(); }
// 最初に1回だけ実行:毎朝の自動実行+回答時の即時通知をまとめてセット
function 初期設定() {
ScriptApp.getProjectTriggers()
.filter(t => ['毎朝レポート', 'onFormSubmit'].includes(t.getHandlerFunction()))
.forEach(t => ScriptApp.deleteTrigger(t));
ScriptApp.newTrigger('毎朝レポート').timeBased().everyDays(1).atHour(通知時刻).create();
ScriptApp.newTrigger('onFormSubmit').forSpreadsheet(SpreadsheetApp.getActive()).onFormSubmit().create();
SpreadsheetApp.getActive().toast(`セットアップ完了!毎朝${通知時刻}時ごろにレポートが届きます。`);
}
このコードには、記事02の即時通知(onFormSubmit)も同梱してあります。だから記事02を使っている方も、遠慮なく丸ごと貼り替えてOK。「初期設定」を実行すると、古いトリガーを掃除したうえで、「毎朝レポート(時間ベース)」と「onFormSubmit(フォーム送信時)」の2つをまとめて張り直してくれます。即時通知と朝レポが、これ1本で両立します。
※本文のスクショとコードで行番号が少しずれて見える箇所がありますが、撮影後にコードを完全版へ改良したためです。掲載しているのが最新版で、動作はこの版で検証しています。
わたしがやらかした3つの事故(先回りしておきます)
きれいな手順だけ書いて終わるのは不誠実なので、わたしが実際に踏んだ地雷を3つ、先に共有します。
事故1:追記したら「関数の中」に貼ってしまった
最初、既存のコードに新しい関数を「追記」しようとして、初期設定関数の閉じカッコ } の手前に貼ってしまいました。すると追記した関数が「関数の中の関数」になってしまい、トリガーからも実行メニューからも見えなくなります。エラーすら出ないので、何が悪いのか分からず固まりました。
直し方はシンプルで、追記は必ず、いちばん下の } のさらに後ろ(=関数の外)に。実行メニューの関数一覧に追記したはずの関数名が出てこなかったら、まずこの事故を疑ってください。
事故2:丸ごと貼り替えたら、前のコードが消えた
次に、コードを丸ごと貼り替えたら、当然ながら記事02の即時通知(onFormSubmit)が消えました。ところが古いトリガーだけが、Apps Script側(時計アイコンの一覧)に残っていました。そのせいで回答が来るたびにエラーになり、「関数が見つかりません」のエラーメールがGoogleから届く、という気まずい状態に。
この記事のコードは、その反省から即時通知も同梱した完全版にしてあります。なので対処は「この記事のコードを丸ごと貼り替えて、初期設定をもう一度実行する」だけ。トリガーの掃除と張り直しまで、初期設定が面倒を見ます。
事故3:“完成イメージ図”を、注釈ごとコピペした
3つ目は、いちばん恥ずかしいやつです。AIとのチャットで見せられた「完成イメージはこんな感じです」という図を、行番号や注釈ごとコードエディタに貼ってしまったことがあります。当然動きません。コピペするのは「コードブロックの中身だけ」。この記事なら、上のグレーの枠の中身だけです。
まとめ:シートを開いて数える朝が消えた
わたしの朝から、「シートを開いて数える」という工程が消えました。外出先のポケットの中で1回震えて、画面を見れば、累計と昨日の増分と内訳が1通に並んでいる。「で、いま何件?」には、LINEを見せれば終わりです。
記事02と合わせると、役割分担はこうなります。
- 記事02:来た瞬間に気づく(即時通知・玄関チャイム)
- 記事03:毎朝の定点観測(まとめ・目覚まし時計)
数字の報告のために自分がカウンターになる必要は、もうありません。数字は、向こうから出勤してきます。
次回予告
次回は、「同じ連絡を、一人ひとり名前だけ変えて送る」あの地味に時間を食う作業——メールの差し込み一斉送信を自動化する予定です。
それでは、定時で帰りましょう。
—
このシリーズのこれまで:創刊号(入口)/記事01:フォーム回答をメールで受け取る/記事02:フォーム回答をLINEに即時通知


コメント