// Sheets · Apps Script

Fix "Cannot call SpreadsheetApp.getUi() from this context" in Apps Script.

SpreadsheetApp.getUi() throws when called from time-driven or form-submit triggers because no editor window is open. Learn how to gate UI calls to menu-invoked functions and route trigger output to Logger or MailApp instead.

I am getting "Cannot call SpreadsheetApp.getUi() from this context" when my Apps Script trigger fires and I cannot figure out why it works in the editor but fails on a schedule.

The script

copy · paste · trigger
Code.gs
Apps Script
// Gate UI calls to menu-invoked paths; triggers use Logger + MailApp
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Tools')
    .addItem('Run check', 'runCheckFromMenu')
    .addToUi();
}

function runCheckFromMenu() {
  // Safe: called only from a menu click, editor window is open
  var ui = SpreadsheetApp.getUi();
  var result = doCheck();
  ui.alert('Check complete: ' + result);
}

function runCheckFromTrigger() {
  // Safe: no getUi() call here, trigger has no editor context
  var result = doCheck();
  Logger.log('Check result: ' + result);
  MailApp.sendEmail(Session.getEffectiveUser().getEmail(), 'Check result', result);
}

function doCheck() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Data');
  return 'Rows: ' + sheet.getLastRow();
}

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

Walkthrough

Why this only breaks in triggers

SpreadsheetApp.getUi() requires an active editor session — a human with a browser tab open on the spreadsheet. When you click Run in the Apps Script IDE or open the sheet and trigger onOpen(), that session exists. When a time-driven trigger fires at 2 AM, or when onFormSubmit() runs because someone submitted a form, there is no browser tab, no human session, and therefore no UI context. Google's runtime refuses the call immediately with that error.

This is a context error, not a permissions error. Adding extra OAuth scopes or changing deployment settings will not fix it. The function genuinely cannot reach a UI that does not exist.

Splitting the work into two entry points

The fix is a clean split: one function for menu-triggered paths (where getUi() is safe), and a separate function for trigger-triggered paths (where it is not). Both call a shared doCheck() function that holds the actual logic. This pattern keeps the core work testable and avoids duplicating spreadsheet reads.

Point your time-driven or onFormSubmit trigger at runCheckFromTrigger(). Point your menu item at runCheckFromMenu(). The doCheck() function never touches the UI at all, so it runs cleanly in either context. I keep this kind of shared-logic function in a separate Utils.gs file once a project has more than three or four such pairs.

For output from the trigger path, Logger.log() writes to the Apps Script execution log (visible under Executions in the Apps Script console), and MailApp.sendEmail() covers any case where you need a durable record sent somewhere. Both work without an editor session.

Using a try/catch when one function must serve both paths

Sometimes you genuinely want a single function that a menu item calls and a trigger also calls — for instance, a shared onEdit() handler that you also invoke manually for testing. A try/catch around the getUi() call lets it degrade gracefully:

try { var ui = SpreadsheetApp.getUi(); ui.alert('Done'); } catch (e) { Logger.log('UI unavailable: ' + e.message); } This pattern is worth knowing, but prefer the split-function approach for any trigger that fires on a real schedule. A try/catch here is defensive fallback, not primary design. The first time I used try/catch as the primary strategy I ended up with a trigger that silently swallowed errors for three days before I noticed the execution log.

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
Does this error mean I need to request additional permissions?
No. The error is a context issue, not a permissions issue. getUi() is unavailable at runtime when no editor session is open, regardless of which OAuth scopes the script holds. Adding scopes will not resolve it.
Can I show a dialog or sidebar from an onFormSubmit trigger?
No. onFormSubmit, time-driven triggers, and onEdit simple triggers (as opposed to installable onEdit triggers invoked via menu) all run without an editor session. Dialogs, sidebars, and alerts are all gated behind SpreadsheetApp.getUi() and will throw in those contexts. Route output to MailApp or Logger instead.
Why does the function work when I press Run in the Apps Script IDE but fail when the trigger fires?
Running from the IDE opens an editor session on your behalf, which satisfies the UI context requirement. The trigger runtime has no such session. The behavior difference is expected and reflects how the two execution environments work, not a bug in your code.
What if I need to notify the user when a trigger finds a problem?
Use MailApp.sendEmail() with Session.getEffectiveUser().getEmail() to reach whoever installed the trigger. For lower-friction notifications, write a structured entry to a dedicated log sheet with sheet.appendRow() and check it on a schedule, or use the UrlFetchApp to POST to a webhook (Slack, ntfy.sh, etc.).