Skip to content
// Sheets · Apps Script

Add a custom menu in Google Sheets.

How to add a persistent custom menu to Google Sheets using Apps Script's onOpen trigger, with working handler functions that don't fail silently when clicked.

I want a custom menu in my Google Sheet so I can run a script with one click instead of opening the script editor every time.

The script

copy · paste · trigger
menu.gs
Apps Script
// Runs automatically when the spreadsheet opens
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('My Tools')
    .addItem('Run Report', 'runReport')
    .addItem('Clear Output', 'clearOutput')
    .addSeparator()
    .addItem('About', 'showAbout')
    .addToUi();
}

function runReport() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.getRange('A1').setValue('Report ran at: ' + new Date());
}

function clearOutput() {
  SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().clearContents();
}

function showAbout() {
  SpreadsheetApp.getUi().alert('My Tools v1.0');
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why the function name is a string, not a reference

The `addItem` call takes two arguments: the label the user sees, and the function name as a plain string. That string is what Apps Script resolves at click-time by looking up a global function in your project. It does not accept a function reference like `addItem('Run Report', runReport)` — pass the bare function and Apps Script silently drops the handler.

This is the failure mode I see most often: someone refactors their code, renames `runReport` to `generateMonthlyReport`, updates the call sites in their own code, but forgets the string in `addItem`. The menu still appears. Clicking it does nothing. No error, no toast, no indication something is wrong. The string and the function name must match exactly, including case.

The practical implication: keep your handler functions at the top level of any `.gs` file in the project, never nested inside another function or wrapped in an IIFE. Apps Script's global scope spans the whole project, so you can put handlers in a separate file (e.g. `handlers.gs`) as long as they aren't nested.

Wiring the trigger: onOpen and the first run

Apps Script treats `onOpen` as a simple trigger — it fires automatically every time the spreadsheet opens, including when you open it yourself after deploying the script. You don't need to configure anything in the Triggers dashboard for this to work.

The first time you paste this code in and save, the menu won't appear yet because the sheet is already open. Reload the tab, or go to Run > Run function > onOpen once in the editor to populate it for your current session. After that, every open will fire it automatically.

One thing worth knowing: `onOpen` runs with limited authorization. It cannot call services that require elevated permissions (like sending email or reading Drive files outside the spreadsheet). If your handler needs those, the permission prompt triggers when the user clicks the menu item for the first time, not when the sheet opens.

Adding submenus and separators for longer menus

For more than four or five items, a flat list gets unwieldy. Apps Script lets you create a submenu with `addSubMenu`, which takes another menu built with `ui.createMenu`. The separator from `addSeparator()` draws a horizontal rule and costs nothing — I use it to group related actions without the overhead of a submenu.

The chaining pattern (`createMenu().addItem().addItem().addToUi()`) must terminate with `addToUi()`. Forgetting that call is the other common silent failure: no error, no menu. The chain builds a menu object in memory; `addToUi()` is what actually attaches it to the spreadsheet's menu bar.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
My menu shows up but clicking an item does nothing — what's wrong?
Almost always a name mismatch. The string you passed to `addItem` does not match any top-level function name in your project. Check spelling, case, and that the function isn't nested inside another function. There is no runtime error when the lookup fails.
Do I need to set up a trigger manually for onOpen to fire?
No. `onOpen` is a reserved simple trigger that Apps Script fires automatically on every open without any Triggers configuration. Just name the function exactly `onOpen` at the top level.
The menu disappears after I reload — how do I make it permanent?
If it disappears on reload, `onOpen` isn't running. Check that the script is bound to the spreadsheet (opened via Extensions > Apps Script, not as a standalone script), and that the file containing `onOpen` is saved. Standalone scripts don't have access to `SpreadsheetApp.getUi()`.
Can I add a menu item that runs code with arguments?
Not directly — `addItem` only accepts a function name with no way to pass arguments. The workaround is to write a thin wrapper function for each variant (`function runReportWeekly() { runReport('weekly'); }`) and register the wrapper name in `addItem`.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.