Blog / Construction Operations

Equipment Maintenance Reminders from a Google Sheet

By Keyona Meeks, Founder, ReRev Labs · Oct 31, 2026 · 9 min read

The short answer: Equipment gets serviced late when the due date lives in someone's head. Keep one row per machine in a Google Sheet: the last service date and how many days between services. A formula gives the next due date and a status. A short Apps Script emails you what is past due or due within a window you choose. How often to service a machine comes from its maker and your safety professional, not from this sheet. I give no intervals. I tested the script with a mock in Node and the formulas in LibreOffice. Nothing was run in Google.

By the end of this post, you'll be able to:

  • Set up an equipment log with a next-due date and a status.
  • Get a daily email of what is past due or due soon.
  • Know what this log cannot do.

I sell build work, so weigh this post with that in mind. This is a tracking aid, not safety advice. Check with an attorney, insurer or safety professional before you rely on this sheet for any safety, contract or legal question.

What rules cover equipment maintenance?

I looked at OSHA's page for 29 CFR 1926.600, "Equipment." As the tool returned it, paragraph (a)(1) is about lights or reflectors on equipment left unattended at night, and (a)(3)(i) says blades, buckets and dump bodies must be lowered or blocked when being repaired or when not in use. The tool found no paragraph in that section that mentions inspection, maintenance or manufacturer's recommendations. I read only that section. Other parts of the construction standards or other rules may cover your equipment. I make no claim about what applies to you.

Service intervals usually come from the maker's manual and your own safety program. I do not know yours, so the interval column below is empty on purpose. Ask a safety professional.

How do you set up the log?

Row 1 holds headings. Use a tab named Equipment.

ColumnHeadingWhat goes in it
AEquipmentThe machine, such as "Loader 2"
BLast serviceThe date of the last service
CInterval daysDays between services, from your source
DNext dueFormula
EStatusFormula

Put these in row 2 and fill them down:

D2: =IF(OR(B2="",C2=""),"",B2+C2)
E2: =IF(A2="","",IF(D2="","Missing data",IF(D2<TODAY(),"Past due "&(TODAY()-D2)&" days",IF(D2-TODAY()<=14,"Due soon","OK"))))

The 14 in the status formula is the warning window. It is my example, and the script has its own WARN_DAYS setting. Keep the two the same.

I tested these in LibreOffice with five made-up rows: past due by 10 days, due in a few days, comfortably ahead, no last-service date and no interval. They returned "Past due 10 days," "Due soon," "OK," "Missing data" and "Missing data." I did not run them in Google Sheets. If a result shows as a date, set that cell's format to number. I could not confirm the menu steps.

This log counts days only. Machines serviced by hours of use need a different column, and I do not cover that.

What does the script look like?

Paste this into the Apps Script project attached to the sheet. It reads the tab and changes nothing. Nothing in it runs until you call it.

// Emails equipment that is past its next service date or due within a window. It reads the "Equipment" tab and changes nothing.
var RECIPIENT = '';       // Paste the address that should get the list. Your decision.
var SEND_HOUR = 7;        // Hour of day, in the script time zone.
var TAB = 'Equipment';
var WARN_DAYS = 14;       // Also list items due within this many days. Your decision.
var MAX_LINES = 40;       // Most lines per section.
var HEADERS = {           // Change the text on the right to match YOUR column headings.
  item: 'Equipment', due: 'Next due'
};

function dayKey(value, tz) {
  if (value === '' || value === null || value === undefined) return '';
  if (Object.prototype.toString.call(value) === '[object Date]') return Utilities.formatDate(value, tz, 'yyyy-MM-dd');
  return String(value).slice(0, 10);
}

function dayNumber(key) {
  var p = key.split('-');
  return Math.round(Date.UTC(Number(p[0]), Number(p[1]) - 1, Number(p[2])) / 86400000);
}

function findColumns(headerRow) {
  var idx = {};
  Object.keys(HEADERS).forEach(function (k) {
    var at = headerRow.map(function (h) { return String(h).trim(); }).indexOf(HEADERS[k]);
    if (at === -1) throw new Error('Missing column in ' + TAB + ': ' + HEADERS[k]);
    idx[k] = at;
  });
  return idx;
}

function sortRows(rows, idx, todayKey, tz) {
  var overdue = [];
  var soon = [];
  var noDate = 0;
  var today = dayNumber(todayKey);
  rows.forEach(function (r) {
    if (String(r[idx.item]).trim() === '') return;
    var key = dayKey(r[idx.due], tz);
    if (key === '' || isNaN(dayNumber(key))) { noDate++; return; }
    var left = dayNumber(key) - today;
    if (left < 0) overdue.push({ item: r[idx.item], due: key, left: left });
    else if (left <= WARN_DAYS) soon.push({ item: r[idx.item], due: key, left: left });
  });
  overdue.sort(function (a, b) { return a.left - b.left; });
  soon.sort(function (a, b) { return a.left - b.left; });
  return { overdue: overdue, soon: soon, noDate: noDate };
}

function section(title, list, wording) {
  var lines = [title];
  if (list.length === 0) lines.push('None.');
  list.slice(0, MAX_LINES).forEach(function (o) { lines.push(o.item + '  (due ' + o.due + ', ' + wording(o.left) + ')'); });
  if (list.length > MAX_LINES) lines.push('Plus ' + (list.length - MAX_LINES) + ' more.');
  return lines;
}

function buildEmail(result, todayKey) {
  var lines = ['Equipment service list, as of ' + todayKey, ''];
  lines = lines.concat(section('PAST DUE', result.overdue, function (n) { return (-n) + ' days late'; }));
  lines.push('');
  lines = lines.concat(section('DUE WITHIN ' + WARN_DAYS + ' DAYS', result.soon, function (n) { return n + ' days left'; }));
  lines.push('', 'Rows with no usable next-due date: ' + result.noDate);
  return lines.join('\n');
}

function sendServiceList() {
  if (!RECIPIENT) throw new Error('Set RECIPIENT first.');
  if (MailApp.getRemainingDailyQuota() < 1) throw new Error('No email quota left today.');
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TAB);
  if (!sheet) throw new Error('Missing tab: ' + TAB);
  var values = sheet.getDataRange().getValues();
  if (values.length === 0) throw new Error('The ' + TAB + ' tab is empty.');
  var idx = findColumns(values[0]);
  var tz = Session.getScriptTimeZone();
  var todayKey = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd');
  var result = sortRows(values.slice(1), idx, todayKey, tz);
  MailApp.sendEmail(RECIPIENT, 'Equipment service: ' + result.overdue.length + ' past due, ' + result.soon.length + ' soon', buildEmail(result, todayKey));
  return result;
}

function installTrigger() {
  ScriptApp.newTrigger('sendServiceList')
    .timeBased()
    .everyDays(1)
    .atHour(SEND_HOUR)
    .inTimezone(Session.getScriptTimeZone())
    .create();
}

What the parts do, with Google's own descriptions as I read them in the method tables this session:

  • getActiveSpreadsheet(): "Returns the currently active spreadsheet, or null if there is none." getSheetByName(name): "Returns a sheet with the given name." getDataRange(): "Returns a Range corresponding to the dimensions in which data is present." getValues(): "Returns the rectangular grid of values for this range."
  • Utilities.formatDate "Formats date according to specification described in Java SE SimpleDateFormat class." Session.getScriptTimeZone() "Gets the time zone of the script."
  • MailApp.sendEmail(recipient, subject, body) "Sends an email message." getRemainingDailyQuota() "Returns the number of recipients you can send emails to for the rest of the day."
  • everyDays(n) "Specifies to run the trigger every n days." atHour(hour) "Specifies the hour the trigger at which the trigger runs." inTimezone(timezone) "Specifies the timezone for the specified dates/time when the trigger runs."

The script reads the Next due column. Because that column holds a formula, the script sees the value the formula produced. It lists past-due items first, then items due within WARN_DAYS, and counts rows with no usable date. It sends the email even when both lists are empty, so you know it ran.

Who should get the email?

You decide. RECIPIENT is a paste value, and the script will not run until you set it. Ask the people involved before you add them.

How do you put it on a daily schedule?

Run installTrigger once and check your project's triggers list for exactly one entry. I could not confirm which minute of the hour a trigger fires, so do not expect 7:00 sharp. Per an earlier review of Google's triggers page, installable triggers "always run under the account of the person who created them." Per an earlier review of Google's authorization page, scopes are "determined automatically by scanning the script code." Read the permission prompt before you approve it.

How do you test it?

  1. Enter five test lines: one past due, one due in 5 days, one due in 30 days, one with no date and one due today.
  2. Check the Next due and Status columns by hand.
  3. Set RECIPIENT to your own address and run sendServiceList. The past-due and soon lines should appear, and the 30-day line should not.
  4. Change a heading by one letter. You should see the missing-heading message. Fix it.
  5. Run installTrigger and check the triggers list, then delete the test lines.

What did the test show?

I tested the script in Node with a mock of the Google services. All 14 checks passed. They covered past-due and soon counts, the order of each list, the window edge at 14 and 15 days, a due-today line, date cells and text dates, the no-date count, empty lists, a missing heading, an empty tab, a blank recipient, a used-up quota, a missing tab, the 40-line cap and the trigger chain. The mock cannot show how Google returns your dates, so steps 1 to 3 matter.

Where does an LLM fit in this?

An LLM can help with the messy input. For example, it can read a maker's manual and suggest the service intervals it lists. A person must check each interval against the manual, because an LLM can pick the wrong model or the wrong table and sound sure. The date math and the email are plain rules. My stance: AI writes the code, plain automation runs it, and an LLM shows up only where the input is messy and a person checks the result. For where to start with your own automations, see what to automate first.

Where this has limits

  • This is a tracking aid, not safety advice. Check with an attorney, insurer or safety professional before you rely on this sheet for any safety, contract or legal question.
  • I give no service intervals. The maker's manual and your safety program decide.
  • I read one OSHA section. It did not mention inspection or maintenance. Other rules may apply.
  • It counts days, not hours of use.
  • It shows what someone typed. It does not know whether a service was done well.
  • Nothing was run in Google. The script test was a mock in Node. The formula test was in LibreOffice.
  • Email quotas apply. I quote the quota call above. Check Google's limits for your account type.
  • No ReRev prices and no promises about results.

If you want help turning this into an equipment system for your team, see our AI operations service or tell us how you track equipment now. Do not send manuals, passwords or login details.

Frequently asked questions

Can I track equipment maintenance in Google Sheets?

Yes, as a tracking aid. One row per machine, a last-service date and an interval give a next-due date.

How often should equipment be serviced?

The maker's manual and your safety program decide. I do not know your equipment.

Does the sheet send reminders?

The script emails a daily list of items past due or due soon, after you install the trigger. Test it first.

Does it track hours of use?

No. It counts days.

Does OSHA 1926.600 require maintenance logs?

In the section text I read, I found no mention of inspection or maintenance. Ask a safety professional what applies to you.

Does it use an LLM?

Not in the script. An LLM can help read a manual, and a person checks it.

Read next: The paperwork map shows where equipment reminders sit with the rest of the office and field paper.