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):
| Col | Header | What goes in it |
|---|---|---|
| A | Asset | e.g. “Van 2”, “Compressor”, “Walk-in cooler” |
| B | Task | e.g. “Oil change”, “Replace filter”, “Annual inspection” |
| C | Location | Where the asset is (optional) |
| D | Every (days) | The interval in days, e.g. 90 for quarterly, 365 for yearly. Blank if the task only goes by mileage. |
| E | Last done | Date of the last service (typed, or pulled from the log in Step 2) |
| F | Next due | A formula (Step 2), formatted as a date |
| G | Owner email | Who should get the reminder |
| H | Remind days | Comma-separated days before the due date, e.g. 14,3,0. Blank uses the default. |
| I | Status | Leave blank while in service; Retired, Paused or Sold stops reminders |
| J | Every (miles) | Optional mileage (or engine-hour) interval, e.g. 5000 |
| K | Last done miles | Odometer or hour-meter reading at the last service |
| L | Current miles | Latest reading; update it weekly or from a form |
| M | Sent log | Leave 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:
- One email per reminder. With
14,3,0, the owner hears 14 days out, 3 days out and on the day. If the script starts late (say 8 days out), it sends one email, not a pile of catch-up emails. - Overdue tasks get one email on the first run after the due date, then stop until the date changes, so nobody is spammed daily. Watch the red rows in the sheet for anything still overdue.
- Reminders reset when service is logged. The Sent log entries are keyed to the due date (and to the mileage service point), so a new Next due starts a fresh cycle without anyone clearing cells. Clear the cell whenever it gets long; nothing breaks.
- Mileage reminders go out once when the asset is within
MILES_WARNof its next service point.
Tips
- Get readings in. Mileage reminders are only as good as Current miles. A two-question form (Asset, reading) filled in at fuel-up, plus
MAXIFSinto column L, keeps it current. - Email limits. Sends count against your Apps Script email quota. The script stops when it runs out and continues the next day.
- Ownership. The trigger runs as whoever created it, and emails come from that account. Create it from a shared operations account that won't leave the company.
- A helper, not a safety system. Email can be filtered or ignored. For inspections a regulator, insurer or manufacturer requires, keep a regular manual review of the “due soon” view as well.
- Need work orders or service reports as documents? See a PDF or Google Doc for each row.
See SheetReminders
Short on time? Watch the 50-second version
Related Google Sheets guides
- How to generate a PDF or Google Doc for each row in Google Sheets
- Best Autocrat alternatives for mail merge to PDF and Docs
- Make invoices or certificates from Google Sheets rows
- Create sales quotes or estimates from Google Sheets
- Generate receipts from Google Sheets rows
- Create purchase orders from Google Sheets
- Make packing slips from Google Sheets orders
- Generate a PDF certificate from a Google Form response
- Issue numbered training completion certificates with expiry dates
- Make a PDF progress report for each student
- Generate offer letters from a Google Sheets hiring tracker
- Email each row’s PDF as an attachment
- Automatic email reminders from a date column
- Contract renewal and expiration alerts from Google Sheets
- Track certificate of insurance (COI) expirations in Google Sheets
- Track employee license and certification expirations
- Track equipment and vehicle maintenance with reminders
- Track warranty expiration dates with reminders
- Birthday and work anniversary reminders from Google Sheets
- Rent due and lease renewal reminders from Google Sheets
- Send invoice payment reminders from Google Sheets
- Highlight overdue tasks and send email reminders
- Follow-up reminders for leads, clients and candidates
- Membership renewal reminders for clubs and associations
- SheetWeave vs Autocrat
- SheetWeave vs Document Studio
- SheetWeave vs RowDocs
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.