How to Track Warranty Expiration Dates in Google Sheets, with Reminders (2026)

Warranties are easy to buy and easy to forget. The laptop, the office printer, the dishwasher, the company vans: each one comes with coverage that quietly runs out after a year or three, and the time you find out is usually the week after it ended. If you already keep purchases or assets in a spreadsheet, a few extra columns turn it into a warranty tracker. This guide sets up a Warranties tab that works out each end date from the purchase date, handles extended warranties, shows what's expiring in the next 90 days, and adds a free Apps Script that emails each owner one digest 60, 30 and 7 days before coverage ends.

It's the same idea as tracking contract renewals, with two differences: the end date usually has to be calculated (purchase date plus a number of months), and an item can have two dates, the manufacturer's warranty and an extended plan that runs past it.

Step 1: a Warranties tab

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

ColHeaderWhat goes in it
AIteme.g. “Dell Latitude 5440 (front desk)”
BSerialSerial number or asset tag; manufacturers ask for it when you file a claim
CVendorWho you'd call: the manufacturer, the store, or the warranty provider
DPurchase dateThe date coverage starts (check the terms: some start at delivery or installation)
EWarranty monthsLength of the standard warranty, e.g. 12, 24 or 36
FWarranty endsA formula (Step 2), formatted as a date
GExtended untilEnd date of an extended warranty or protection plan, if you bought one; otherwise blank
HCoverage endsA formula (Step 2): the later of F and G
IDays leftA formula (Step 2)
JOwner emailWho should get the reminder; blank sends it to you
KStatusLeave blank while you own it; Retired, Sold, Disposed, Replaced or Claimed stops reminders
LProof of purchaseA link to the receipt or invoice in Drive (see the tips)
MSent logLeave empty; the script records what it sent

Format D, F, G and H as dates (Format › Number › Date). A date typed as text, like “Jan 11”, won't calculate.

Step 2: end date, coverage and days left

In F2, fill down. EDATE adds whole months and handles short months (a 12-month warranty bought on February 29 ends February 28):

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

That gives the anniversary date. If your terms say coverage lasts “through” the day before (a one-year warranty from January 11 ending January 10), use EDATE(D2,E2)-1 instead. When the paperwork prints an exact end date, just type it over the formula.

In H2, the date coverage actually runs out, whichever is later:

=IF(AND(F2="",G2=""),"",MAX(F2,G2))

In I2, the days remaining (format it as a plain number; negative means it has expired):

=IF(H2="","",H2-TODAY())

TODAY() recalculates every day, so Days left counts down on its own.

Step 3: what's expiring in the next 90 days

On a new tab, this lists every item still in service whose coverage ends within 90 days, soonest first:

=SORT(FILTER({Warranties!A2:A,Warranties!C2:C,Warranties!H2:H,Warranties!I2:I},
  Warranties!H2:H<>"",Warranties!I2:I>=0,Warranties!I2:I<=90,Warranties!K2:K=""),4,TRUE)

A FILTER that finds nothing shows #N/A; wrap it in IFERROR(…,"Nothing expiring") if you like. Format the third column as a date. For a quick count on a dashboard: =COUNTIFS(Warranties!I2:I,">=0",Warranties!I2:I,"<=30",Warranties!K2:K,"").

Step 4: conditional formatting

Select I2:I, open Format › Conditional formatting, pick Custom formula is, and add these rules:

Custom formulaColorMeaning
=AND(ISNUMBER(I2),I2>=0,I2<=30)RedEnds within 30 days
=AND(ISNUMBER(I2),I2>30,I2<=90)YellowEnds within 90 days
=AND(ISNUMBER(I2),I2<0)GrayExpired

Step 5: email reminders before coverage ends

Sheets can't send an email when a date arrives on its own; its notification rules react to edits, not the calendar. A short Apps Script on a daily trigger does it. Open Extensions › Apps Script, paste this, set DEFAULT_TO (or leave it blank to email yourself), save, run sendWarrantyReminders once to approve permissions, then add a trigger: Triggers › Add trigger › sendWarrantyReminders › Time-driven › Day timer.

/**
 * Daily warranty expiration reminders from a "Warranties" tab.
 * Headers used (any order): Item, Warranty ends, Sent log.
 * Optional: Serial, Vendor, Extended until, Owner email, Status.
 * Emails each owner one digest 60, 30 and 7 days before coverage ends
 * (the later of Warranty ends and Extended until). Blank owner = you.
 */
const SHEET_NAME   = 'Warranties';
const DEFAULT_TO   = '';               // e.g. 'it@yourcompany.com' (blank = you)
const REMIND_DAYS  = [60, 30, 7];      // days before coverage ends
const STOP_STATUS  = /^(retired|sold|disposed|replaced|claimed)$/i;
const MAX_EMAILS   = 20;               // safety cap per run

function sendWarrantyReminders(now) {
  const sh   = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  const rows = sh.getDataRange().getValues();
  const head = rows[0].map((h) => String(h).trim());
  const col  = (name) => head.indexOf(name);
  const c = {
    item: col('Item'), serial: col('Serial'), vendor: col('Vendor'),
    ends: col('Warranty ends'), ext: col('Extended until'),
    owner: col('Owner email'), status: col('Status'), log: col('Sent log'),
  };
  if (c.item === -1 || c.ends === -1 || c.log === -1) {
    throw new Error('Need Item, Warranty ends and Sent log columns.');
  }
  const tz    = Session.getScriptTimeZone();
  // A time-driven trigger passes an event object, so only use a real Date.
  const today = now instanceof Date ? new Date(now) : new Date();
  today.setHours(0, 0, 0, 0);
  const get    = (row, i) => (i === -1 ? '' : row[i]);
  const isDate = (v) => v instanceof Date && !isNaN(v);
  const ymd    = (d) => Utilities.formatDate(d, tz, 'yyyy-MM-dd');
  const nice   = (d) => Utilities.formatDate(d, tz, 'EEE, MMM d, yyyy');
  const daysTo = (d) => {
    const x = new Date(d); x.setHours(0, 0, 0, 0);
    return Math.round((x - today) / 86400000);
  };
  const when  = (n) => (n === 0 ? 'today' : n === 1 ? 'tomorrow' : `in ${n} days`);
  const hitOf = (left, list) =>
    left < 0 ? undefined : list.filter((n) => left <= n).sort((a, b) => a - b)[0];
  const me = Session.getEffectiveUser().getEmail();
  const byOwner = {}, logs = [];

  for (let r = 1; r < rows.length; r++) {
    const row = rows[r];
    if (!row[c.item] || STOP_STATUS.test(String(get(row, c.status)).trim())) continue;
    const base = get(row, c.ends), ext = get(row, c.ext);
    // Coverage ends on the later of the two dates.
    const end = isDate(ext) && (!isDate(base) || ext > base) ? ext : base;
    if (!isDate(end)) continue;
    const left = daysTo(end);
    const hit  = hitOf(left, REMIND_DAYS);
    if (hit === undefined) continue;
    const sent = String(row[c.log] || '').split(';').filter(String);
    const key  = `W${ymd(end)}@${hit}`;
    if (sent.includes(key)) continue;
    const label = [row[c.item], get(row, c.serial) && `S/N ${get(row, c.serial)}`,
                   get(row, c.vendor)].filter(String).join(', ');
    const kind  = end === ext ? 'extended warranty' : 'warranty';
    const to = String(get(row, c.owner)).trim() || DEFAULT_TO || me;
    (byOwner[to] = byOwner[to] || []).push({
      left, text: `${label}: ${kind} ends ${when(left)} (${nice(end)})`,
      r: r + 1, log: sent.concat(key).slice(-9).join(';'),
    });
  }

  let emails = 0;
  for (const to of Object.keys(byOwner)) {
    if (emails >= MAX_EMAILS || MailApp.getRemainingDailyQuota() < 1) break;
    const items = byOwner[to].sort((a, b) => a.left - b.left);
    MailApp.sendEmail(to, `Warranties ending soon (${items.length})`,
      'Check these before coverage runs out (file claims, book a last ' +
      'inspection, or decide on an extension):\n\n- ' +
      items.map((x) => x.text).join('\n- ') + '\n\nSent from the Warranties sheet.');
    // Log only after the email for this owner went out.
    items.forEach((x) => sh.getRange(x.r, c.log + 1).setValue(x.log));
    emails++;
  }
}

How it behaves:

We mock-tested this logic before publishing: the first run sent one digest per owner with the right lines in date order, a rerun the same day and the next day sent nothing, an extended warranty was picked over the standard one (and an extension dated earlier than the standard end was ignored), a missed 7-day reminder went out once on the last day, and nothing was logged when the email quota was used up.

Tips

Want reminders without maintaining a script? SheetReminders (disclosure: we make it) is a Google Sheets add-on that watches a date column and emails the right person before it arrives. Point it at the Coverage ends column from Step 2: several reminders per date (like 60, 30 and 7 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. 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

Related Google Sheets guides

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