GASでGoogleフォーム打刻→残業・打刻漏れを自動検知 月¥0の勤怠管理アラート
「誰が退勤打刻を忘れたか」「今月あの人は残業何時間か」を月末にまとめて確認していませんか。Googleフォームでの打刻とGoogle Apps Scriptを組み合わせれば、打刻漏れと残業超過を毎日自動でチェックし、担当者にメールで知らせる仕組みを月¥0で作れます。
- Googleフォームで出勤・退勤を打刻するだけで、記録はスプレッドシートに自動蓄積される
- 毎日決まった時刻に、打刻漏れ(退勤忘れ)を自動検出してメールで通知
- 残業時間を自動集計し、月間の目安時間を超えたら早めにアラート
- 外部の勤怠管理クラウドサービスは不要。Google標準機能だけで月¥0から始められる
- 本記事のコードはそのままコピーして動く(打刻チェック+残業集計+メール通知)
- 設定は約45分、必要なのはGoogleアカウントのみ
編集長の見解:勤怠管理でありがちな失敗は「打刻漏れも残業超過も、月末にExcelを見て初めて気づく」ことです。気づいたときには本人の記憶も曖昧で、修正申請の往復に余計な時間がかかります。ここで重要なのは、高額な勤怠管理クラウドサービスに乗り換える前に、Googleが無料で提供している実行環境(GAS)で「毎日その場で気づける」仕組みに変えることです。従業員10〜20名規模なら、これだけで十分実用に耐えます。
🔍 なぜ勤怠チェックは「月末にまとめて」になりがちなのか
編集部のシミュレーションでは、従業員15名の小規模事業者が紙のタイムカードやExcelで勤怠を管理している場合、月末に「打刻漏れの確認」「残業時間の集計」「本人への確認連絡」に平均2〜3時間かかると試算しています。これは実在企業の実測値ではなく、一般的な作業内容から編集部が見積もったものです。
この作業が月末に集中する理由は、日々の打刻データを誰も毎日見ていないことにあります。
- 退勤打刻を忘れても、当日中に誰も気づかない
- 残業が重なっていても、本人も担当者も月末の集計まで把握できない
- 36協定(時間外労働の上限を労使で取り決める協定)の目安時間に近づいていることに、超過してから気づく
Google Apps Script(以下GAS)は、Googleアカウントに無料で付属するプログラム実行環境です。フォームの回答が記録されるスプレッドシートを毎日決まった時刻にチェックし、異常があれば即座にメールで知らせる処理を組めます。
- 費用: 個人向けGoogleアカウントでも無料で利用可能
- 人数: 従業員数が増えても課金は発生しない(スプレッドシートの行が増えるだけ)
- 依存度: 外部の勤怠クラウドサービスの契約・値上げに左右されない
必要なのは「フォームとスプレッドシートを整えてコードを貼る」という最初の一手間だけです。次のセクションで全体の流れを確認します。
🗺 全体フロー — 打刻からアラートまで自動化
構造はシンプルです。従業員がフォームで打刻すると回答がスプレッドシートに溜まり、GASが毎日決まった時刻にそれを読み取って、打刻漏れと残業超過をチェックしてメールを送ります。
打刻はフォーム入力のみ、チェックと通知はすべて自動
登場する部品を整理します。
| 部品 | 役割 | 公式ドキュメント |
|---|---|---|
| Googleフォーム | 出退勤の打刻入力。回答はスプレッドシートに保存する設定にする | Google フォームの使い方 |
| SpreadsheetApp | 打刻ログの読み取り、集計シートへの書き込み | SpreadsheetApp リファレンス |
| 時間主導型トリガー | 毎日決まった時刻にチェック処理を起動(エディタの画面から設定) | インストール型トリガーの設定ガイド |
| PropertiesService | 通知先メールアドレスなどの設定値をコードの外で保持 | PropertiesService リファレンス |
| MailApp | チェック結果をメールで担当者へ通知 | MailApp リファレンス |
| UrlFetchApp | (任意)Slackへの通知を送る | UrlFetchApp リファレンス |
これらをつなぐコードは、このあとのセクションでそのまま貼り付けられる形で公開します。まずは土台となるスプレッドシートとフォームの形を確認します。
💰 必要なものと費用の目安
追加の支出は基本的に発生しません。用意するものを表で確認してください。
| 必要なもの | 役割 | 費用の目安 |
|---|---|---|
| Googleアカウント(無料 or Workspace) | フォーム・スプレッドシート・GASの実行 | 無料(Workspaceは既存契約で可) |
| Googleフォーム | 出退勤の打刻入力画面 | 無料 |
| Google Apps Script | 打刻チェック・集計・メール送信の実行環境 | 無料 |
| (任意)Slack | メールに加えてチャットにも通知したい場合 | 無料プランで可 |
💡 編集部の判断材料:すでにSmartHRやマネーフォワードクラウド勤怠、KING OF TIMEなどの勤怠管理クラウドサービスを契約中なら、無理に乗り換える必要はありません。GASが向くのは「まずは打刻漏れと残業超過の“気づき”だけを自動化したい」「従業員数が少なく専用サービスの契約が割高に感じる」ケースです。人数が30名を超え、給与計算や有給管理まで一体で自動化したい段階になったら、専用クラウドサービスへの移行を検討する目安になります。
判断がついたら、実際の構築に進みます。
🛠 設定手順 — 約45分で完成
作業は「フォームとスプレッドシートの土台を作る」「GAS側でコードを貼ってトリガーを設定する」の2段階です。プログラミングの知識は不要で、コードは本記事のものをそのまま貼り付ければ動きます。
checkKintaiAndAlert を「時間主導型」→「日付ベースのタイマー」→希望の時間帯(例: 午後8時〜9時)で設定します。checkKintaiAndAlert を手動実行してメールの内容を確認します。当日分は実行のたびに上書きされる作りなので、動作確認で何度実行しても残業時間が水増しされることはありません。貼り付けるコード(そのまま動きます)
以下をApps Scriptエディタの コード.gs に貼り付けてください。毎日1回実行する checkKintaiAndAlert が本体です。
/**
* 「打刻ログ」シートを読み、当日分の打刻漏れ・退勤忘れ・残業超過をチェックして
* メールで通知する。時間主導型トリガー(日付ベースのタイマー)で毎日実行する想定。
* 同じ日に何度実行しても集計結果は変わらない(当日分を上書きする方式)。
*/
function checkKintaiAndAlert() {
const props = PropertiesService.getScriptProperties();
const notifyTo = props.getProperty('NOTIFY_EMAIL');
const overtimeAlertHours = Number(props.getProperty('OVERTIME_ALERT_HOURS') || '45');
if (!notifyTo) {
throw new Error('NOTIFY_EMAIL がスクリプトプロパティに未設定です');
}
const ss = SpreadsheetApp.getActiveSpreadsheet();
const logSheet = ss.getSheetByName('打刻ログ');
const rosterSheet = ss.getSheetByName('従業員名簿');
if (!logSheet) throw new Error('「打刻ログ」シートが見つかりません。シート名を確認してください');
if (!rosterSheet) throw new Error('「従業員名簿」シートが見つかりません。シート名を確認してください');
const summarySheet = getOrCreateSummarySheet_(ss);
// 日付も曜日もすべて日本時間で判定する(プロジェクトのタイムゾーン設定に左右されないようにするため)
const today = new Date();
const todayStr = Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy-MM-dd');
const monthKey = todayStr.slice(0, 7); // 'yyyy-MM'
// 土日はスキップ(就業カレンダーが必要な場合はここを拡張する)
const dow = new Date(todayStr + 'T00:00:00Z').getUTCDay(); // 0=日曜, 6=土曜
if (dow === 0 || dow === 6) return;
// 従業員名簿を取得(1行目は見出し行として読み飛ばす)
const rosterLastRow = rosterSheet.getLastRow();
const roster = rosterLastRow > 1
? rosterSheet.getRange(2, 1, rosterLastRow - 1, 1)
.getValues().map((r) => String(r[0]).trim()).filter(String)
: [];
// 打刻ログから当日分を抽出し、氏名ごとに出勤/退勤の最早・最遅時刻を集計
const data = logSheet.getDataRange().getValues();
// 列: 0=タイムスタンプ 1=氏名 2=種別(Googleフォームのデフォルト列順)
const todayPunches = {}; // { 氏名: { in: Date|null, out: Date|null } }
for (let i = 1; i < data.length; i++) {
const timestamp = data[i][0];
if (!timestamp) continue;
const name = String(data[i][1] || '').trim();
const type = String(data[i][2] || '').trim();
if (!name) continue;
const punchTime = new Date(timestamp);
const dateStr = Utilities.formatDate(punchTime, 'Asia/Tokyo', 'yyyy-MM-dd');
if (dateStr !== todayStr) continue;
if (!todayPunches[name]) todayPunches[name] = { in: null, out: null };
if (type === '出勤') {
if (!todayPunches[name].in || punchTime < todayPunches[name].in) todayPunches[name].in = punchTime;
} else if (type === '退勤') {
if (!todayPunches[name].out || punchTime > todayPunches[name].out) todayPunches[name].out = punchTime;
}
}
const noPunch = []; // 出勤・退勤とも記録なし
const missingOut = []; // 出勤はあるが退勤記録なし(退勤忘れの疑い)
const overtimeToday = []; // 本日残業が発生した人(参考情報)
const overLimit = []; // 月間累計が目安時間を超えた人
roster.forEach((name) => {
const punch = todayPunches[name];
if (!punch || (!punch.in && !punch.out)) {
noPunch.push(name);
return;
}
if (punch.in && !punch.out) {
missingOut.push(name);
return;
}
if (punch.in && punch.out) {
const workedMs = punch.out.getTime() - punch.in.getTime();
let workedHours = workedMs / (1000 * 60 * 60);
// 労働基準法第34条: 6時間超で45分、8時間超で60分の休憩を控除する目安
if (workedHours > 8) workedHours -= 1;
else if (workedHours > 6) workedHours -= 0.75;
const overtimeHoursToday = Math.max(0, workedHours - 8);
if (overtimeHoursToday > 0) {
overtimeToday.push(name + '(本日 約' + overtimeHoursToday.toFixed(1) + '時間)');
}
const cumulative = upsertDailyOvertime_(summarySheet, name, todayStr, monthKey, overtimeHoursToday);
if (cumulative >= overtimeAlertHours) {
overLimit.push(name + '(今月累計 約' + cumulative.toFixed(1) + '時間)');
}
}
});
if (noPunch.length === 0 && missingOut.length === 0 && overLimit.length === 0) {
return; // 異常なしの日はメールを送らない
}
const bodyLines = [
todayStr + ' の勤怠チェック結果です。',
'',
'■ 本日打刻記録なし(' + noPunch.length + '名): ' + (noPunch.join('、') || 'なし'),
'■ 退勤打刻の記録なし(' + missingOut.length + '名): ' + (missingOut.join('、') || 'なし'),
'■ 今月の残業が目安(' + overtimeAlertHours + '時間)に達した人(' + overLimit.length + '名): ' + (overLimit.join('、') || 'なし'),
'',
'[参考]本日残業が発生した人(' + overtimeToday.length + '名): ' + (overtimeToday.join('、') || 'なし'),
'',
'※目安時間は36協定の原則的な上限(月45時間)を参考にした設定値です。実際の協定内容に合わせてスクリプトプロパティで調整してください。',
];
const body = bodyLines.join('\n');
MailApp.sendEmail(notifyTo, '【勤怠アラート】' + todayStr + ' の確認事項', body);
const slackWebhook = props.getProperty('SLACK_WEBHOOK_URL');
if (slackWebhook) {
postToSlack_(slackWebhook, body);
}
}
/**
* 「残業集計」シートの 氏名×日付 の行を上書き更新し、同じ月の合計残業時間を返す。
* 加算ではなく上書きなので、同じ日に何度実行しても二重計上されない。
*/
function upsertDailyOvertime_(summarySheet, name, dateStr, monthKey, hoursToday) {
const data = summarySheet.getDataRange().getValues();
let matchedRow = 0;
let monthlyTotal = 0;
for (let i = 1; i < data.length; i++) {
if (String(data[i][0]).trim() !== name) continue;
const rowDate = normalizeDateCell_(data[i][1]);
if (rowDate === dateStr) {
matchedRow = i + 1; // 実際のシート行番号(見出し行の分だけずれる)
data[i][2] = hoursToday; // 当日分は最新の計算結果で置き換える
}
if (rowDate.slice(0, 7) === monthKey) {
monthlyTotal += Number(data[i][2] || 0);
}
}
if (matchedRow) {
summarySheet.getRange(matchedRow, 3).setValue(hoursToday);
} else {
summarySheet.appendRow([name, dateStr, hoursToday]);
monthlyTotal += hoursToday;
}
return monthlyTotal;
}
/**
* 日付列の値を 'yyyy-MM-dd' の文字列に揃える。
* スプレッドシートは '2026-07-30' のような文字列を日付型として保存し直すことがあり、
* そのまま文字列比較すると一致せず、毎日新しい行が増えて累計が正しく出なくなるため。
*/
function normalizeDateCell_(value) {
if (value instanceof Date) {
return Utilities.formatDate(value, 'Asia/Tokyo', 'yyyy-MM-dd');
}
return String(value).trim();
}
/**
* 「残業集計」シートを取得、なければヘッダー付きで新規作成する。
*/
function getOrCreateSummarySheet_(ss) {
let sheet = ss.getSheetByName('残業集計');
if (!sheet) {
sheet = ss.insertSheet('残業集計');
sheet.appendRow(['氏名', '日付', '当日の残業時間']);
}
return sheet;
}
/**
* Slack Incoming Webhook にテキストを投稿する(SLACK_WEBHOOK_URL 未設定時は呼ばれない)。
*/
function postToSlack_(webhookUrl, text) {
UrlFetchApp.fetch(webhookUrl, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ text: text }),
muteHttpExceptions: true,
});
}
スクリプトプロパティの登録(コードに直書きしない)
通知先メールアドレスなどは、コードに直接書かず「スクリプトプロパティ」に保存します。
- Apps Scriptエディタ左の**歯車アイコン(プロジェクトの設定)**を開く
- 最下部の「スクリプト プロパティ」で「スクリプト プロパティを追加」をクリック
- 以下の3件を登録する(
SLACK_WEBHOOK_URLは任意)
| プロパティ名 | 値の例 | 必須 |
|---|---|---|
NOTIFY_EMAIL | roumu@example.com | 必須 |
OVERTIME_ALERT_HOURS | 45 | 任意(未設定時は45) |
SLACK_WEBHOOK_URL | Slack Incoming Webhook の URL | 任意 |
💡 編集部メモ:「本日打刻記録なし」は有給・休日出勤なしの従業員も含まれます。誤検知が気になる場合は、従業員名簿シートに「本日出勤予定」列を追加し、コードの判定条件に組み込むと精度が上がります。まずはシンプルな形で運用を始め、現場の実態に合わせて拡張するのが現実的です。
保存とトリガー設定が終わったら、次のセクションでつまずきやすい点を確認します。
⚠️ 失敗パターンと運用上の注意
GASは無料で強力ですが、勤怠データは労務管理に直結するため、通知系のレシピよりも慎重な運用が必要です。編集部が特に注意すべきと考える2点を挙げます。
⚠️ 失敗パターン1:半休・遅刻・出張などの例外を考慮せず誤検知が量産される。本記事のコードは「出勤→退勤」のシンプルな1日1組を前提にしています。休憩を挟んで一度退勤打刻をするような運用や、直行直帰の営業担当がいる場合はそのままでは誤検知が増えます。まずは事務所勤務者など運用がシンプルなグループに絞って試験導入し、例外パターンが見えてから条件分岐を追加してください。
⚠️ 失敗パターン2:タイムゾーンのずれで日付や曜日の判定がずれる。日付の境界を決めているのはApps Script側のタイムゾーン(エディタの歯車アイコン →「プロジェクトの設定」)であり、スプレッドシート側の設定(ファイル > 設定)ではありません。新規プロジェクトの初期値は日本時間ではないため、ここが未設定のままだと「月曜なのに土日と判定されてスキップされる」といったずれが起こります。STEP 4 で必ず日本標準時に変更してください(本記事のコードは日付・曜日判定を日本時間に固定しているため、設定を直せばずれません)。スプレッドシート側の設定は画面表示用として、あわせて「日本」に揃えておくと混乱がありません。
初回実行時には、Googleから「このアプリは確認されていません」という趣旨の警告が出ます。これは自分で書いたスクリプトに、自分のデータへのアクセスを許可するための画面です。詳細を開いてプロジェクト名が自分のものであることを確認し、承認すれば先へ進めます。
| トラブル | 起こりやすい原因 | 編集部の対策 |
|---|---|---|
| メールが届かない | NOTIFY_EMAIL の未設定・入力ミス | スクリプトプロパティの値を再確認 |
| 全員が「打刻記録なし」になる | 「従業員名簿」の氏名と「打刻ログ」の氏名表記が不一致 | 全角/半角・姓名の間のスペース等を統一 |
| 残業時間が実態と合わない | 休憩控除ルールが実際の就業規則と異なる | コード中の休憩控除(45分/60分)の分岐を自社規定に合わせて調整 |
| Slack通知だけ届かない | SLACK_WEBHOOK_URL のURLが誤り、または未発行 | Slack側でIncoming Webhookを再発行し貼り直す |
なおGASには1日あたりの上限がありますが、本レシピの負荷は「1日1通のメール・数秒の実行」なので、通常の従業員規模では届きません。公式の割り当て表で確認できる主な数値は次のとおりです。
| 項目 | 無料のGoogleアカウント | Google Workspace | 本レシピの使用量 |
|---|---|---|---|
| メール送信先 | 100件/日 | 1,500件/日 | 1日1通 |
| トリガーの合計実行時間 | 90分/日 | 6時間/日 | 1日数秒 |
| 1回あたりの実行時間 | 6分/回 | 6分/回 | 数秒 |
| 外部通信(URL Fetch)の回数 | 20,000回/日 | 100,000回/日 | 0〜1回/日(Slack通知時のみ) |
数値はApps Scriptの割り当てと制限(公式)に基づきます。次のセクションで、この45分の投資が経営上どう返ってくるかを整理します。
🚀 経営者はこの45分をどう回収するか
編集部の想定シナリオを1つ挙げます。従業員15名の整体院チェーンが、紙のタイムカードと目視確認で勤怠を管理しているケースです(実在の企業の事例ではありません)。
- 打刻漏れの洗い出し(月末に約1時間)
- 残業時間の手計算(約1時間)
- 本人への確認連絡(約30分〜、記憶が曖昧で難航しがち)
- 打刻漏れは当日中にメールで判明
- 残業累計は自動集計、目安超過も自動検知
- 月末の確認作業は最終チェックのみ(約20分)
月あたり約2時間分の確認作業を圧縮する編集部の試算
かかった費用は¥0、かかった時間は初回設定の約45分だけです。基本形が動いたら、次の応用も同じコードの延長で組めます。
- 有給・休日出勤の判定追加: 従業員名簿シートに「本日の勤務予定」列を足し、誤検知をさらに減らす
- 給与計算との連携: 「残業集計」シートの値を給与計算担当者が参照する運用にすれば、集計の二度手間がなくなる
- 入社手続きの自動化とあわせて: 打刻対象者の追加・削除が多い場合はZapierで採用〜入社手続きを自動化するレシピも参考になります
💡 編集部おすすめ:36協定の上限規制や休憩時間のルールなど、労務管理の基礎知識を体系的に押さえておきたい方には、実務書での学習もおすすめです。
Amazon で「労務管理 実務 中小企業」の書籍を探す → ※ PR・アフィリエイトリンクを含みます
当サイトでは、労務・勤怠まわりの自動化を複数紹介しています。あわせて読むと設計の引き出しが増えます。
- Google Apps ScriptでGoogleフォーム回答をSlack即時通知する無料レシピ
- GASでスプレッドシート→請求書PDF自動生成→メール送信するレシピ
- Zapierで採用〜入社手続きを自動化する中小企業人事向けレシピ
- Power Automateで承認ワークフローをTeamsで完結させるレシピ
編集部のまとめ:このレシピの本質は「打刻漏れと残業超過に、当日気づける仕組みに変える」ことです。GASを選べば、その仕組みを月¥0で、従業員数が増えても値上がりしない形で持てます。まずは事務所勤務者など運用がシンプルな一部のメンバーで試験導入し、例外パターンを洗い出したうえで対象を広げるのが堅実な順序です。なお本記事のコードが数えているのは「1日8時間を超えた分」の積み上げであり、週40時間を超えた分などは含みません。あくまで早めに気づくための目安として使い、36協定の目安時間や休憩時間のルールは実際の就業規則・労使協定と必ず突き合わせてください。最終的な労務判断は社会保険労務士等の専門家にも確認することをおすすめします。
出典・参考情報
- Google Apps Script 公式サイト
- SpreadsheetApp リファレンス
- MailApp リファレンス
- PropertiesService リファレンス
- インストール型トリガーの設定ガイド
- Apps Scriptの割り当てと制限
- Slack Incoming Webhook 公式ドキュメント
- 休憩時間は法律で決まっていますか(厚生労働省)
- 時間外労働の上限規制 わかりやすい解説(厚生労働省)
Mira / AI経営ラボ 編集長