Skip to content
Book a Consultation

You set up a Google Apps Script trigger, tested the function once, and moved on. Now the status column in your sheet is no longer updating, the daily summary email stopped arriving, or a form response sits in the spreadsheet with nothing downstream happening. Sometimes the Executions list shows a red “Failed” entry. Sometimes it shows nothing at all, which is more confusing.

Those two situations point to different problems. A failed execution means the trigger fired and your code broke. A missing execution usually means the trigger never fired, either because of how it was created, what kind of event it listens for, or whose account it runs under. This guide walks through both, using the behavior described in Google’s current Apps Script documentation.

Quick diagnostic checklist

  • Open the script’s Executions page and look for runs of your function at the time the event happened. Present and failed, or absent entirely?
  • Confirm whether you are relying on a simple trigger (a function named onEdit, onOpen, and so on) or an installable trigger listed on the Triggers page.
  • Check whether the change was made by a person typing in the sheet, or by a script, form, import, or API. Only some of those fire edit triggers.
  • Check which Google account created the installable trigger and whether that account still has access and authorization.
  • Look for quota-related messages such as “Service invoked too many times” or “Exceeded maximum execution time”.
  • Look for duplicate triggers pointing to the same function.

Common causes of a trigger that doesn’t fire or fails

1. A simple trigger is trying to do something that needs authorization

Simple triggers are the reserved functions such as onEdit(e) and onOpen(e). They are convenient because you don’t have to install anything, but Google’s simple trigger documentation lists strict limits. The one that catches most people: a simple trigger can’t call services that require authorization. Sending mail through MailApp or GmailApp, opening a different spreadsheet, or writing to Drive will fail from a plain onEdit. Simple triggers also can’t run longer than 30 seconds and don’t run when the file is opened in view-only or comment-only mode.

What it looks like: the part of the function that edits the current sheet works, but the email or cross-file step never happens, and the execution shows an authorization error. The fix is to rename the function (so it is no longer a reserved name) and attach it to an installable trigger instead.

2. The change was made by a script, a form, or an API, not a person

The documentation states plainly that script executions and API requests don’t cause triggers to run. If another Apps Script function calls Range.setValue(), or an external tool such as Zapier, Make, or n8n writes rows through the Google Sheets API, your edit trigger will not fire. This applies to installable triggers as well as simple ones.

This is the most common reason people report that “onEdit works when I type but not when the data comes in automatically.” For Google Form responses, use a form submit trigger. For data written by an integration platform, have that platform call your logic directly (for example, through an Apps Script web app) or process new rows on a time-driven schedule.

3. The installable trigger belongs to someone else

Installable triggers always run under the account of the person who created them. If a colleague created the trigger, it uses their permissions and their quotas, even when you are the person editing the sheet. Google also notes that one account can’t see triggers installed by another account. So you might open the Triggers page, see nothing, and assume no trigger exists, when a teammate’s trigger is still firing (or failing) in the background.

Problems appear when that person leaves the company, loses access to the file, or has their account suspended. The troubleshooting guide also advises making sure installable triggers run as a user within your organization.

4. Authorization is missing, expired, or out of date

When you add code that uses a new service (for example, you add a Drive export to a function that previously only touched the sheet), the script needs additional permissions. A trigger can’t show an authorization dialog, so the run fails. Google’s troubleshooting page lists the error “Authorization is required to perform that action” and notes that triggers firing before authorization or after it has expired often cause it. The authorization guide explains that triggers must be authorized by the user who created them.

5. A quota or execution time limit was reached

The quotas page currently lists a 6-minute maximum per script execution, 20 triggers per user per script, and a daily cap on total trigger runtime (90 minutes for consumer accounts, 6 hours for Google Workspace accounts). Email is limited by recipients per day: 100 for consumer accounts and 1,500 for Workspace accounts at the time of writing. Quotas are per user, reset 24 hours after the first request, and Google says they can change without notice.

A trigger that worked for months can start failing when data volume grows, because a loop that used to finish in 40 seconds now takes seven minutes.

6. Time-driven triggers don’t run at the exact minute

If your “9 AM” trigger runs at 9:37, that is expected. Apps Script picks a time within the hour you chose and keeps it roughly consistent from day to day. The ClockTriggerBuilder reference says nearMinute() has about 15 minutes of variance, and everyMinutes() only accepts 1, 5, 10, 15, or 30. If a process needs a precise send time, Apps Script alone may not be the right scheduler.

7. The code assumes a single-cell edit or a manual run

In the edit event object, e.value and e.oldValue are only populated for single-cell edits. Pasting a block of data or clearing several cells leaves e.value undefined, so a check like if (e.value === 'Approved') silently does nothing. Separately, clicking Run on a trigger function in the editor passes no event object at all, which produces a TypeError about reading properties of undefined. That error only tells you the test method is wrong, not that the trigger is broken.

8. Duplicate or orphaned triggers

Every time a setup function calls ScriptApp.newTrigger(), it creates another trigger. Run it three times and your handler fires three times per event, which can cause duplicate emails and overlapping writes. The reverse also happens: a function gets renamed, and the installable trigger still points at the old name, so every run fails because the handler can’t be found.

Step-by-step troubleshooting

  1. Reproduce the event on a copy. Make a copy of the spreadsheet (which also copies a bound script) or use a test tab with dummy records. Avoid testing against live customer data, especially if the trigger sends email.
  2. Check the Executions page. In the Apps Script editor, open Executions. The dashboard documentation shows a Type column (for example, Trigger or Time Driven), start time, duration, and status. If you see a failed run, open it and read the error and log lines. If there is no run at all, skip to step 4.
  3. Read the error text literally. “Authorization is required to perform that action” points to cause 4. “Exceeded maximum execution time” points to runtime. “Service invoked too many times” points to a daily quota. A TypeError on e.range or e.value points to cause 7.
  4. Confirm the trigger exists and belongs to the right account. Open the Triggers page (the clock icon) while signed in as the account that should own the trigger. Check the function name, the event source (From spreadsheet or Time-driven), and the event type (On edit, On change, On form submit). If the list is empty, ask whoever originally built the automation to check their account.
  5. Confirm the event actually qualifies. If the data arrived through an API, another script, or an import, an edit trigger will not fire. Switch to a form submit trigger, a time-driven batch process, or a direct call from the integration.
  6. Re-authorize. Signed in as the trigger owner, run any function in the script from the editor and accept the permission dialog. This picks up any new scopes the code now needs.
  7. Remove duplicates. Delete extra triggers pointing at the same handler, or run a setup function that checks ScriptApp.getProjectTriggers() before creating a new one (see the code below).
  8. Make the handler defensive and observable. Add guard clauses, a lock, and logging so the next failure tells you exactly what happened.

Code example: a safer installable edit handler

This example handles edits to a status column on a sheet named “Leads”. It exits early for irrelevant edits, uses LockService to avoid two runs processing the same row at once, logs what it did, and re-throws errors so the execution shows as failed instead of quietly succeeding. Replace the example email address with your own.

const CONFIG = {
  sheetName: 'Leads',
  statusColumn: 6,               // column F
  notifyEmail: 'sales@example.com'
};

// Attach this function to an installable "On edit" trigger.
// Do NOT name it onEdit, or it will also run as a simple trigger.
function handleLeadEdit(e) {
  if (!e || !e.range) {
    console.warn('No event object. Test by editing the sheet, not with Run.');
    return;
  }
  const range = e.range;
  const sheet = range.getSheet();
  if (sheet.getName() !== CONFIG.sheetName) return;
  if (range.getColumn() !== CONFIG.statusColumn) return;
  if (range.getNumRows() !== 1 || range.getNumColumns() !== 1) return;
  if (range.getRow() === 1) return; // header row

  // e.value is undefined for pastes, so read the cell directly.
  const status = String(range.getValue()).trim();
  if (status !== 'Qualified') return;

  const lock = LockService.getDocumentLock();
  if (!lock.tryLock(10000)) {
    console.error('Lock not acquired for row ' + range.getRow());
    return;
  }
  try {
    const row = range.getRow();
    const values = sheet.getRange(row, 1, 1, CONFIG.statusColumn).getValues()[0];
    const name = values[0];
    const email = values[1];
    MailApp.sendEmail(
      CONFIG.notifyEmail,
      'Lead qualified: ' + name,
      'Row ' + row + ' was marked Qualified.\nContact: ' + email
    );
    console.log('Notification sent for row ' + row);
  } catch (err) {
    console.error('handleLeadEdit failed: ' + err.stack);
    throw err; // keep the execution marked as Failed
  } finally {
    lock.releaseLock();
  }
}

// Run once from the editor, signed in as the account that should own the trigger.
function installLeadEditTrigger() {
  const exists = ScriptApp.getProjectTriggers().some(function (t) {
    return t.getHandlerFunction() === 'handleLeadEdit';
  });
  if (exists) {
    console.log('Trigger already installed.');
    return;
  }
  ScriptApp.newTrigger('handleLeadEdit')
    .forSpreadsheet(SpreadsheetApp.getActive())
    .onEdit()
    .create();
}

Two details are worth pointing out. First, getProjectTriggers() only returns the current user’s triggers for the project, so the duplicate check protects against your own repeat runs but not against a colleague’s trigger. Second, reading the row with one getValues() call is faster than several separate getValue() calls, which matters as the handler grows.

Sample workflow (hypothetical): A small sales team tracks leads in Google Sheets and wants an email alert when a rep marks a lead as Qualified. This is an illustration, not a client project.

ElementDetails
TriggerInstallable On edit trigger on the lead spreadsheet, owned by a shared operations account.
ApplicationsGoogle Sheets, Apps Script, MailApp (Gmail for delivery).
Data inputsLead name (column A), email (column B), status (column F) changed by a person.
Processing logicIgnore other sheets, columns, multi-cell pastes, and the header row; act only when status becomes Qualified; lock the document during processing.
Expected outputsOne notification email per qualified lead and a log line in Executions.
Failure pointsStatus set by an import or API (trigger won’t fire), expired authorization, daily email recipient quota, trigger owner losing access.
Error handlingtry/catch with logging, error re-thrown so the run is marked Failed, trigger failure emails enabled for the owner.
Validation and testingTest on a copy with dummy rows; try a single edit, a paste, an edit on another tab, and two quick edits in a row.
Business applicationFaster handoff from qualification to follow-up without anyone watching the sheet.

When a native feature is enough: if the team only needs to know that something changed, Google Sheets’ built-in notification settings may cover it with no code. Custom development makes sense when the alert depends on a specific value, needs row details, or has to reach people who don’t have the sheet open. If statuses are set by a CRM or integration rather than a person, the design needs to change to a time-driven or webhook-based approach.

How to confirm the issue is fixed

  • Perform the real triggering action (a manual edit, a form submission, or waiting for the scheduled hour) on your test copy and confirm a new entry appears in Executions with a Completed status.
  • Open that execution and confirm your log lines appear in the order you expect.
  • Confirm the output: the email arrived, the row updated, or the file was created, and it happened once, not twice.
  • Test the edge cases that caused the problem: a paste, an edit on the wrong tab, and an edit by a second user.
  • For time-driven triggers, check over several days rather than one run, since timing within the hour varies.

Common mistakes to avoid

  • Testing a trigger function by clicking Run and concluding the trigger is broken because e is undefined.
  • Naming an installable trigger’s handler onEdit, which makes it run as a simple trigger too.
  • Running a trigger setup function repeatedly and creating stacked duplicates.
  • Expecting onEdit to fire for rows written by Zapier, Make, n8n, or another script.
  • Catching errors and swallowing them, which marks broken runs as successful.

Prevention and monitoring

  • Use a stable owner. Create important installable triggers from an account the business controls long-term, and document which account owns them.
  • Enable failure notifications. When you add or edit a trigger in the editor, choose how often Google should email the owner about failed runs. Make sure that inbox is monitored.
  • Log with console. Google’s logging guide notes that editor execution logs don’t persist very long, while Cloud Logging keeps logs for longer and exceptions can feed Cloud Error Reporting.
  • Watch runtime trends. If durations in Executions creep toward the 6-minute limit, split the work into batches before it starts failing.

If your triggers are part of a larger spreadsheet system (reporting, lead routing, or document generation), our Google Sheets and Apps Script development service covers building and stabilizing those workflows. For a scheduled reporting example, see How to Automate Google Sheets Reporting With Apps Script, and if rows are arriving from another app, Zapier Not Sending Data Between Apps? covers the sending side.

Still having trouble with your Apps Script triggers?

Tell Modern Streamline which spreadsheet and applications are involved, what the trigger should do, and what is happening instead. We can review your script and trigger setup and discuss an appropriate fix.

Get Help With Your Automation

FAQ

Why does onEdit work when I type but not when data is imported?

Edit triggers respond to user edits. Google’s documentation states that script executions and API requests don’t cause triggers to run, so rows written by another script or by an integration platform using the Sheets API won’t fire them. Use a form submit trigger, a time-driven check for new rows, or have the integration call your logic directly.

Why can’t my onEdit function send email?

A simple onEdit can’t use services that require authorization, and Gmail is one of them. Rename the function and attach it to an installable On edit trigger, then authorize the script from the editor.

Why does my daily trigger run at a different time than I set?

Apps Script chooses a time within the hour you selected and keeps it fairly consistent. Exact-minute scheduling is not guaranteed.

I can’t see the trigger on the Triggers page, but it still runs. Why?

Installable triggers are tied to the account that created them, and other accounts can’t see them. Ask the collaborators on the file to check their own Triggers page.

Sources