With Menu Cooldown

A menu item that archives rows or sends emails is dangerous when someone double-clicks it. withMenuCooldown(key, seconds, fn, opts) runs fn only if this menu action has not run in the last N seconds. Otherwise it shows a toast and returns null.

When to use

  • Destructive or expensive custom menu actions
  • Anything people tend to click twice while waiting
  • Pairing with a confirm step for archive or delete flows

How to use this snippet

/** * Run fn unless this menu key ran within the last `seconds`. * Uses DocumentProperties for the timestamp and a document lock * so two people (or a double-click) cannot both pass the check. * Returns null when blocked by cooldown or a busy lock; otherwise returns fn(). * Errors from fn propagate after the lock is released. * * @param {string} key stable id for this menu action * @param {number} seconds cooldown length * @param {function(): *} fn * @param {{title?: string, lockMs?: number}} [opts] * @return {*} */ function withMenuCooldown(key, seconds, fn, opts) { opts = opts || {}; var title = opts.title || 'Menu'; var ss = SpreadsheetApp.getActiveSpreadsheet(); var lock = LockService.getDocumentLock(); if (!lock.tryLock(opts.lockMs || 5000)) { if (ss) ss.toast('Please wait — another action is running.', title, 5); return null; } try { var props = PropertiesService.getDocumentProperties(); var propKey = 'menuCooldown:' + key; var now = Date.now(); var last = Number(props.getProperty(propKey) || 0); var waitMs = Math.max(0, Number(seconds) || 0) * 1000; if (waitMs && now - last < waitMs) { var left = Math.ceil((waitMs - (now - last)) / 1000); if (ss) ss.toast('Wait ' + left + 's before running again.', title, 5); return null; } props.setProperty(propKey, String(now)); return fn(); } finally { lock.releaseLock(); } }

Example

function onOpen() { SpreadsheetApp.getActive().addMenu('Ops', [{ name: 'Archive old rows', functionName: 'menuArchiveOld' }]); } function menuArchiveOld() { withMenuCooldown('archiveOld', 30, function () { // ... archive work ... SpreadsheetApp.getActive().toast('Done', 'Archive', 5); }, { title: 'Archive' }); }

Notes

  • Document scope: the cooldown timestamp lives in PropertiesService.getDocumentProperties(), so everyone using this spreadsheet shares it. That is usually what you want for a menu. Script properties would only throttle one user's script install.
  • Locking: LockService.getDocumentLock() wraps the read-check-write so two concurrent clicks cannot both see "clear" and both run. If the lock is busy, you get a toast and null, not a thrown error.
  • The cooldown timestamp is written before fn runs. A failed fn still burns the cooldown; that stops a crash-loop of retries from a menu mash.
  • This is not an undo. Pair destructive actions with an archive tab and a confirm step. See Confirm Dangerous Action.

Happy Coding!

NitroGAS Chrome Extension

Want this snippet handy inside the Apps Script editor? NitroGAS gives you free snippets — plus optional Co-Pilot when you want a boost (Day Pass — $3, or 1-week, 1-month, or yearly passes — no subscription). Happy Coding!

Get the Extension