Overtime risk should surface before payroll closes. A weekly rollup with one email per employee-week keeps noise down.
overtimeAlert aggregates hours by employee|ISO-week, and when totals exceed OT_LIMIT sends MailApp to MANAGER.
Column D stores the weekKey after alerting so repeats do not spam.
Adjust OT_LIMIT for local labor rules; this is not legal advice.
Need this built? Hire a Google Apps Script developer →
Sheet / project setup
| Sheet | Column | Purpose |
|---|---|---|
| Timesheets | A employee | Worker key |
| Timesheets | B date | Work date |
| Timesheets | C hours | Decimal hours |
| Timesheets | D alertedWeekKey | Dedupe marker |
| OT_LIMIT | 40 | Weekly threshold |
| MANAGER | managers@… | Alert destination |
What this script does
Weekly overtime detection with one email per employee-week.
Prerequisites
Hours calculated; MailApp auth; manager inbox.
- Date column real Dates
- OT_LIMIT set
- Run after hours calc
Walkthrough
Create >40 hours in one ISO week; run; receive one email; rerun silent.
Edge cases
ISO week boundaries may differ from your payroll week — swap isoWeekKey_ if needed.
- alertedWeekKey prevents spam
- Multiple rows share one email
How to test
Two employees over limit → two emails.
Hardening for production
CC employee; write OvertimeLog; use Chat.
Variations
Daily OT over 8 hours instead of weekly 40.
Full code: overtimeAlert()
Run timesheetHoursCalculation first, then overtimeAlert() on a daily trigger near week end.
/**
* Email managers when an employee exceeds weekly overtime hours.
*/
const TS = "Timesheets"; // A employee, B date, C hours, D alertedWeekKey
const OT_LIMIT = 40;
const MANAGER = "managers@example.com";
function overtimeAlert() {
const sheet = SpreadsheetApp.getActive().getSheetByName(TS);
const values = sheet.getDataRange().getValues();
const totals = {};
const weekKeys = {};
for (let i = 1; i < values.length; i++) {
const emp = values[i][0];
const dt = values[i][1];
const hours = Number(values[i][2]) || 0;
if (!(dt instanceof Date)) continue;
const weekKey = emp + "|" + isoWeekKey_(dt);
totals[weekKey] = (totals[weekKey] || 0) + hours;
weekKeys[i] = weekKey;
}
const alerted = {};
for (let i = 1; i < values.length; i++) {
const weekKey = weekKeys[i];
if (!weekKey) continue;
if (totals[weekKey] <= OT_LIMIT) continue;
if (values[i][3] === weekKey) continue; // row already marked for this week alert
if (alerted[weekKey]) {
values[i][3] = weekKey;
continue;
}
const emp = values[i][0];
MailApp.sendEmail(
MANAGER,
"Overtime alert: " + emp,
emp + " has " + totals[weekKey].toFixed(2) + " hours in week " + weekKey.split("|")[1] +
" (limit " + OT_LIMIT + ")."
);
alerted[weekKey] = true;
values[i][3] = weekKey;
}
sheet.getRange(1, 1, values.length, values[0].length).setValues(values);
}
function isoWeekKey_(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
return d.getUTCFullYear() + "-W" + String(weekNo).padStart(2, "0");
}- Line 5: Weekly hours threshold.
- Line 19: Builds an employee-independent week id.
- Line 20: Aggregates hours per employee-week.
- Line 35: Manager notification.
- Line 4: Column D dedupe stamp.
- Line 30: Ensures one email even across many rows.
Deploy this example
- 01
Open Apps Script
In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.
- 02
Paste and save
Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.
- 03
Authorize once
Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.
- 04
Add the trigger
Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.
Before you run: overtime alert
- 1Hours column populated
- 2OT_LIMIT matches policy
- 3MANAGER address monitored
- 4Payroll week definition checked vs ISO
- 5MailApp authorized
- 6Dedupe column blank for new weeks
Frequently asked questions
Replace isoWeekKey_ with a custom week-start calculator.
Aggregate by employee|date and compare to 8.
Filter employees via a roster sheet before totaling.
So any row in that week shows the alert was sent.
Unnecessary if weekKey changes each week.
toFixed(2) is display-only in the email; totals use raw numbers.
One mail per over-limit employee-week is usually fine.