How to Track Equipment and Vehicle Maintenance in Google Sheets (with Due-Date Email Reminders, 2026)

Oil changes, filter swaps, fire-extinguisher inspections, HVAC service, forklift checks: most small fleets and shops track them in a spreadsheet, and the sheet only helps if someone remembers to look at it. This guide sets up a maintenance sheet where logging a service moves the next due date forward on its own, handles tasks that are due by mileage or engine hours as well as by date, and emails the person responsible before anything is due.

It differs from an expiration tracker like the license and certification tracker in one big way: maintenance repeats forever. The due date isn't typed once; it's always “last done plus the interval”, so the sheet's job is to keep that math right.

Step 1: a Maintenance tab with one row per asset per task

Create a tab named Maintenance with these headers in row 1. The script below finds columns by name, but the formulas in Step 2 assume this order (A to M):

ColHeaderWhat goes in it
AAssete.g. “Van 2”, “Compressor”, “Walk-in cooler”
BTaske.g. “Oil change”, “Replace filter”, “Annual inspection”
CLocationWhere the asset is (optional)
DEvery (days)The interval in days, e.g. 90 for quarterly, 365 for yearly. Blank if the task only goes by mileage.
ELast doneDate of the last service (typed, or pulled from the log in Step 2)
FNext dueA formula (Step 2), formatted as a date
GOwner emailWho should get the reminder
HRemind daysComma-separated days before the due date, e.g. 14,3,0. Blank uses the default.
IStatusLeave blank while in service; Retired, Paused or Sold stops reminders
JEvery (miles)Optional mileage (or engine-hour) interval, e.g. 5000
KLast done milesOdometer or hour-meter reading at the last service
LCurrent milesLatest reading; update it weekly or from a form
MSent logLeave empty; the script records what it sent

One asset with four recurring jobs gets four rows. That's what lets the oil change run every 5,000 miles while the same van's registration runs every 365 days.

Step 2: next due dates that roll forward

In F2 (Next due), fill down:

=IF(OR(D2="",E2=""),"",E2+D2)

Add a Days left column if you like, =IF(F2="","",F2-TODAY()), plus conditional formatting on the data range with custom formulas =AND($F2<>"",$F2<TODAY()) (red, overdue) and =AND($F2>=TODAY(),$F2-TODAY()<=14) (amber, due soon).

Better: a service log that updates Last done for you. Make a second tab named Log with headers Date, Asset, Task, Miles, Notes, Done by, and add a row every time work is done. A Google Form linked to the sheet works well for techs on a phone, as long as its Asset and Task answers match the Maintenance tab exactly (use dropdown questions). Then replace the typed values in E and K with lookups that take the latest entry:

E2: =LET(d,MAXIFS(Log!$A:$A,Log!$B:$B,$A2,Log!$C:$C,$B2),IF(d=0,"",d))
K2: =LET(m,MAXIFS(Log!$D:$D,Log!$B:$B,$A2,Log!$C:$C,$B2),IF(m=0,"",m))

Now logging a service is the only thing anyone has to do: Last done updates, Next due moves forward, and the reminder history for the old due date stops applying. You also get a full service history for warranty claims, resale or inspections, which a single “last done” cell can't give you.

For a “due in the next 30 days” view on another tab, sorted by date:

=QUERY(Maintenance!A:M,"select A,B,C,F,G where F is not null and F <= date '"&TEXT(TODAY()+30,"yyyy-mm-dd")&"' order by F",1)

Step 3: mileage and engine hours

Some tasks go by use, not by the calendar. With J, K and L filled in, miles left until the next service is:

=IF(OR(J2="",K2="",L2=""),"",K2+J2-L2)

Many manufacturers say “every 5,000 miles or 6 months, whichever comes first”. Fill in both D and J for those rows; the script below checks both and reminds on whichever comes first. The same columns work for engine hours on generators, mowers or forklifts; change the unit label in the script.

Step 4: email the owner before service is due

Google Sheets won't send an email when a date arrives on its own; its notification rules react to edits, not to the calendar. A short Apps Script with a daily trigger does it. Open Extensions › Apps Script, paste this, save, run sendMaintenanceReminders once to approve permissions, then add a trigger: Triggers › Add trigger › sendMaintenanceReminders › Time-driven › Day timer.

/**
 * Daily maintenance reminders from a "Maintenance" tab.
 * Headers used (any order): Asset, Task, Next due, Owner email, Sent log.
 * Optional: Remind days (e.g. "14,3,0"), Status, Location,
 * Every (miles), Last done miles, Current miles (for mileage/hours-based tasks).
 */
const SHEET_NAME   = 'Maintenance';
const DEFAULT_DAYS = [14, 3, 0];
const MILES_WARN   = 500;          // remind when this close to the next service
const UNIT         = 'miles';      // or 'hours' for engine-hour meters
const STOP_STATUS  = /^(retired|paused|sold)$/i;

function sendMaintenanceReminders() {
  const sh   = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  const rows = sh.getDataRange().getValues();
  const head = rows[0].map(String);
  const col  = (name) => head.indexOf(name);
  const c = {
    asset: col('Asset'), task: col('Task'), due: col('Next due'),
    email: col('Owner email'), days: col('Remind days'), status: col('Status'),
    where: col('Location'), everyMi: col('Every (miles)'),
    lastMi: col('Last done miles'), curMi: col('Current miles'), log: col('Sent log'),
  };
  if (c.email === -1 || c.log === -1 || (c.due === -1 && c.curMi === -1)) {
    throw new Error('Need Owner email, Sent log, and Next due or Current miles columns.');
  }
  const tz    = Session.getScriptTimeZone();
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const get   = (row, i) => (i === -1 ? '' : row[i]);

  for (let r = 1; r < rows.length; r++) {
    const row = rows[r];
    if (!row[c.email] || STOP_STATUS.test(String(get(row, c.status)).trim())) continue;
    const sent  = String(row[c.log] || '').split(';').filter(String);
    const notes = [];
    const what  = `${get(row, c.asset)}: ${get(row, c.task)}`;

    // 1) Date-based: Next due
    const due = get(row, c.due);
    if (due instanceof Date) {
      const d = new Date(due); d.setHours(0, 0, 0, 0);
      const left = Math.round((d - today) / 86400000);
      const key  = Utilities.formatDate(due, tz, 'yyyy-MM-dd');
      const custom = String(get(row, c.days)).split(',')
        .map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
      const days = custom.length ? custom : DEFAULT_DAYS;
      // Largest reminder day already reached that hasn't been sent for this due date.
      const hit = days.filter((n) => left <= n).sort((a, b) => a - b)[0];
      if (hit !== undefined && !sent.includes(`${key}@${hit}`)) {
        notes.push(left < 0 ? `overdue since ${key} (${-left} days)`
                 : left === 0 ? `due today (${key})` : `due ${key} (${left} days)`);
        sent.push(`${key}@${hit}`);
      }
    }

    // 2) Mileage/hours-based: Last done miles + Every (miles)
    const every = Number(get(row, c.everyMi)), last = Number(get(row, c.lastMi));
    const cur   = Number(get(row, c.curMi));
    if (every > 0 && get(row, c.lastMi) !== '' && get(row, c.curMi) !== '') {
      const nextAt = last + every, milesLeft = nextAt - cur;
      if (milesLeft <= MILES_WARN && !sent.includes(`mi${nextAt}`)) {
        notes.push(milesLeft < 0 ? `${-milesLeft} ${UNIT} past the ${nextAt} service point`
                                 : `${milesLeft} ${UNIT} to go (service at ${nextAt})`);
        sent.push(`mi${nextAt}`);
      }
    }

    if (!notes.length) continue;
    if (MailApp.getRemainingDailyQuota() < 1) break;
    const where = get(row, c.where) ? `\nLocation: ${get(row, c.where)}` : '';
    MailApp.sendEmail(String(row[c.email]),
      `Maintenance due: ${what}`,
      `${what} is ${notes.join(' and ')}.${where}\n\n` +
      `When it's done, log it so the next due date moves forward.`);
    sh.getRange(r + 1, c.log + 1).setValue(sent.join(';'));
  }
}

How it behaves:

Tips

Want reminders without maintaining a script? SheetReminders (disclosure: we make it) is a Google Sheets add-on that watches a date column, such as Next due, and emails the right person before it arrives: several reminders per date (like 14, 3 and 0 days), personalized messages with {{Column name}} and {{Days left}}, a digest mode with one email per person, skipping rows marked Done, and a sent log written back to the row. It works on dates; mileage checks stay with the script above. Sheet data stays inside your Google account and it uses narrow scopes. 14-day free trial, no credit card required. It is live on the Google Workspace Marketplace.
See SheetReminders
Short on time? Watch the 50-second version

Related Google Sheets guides

Instructions refer to Google Sheets, Google Docs, Google Forms, Google Drive and Apps Script as they worked on November 11, 2026. Not affiliated with Google or any other vendor named here. Google Sheets, Google Docs, Google Forms, Google Slides and Google Drive are trademarks of Google LLC. Features change, so verify in your own account. Questions or corrections: hello@loomwise.app.