How to Make an Equipment Sign-Out Sheet in Google Sheets (with Due-Back and Overdue Email Reminders, 2026)

Laptops, cameras, tripods, lab kits, tools, library-style lending closets: lots of schools, makerspaces, AV teams and small offices lend equipment out and track it in a spreadsheet. The hard part isn't writing down who took what. It's getting things back. This guide sets up a sign-out sheet that shows what's out and what's late at a glance, and a free Apps Script that emails each borrower the day before it's due, on the day, and a few times after it's late, with one email per person even if they borrowed several items.

This is a different job from tracking equipment maintenance: here the date that matters is when an item is due back from a person, and reminders stop as soon as it's returned.

Step 1: a Loans tab with one row per checkout

Create a tab named Loans with these headers in row 1. The script finds columns by name, so the order is up to you, but the formulas below assume A to H:

ColHeaderWhat goes in it
AIteme.g. “Camera 2”, “Chromebook 14”, “Drill kit”. Use the same name or asset tag every time.
BBorrowerName, used to greet them in the email
CBorrower emailWhere reminders go
DChecked outDate it left
EDue backDate it should come back, formatted as a date
FReturnedLeave blank while it's out. Type the return date (Ctrl+; inserts today) when it comes back. Anything in this cell stops reminders.
GNotesOptional, e.g. “with charger and bag”. It's included in the email.
HSent logLeave empty; the script records what it sent

Each checkout is a new row, and rows are never reused. That gives you a full lending history (who had Camera 2 when the lens got scratched?) instead of one cell that gets overwritten.

Faster checkouts with a form. A Google Form linked to the sheet lets borrowers or staff sign items out from a phone. Make Item a dropdown so names always match. Form responses land on their own tab, so either use that tab as Loans (rename its header cells to match the ones above; Google Forms adds a Timestamp column, which is fine) or keep Loans as the sheet staff edit by hand. If you set a standard loan period, Due back can be a formula, e.g. =IF(D2="","",D2+7) for one week. Type over it for longer loans.

Step 2: see what's out and what's late

Add a Status column (I) and fill down:

=IF(A2="","",IF(F2<>"","Returned",IF(E2="","Out",IF(E2<TODAY(),"Overdue "&(TODAY()-E2)&" days",IF(E2=TODAY(),"Due today","Out")))))

Then add conditional formatting on A2:I with custom formulas =AND($F2="",$E2<>"",$E2<TODAY()) (red, overdue) and =AND($F2="",$E2=TODAY()) (amber, due today). On another tab, everything still out, oldest due date first:

=QUERY(Loans!A:G,"select A,B,C,E where A is not null and F is null order by E",1)

Availability by item. Keep a tab named Items with one item name per row in column A. In B2, fill down to show whether each item is in or who has it:

=IF(A2="","",IF(COUNTIFS(Loans!$A:$A,A2,Loans!$F:$F,"")=0,"Available","Out: "&TEXTJOIN(", ",TRUE,FILTER(Loans!$B:$B,Loans!$A:$A=A2,Loans!$F:$F=""))))

If an item shows two borrowers at once, someone forgot to mark a return, which is worth knowing too.

Step 3: email borrowers before and after the due date

Google Sheets doesn't send email when a date arrives; 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 sendLoanReminders once to approve permissions, then add a trigger: Triggers › Add trigger › sendLoanReminders › Time-driven › Day timer (morning works best).

/**
 * Due-back and overdue reminders for an equipment sign-out sheet.
 * "Loans" tab headers used (any order): Item, Borrower, Borrower email,
 * Due back, Returned, Sent log. Optional: Checked out, Notes.
 * A row with a date (or anything) in Returned gets no more emails.
 */
const SHEET_NAME    = 'Loans';
const BEFORE_DAYS   = [1, 0];        // days before the due date (0 = due today)
const OVERDUE_DAYS  = [1, 3, 7];     // days after the due date
const MANAGER_EMAIL = '';            // e.g. 'equipment@yourschool.org'; CC'd from the last overdue reminder on
const MAX_EMAILS    = 50;            // per run; the rest go out tomorrow

function sendLoanReminders() {
  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'), who: col('Borrower'), email: col('Borrower email'),
    out: col('Checked out'), due: col('Due back'), back: col('Returned'),
    notes: col('Notes'), log: col('Sent log'),
  };
  for (const k of ['item', 'email', 'due', 'back', 'log']) {
    if (c[k] === -1) throw new Error('Missing column for ' + k + '. Check the headers in row 1.');
  }
  const tz    = Session.getScriptTimeZone();
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const fmt   = (d) => Utilities.formatDate(d, tz, 'EEE MMM d');
  const lastOverdue = Math.max(...OVERDUE_DAYS);

  // Group what is due per borrower so each person gets one email per run.
  const byEmail = {};
  for (let r = 1; r < rows.length; r++) {
    const row = rows[r];
    const email = String(row[c.email]).trim();
    const due = row[c.due];
    if (!email || !(due instanceof Date) || String(row[c.back]).trim() !== '') continue;
    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 sent = String(row[c.log] || '').split(';').filter(String);
    // The most urgent step reached so far that hasn't been sent for this due date.
    let step = null;
    if (left >= 0) {
      const b = BEFORE_DAYS.filter((n) => left <= n).sort((x, y) => x - y)[0];
      if (b !== undefined) step = 'b' + b;
    } else {
      const late = -left;
      const o = OVERDUE_DAYS.filter((n) => late >= n).sort((x, y) => y - x)[0];
      if (o !== undefined) step = 'o' + o;
    }
    if (!step || sent.includes(key + '@' + step)) continue;
    (byEmail[email] = byEmail[email] || []).push({
      r, row, left, step, sent, key,
      final: step === 'o' + lastOverdue,
    });
  }

  let emails = 0;
  for (const email of Object.keys(byEmail)) {
    if (emails >= MAX_EMAILS || MailApp.getRemainingDailyQuota() < 1) break;
    const loans = byEmail[email];
    const name  = String(loans[0].row[c.who] || '').trim();
    const late  = loans.some((l) => l.left < 0);
    const lines = loans.map((l) => {
      const when = l.left < 0 ? `was due ${fmt(l.row[c.due])} (${-l.left} day${l.left === -1 ? '' : 's'} overdue)`
                 : l.left === 0 ? 'is due back today' : `is due back ${fmt(l.row[c.due])}`;
      const note = c.notes !== -1 && l.row[c.notes] ? ` (${l.row[c.notes]})` : '';
      return `- ${l.row[c.item]}${note} ${when}`;
    });
    const opts = {};
    if (MANAGER_EMAIL && loans.some((l) => l.final)) opts.cc = MANAGER_EMAIL;
    MailApp.sendEmail(email,
      late ? 'Overdue: please return borrowed equipment' : 'Reminder: equipment due back soon',
      `Hi${name ? ' ' + name : ''},\n\n` + lines.join('\n') +
      `\n\nIf you've already returned it, reply and we'll fix the sheet. Thanks!`, opts);
    emails++;
    for (const l of loans) {
      l.sent.push(l.key + '@' + l.step);
      sh.getRange(l.r + 1, c.log + 1).setValue(l.sent.join(';'));
    }
  }
  return emails;
}

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 Due back, and emails the right person before it arrives: several reminders per date (like 1 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. 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 Forms and Apps Script as they worked on November 13, 2026. Not affiliated with Google or any other vendor named here. Google Sheets, Google Docs, Google Forms and Google Drive are trademarks of Google LLC. Features change, so verify in your own account. Questions or corrections: hello@loomwise.app.