Простые триггеры: onOpen и onEdit

Как научить таблицу показывать своё меню при открытии и сама отмечать время правки — без единой кнопки «Запустить».

Простой триггер (simple trigger) — функция с зарезервированным именем (onOpen, onEdit, onSelectionChange), которую Google Таблицы вызывают сами, как только произошло соответствующее событие. Регистрировать её нигде не нужно: достаточно правильно назвать.

Зачем это нужно

Пока вы работаете со скриптом в одиночку, запускать его из редактора терпимо. Но как только таблицей пользуется отдел, картина меняется: бухгалтер не должен открывать «Расширения → Apps Script» и искать нужную функцию в списке. Ему нужен пункт меню с понятным названием — или чтобы вообще ничего не нажимать, и всё случилось само.

Ровно это и дают простые триггеры. Два из них закрывают 90% задач: onOpen — «человек открыл файл», onEdit — «человек изменил ячейку». Вы пишете функцию с таким именем, сохраняете проект, и Google начинает вызывать её за вас.

onOpen: своё меню в таблице

Функция onOpen выполняется каждый раз, когда пользователь открывает документ. Классическое применение — собрать собственное меню рядом со стандартными «Файл», «Правка», «Вид».

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Отчёты')
    .addItem('Обновить сводку', 'obnovitSvodku')
    .addItem('Очистить черновики', 'ochistitChernoviki')
    .addSeparator()
    .addItem('О скрипте', 'pokazatSpravku')
    .addToUi();
}

function obnovitSvodku() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  // ... здесь пересчёт данных ...
  ss.toast('Сводка пересчитана', 'Готово', 3);
}

function pokazatSpravku() {
  SpreadsheetApp.getUi().alert('Меню обновляет сводку по данным листа «Заявки».');
}

Разберём построчно:

  • SpreadsheetApp.getUi() — доступ к интерфейсу таблицы: меню, диалоги, боковые панели.
  • createMenu('Отчёты') — создаёт меню; текст в скобках увидят пользователи.
  • addItem('Обновить сводку', 'obnovitSvodku') — первый аргумент это подпись, второй — имя функции строкой. Частая ошибка новичка: написать addItem('Обновить сводку', obnovitSvodku) без кавычек. Кавычки обязательны.
  • addSeparator() — разделительная черта, чисто косметика.
  • addToUi() — без этой строки меню не появится: до неё вы просто описывали конструкцию в памяти.

Сохраните проект и перезагрузите страницу таблицы — меню появляется только при открытии документа, а не по волшебству в уже открытой вкладке.

onEdit: реакция на правку

Функция onEdit вызывается после того, как пользователь изменил ячейку. В неё Google передаёт объект события — обычно его называют e. Это «протокол происшествия»: что именно поменяли, где и на что.

ПолеЧто в нём
e.rangeдиапазон, который правили (объект Range)
e.valueновое значение — только если правили одну ячейку
e.oldValueчто было до правки (тоже только для одной ячейки; при вводе в пустую ячейку поля просто нет)
e.sourceсама таблица (Spreadsheet)
e.authModeрежим авторизации, в котором запущен триггер

Практическая задача: на листе «Заявки» в колонке C менеджеры ставят статус. Нужно, чтобы в колонке D автоматически появлялась дата и время последнего изменения статуса.

const LIST_ZAYAVKI = 'Заявки';
const KOL_STATUS = 3; // колонка C
const KOL_METKA = 4;  // колонка D

function onEdit(e) {
  const range = e.range;
  const sheet = range.getSheet();

  // 1. Нас интересует только один лист
  if (sheet.getName() !== LIST_ZAYAVKI) return;
  // 2. И только колонка со статусом
  if (range.getColumn() !== KOL_STATUS) return;
  // 3. Шапку не трогаем
  if (range.getRow() === 1) return;

  sheet.getRange(range.getRow(), KOL_METKA).setValue(new Date());
}

Три ранних выхода в начале — не придирка, а главный приём для onEdit. Триггер срабатывает на любую правку в любой ячейке документа, поэтому первое, что делает функция, — проверяет, её ли это случай, и молча выходит, если нет. Без этих проверок скрипт будет штамповать дату всякий раз, когда кто-то поправит опечатку на соседнем листе.

Журнал изменений на e.oldValue

Ещё один частый сценарий — вести историю правок: кто, когда, что было и что стало. Пригодится, когда цифры в общем файле «сами меняются».

function onEdit(e) {
  const log = e.source.getSheetByName('Журнал');
  if (!log) return;

  log.appendRow([
    new Date(),
    e.range.getSheet().getName(),
    e.range.getA1Notation(),
    e.oldValue === undefined ? '' : e.oldValue,
    e.value === undefined ? '(несколько ячеек)' : e.value
  ]);
}

Результат: в листе «Журнал» появляется строка вида

14.07.2026 10:32:05 | Заявки | C7 | Новая | Оплачено

Обратите внимание на проверку undefined: если пользователь выделил и удалил сразу пять ячеек, полей e.value и e.oldValue в объекте события просто не будет. Код, который слепо им доверяет, начнёт писать в журнал пустоту.

Что НЕ считается правкой

Это место, где ломается больше всего сценариев. Простой onEdit реагирует только на правку человеком в интерфейсе. Он не сработает:

  • когда значение записал другой скрипт через setValue() или setValues();
  • когда пересчиталась формула (например, =NOW() или ссылка на другой лист);
  • когда данные подтянула IMPORTRANGE;
  • когда строку добавила отправка Google Формы — для этого есть отдельный триггер onFormSubmit, о нём в третьем уроке раздела;
  • когда таблицу изменили через Sheets API извне.

Иначе говоря: onEdit — это про пальцы на клавиатуре, а не про любое изменение данных.

Ограничения простых триггеров

Теперь главное. Простые триггеры выполняются без авторизации: Google никогда не показывает окно «Приложению нужен доступ к вашей почте». Удобно — но у медали есть обратная сторона. Раз разрешений никто не давал, то и сервисов, которым разрешения нужны, скрипт не увидит.

Можно из простого триггераНельзя
читать и писать текущую таблицу (SpreadsheetApp.getActive...)отправлять письма (MailApp, GmailApp)
показывать меню, тосты, диалогиходить во внешние API (UrlFetchApp)
форматировать ячейки, добавлять строкиоткрывать другие файлы (SpreadsheetApp.openById, DriveApp)
писать в Logger/consoleработать дольше 30 секунд

Есть и менее очевидные ограничения:

  • Триггер не выполнится у пользователя, открывшего файл только для просмотра: правок он не делает, а onOpen в режиме просмотра урезан.
  • Функция должна называться ровно onEdit. onEdit2, myOnEdit, OnEdit — это обычные функции, Google их не вызовет.
  • В одном проекте скрипта может быть только одна функция с таким именем. Если вы объявите onEdit в двух файлах проекта, победит одна из них — обычно последняя, и это будет очень неочевидный баг. Собирайте всю логику в одной onEdit и разводите её условиями.

Вывод простой: как только вам нужно отправить письмо, дёрнуть внешний сервис или что-то сделать по расписанию — простого триггера не хватит. Нужен устанавливаемый, о нём следующий урок.

Как это работает

Когда вы открываете таблицу с привязанным (bound) скриптом, Google просматривает проект и ищет функции с зарезервированными именами. Найдя onOpen, он выполняет её от имени текущего пользователя — того, кто открыл файл, — в ограниченном режиме авторизации. Именно этот режим лежит в e.authMode и обычно равен LIMITED.

Ключевое следствие: код исполняется у каждого, кто открыл документ. Ваш коллега, у которого есть право на правку, тоже увидит меню и тоже запустит вашу onEdit — но от своего имени и со своими правами. Никаких «а у меня всё работало» тут не бывает: работает у всех одинаково, потому что разрешений нет ни у кого.

Второе следствие — производительность. onEdit выполняется в момент правки, и пока он думает, таблица подтормаживает. Тяжёлые операции (пересчёт тысяч строк, выгрузка в PDF) из onEdit делать нельзя: и 30 секунд закончатся, и пользователей вы разозлите.

Частые ошибки

  • Пытаются отправить письмо из onEdit. В журнале появляется You do not have permission to call MailApp.sendEmail. Это не баг прав доступа к почте — это архитектурное ограничение простых триггеров. Лечится переходом на устанавливаемый триггер.
  • Тестируют onEdit кнопкой «Выполнить» в редакторе. Google не подставляет объект события, e оказывается undefined, и вы получаете TypeError: Cannot read properties of undefined (reading 'range'). Правильный способ — реально изменить ячейку в таблице, а результат посмотреть в журнале выполнений. Либо написать обёртку с поддельным событием:
function testOnEdit() {
  const sheet = SpreadsheetApp.getActive().getSheetByName('Заявки');
  onEdit({
    range: sheet.getRange('C2'),
    value: 'Оплачено',
    oldValue: 'Новая',
    source: SpreadsheetApp.getActive()
  });
}
  • Забывают проверить лист и колонку. Скрипт срабатывает на каждую правку во всём документе, ставит метки где попало и тормозит файл.
  • Ждут, что onEdit поймает ответ формы или пересчёт формулы. Не поймает — см. выше.
  • Ставят onOpen в отдельный (не привязанный) проект. Простые триггеры живут только в скрипте, привязанном к документу: «Расширения → Apps Script» из самой таблицы.
  • Считают строки от нуля. range.getRow() возвращает номер строки листа: шапка — это 1, а не 0. Массивы в JavaScript начинаются с нуля, лист — с единицы; на стыке рождается сдвиг на одну строку.

Итоги

  • onOpen и onEdit — функции с зарезервированными именами; их не нужно регистрировать, Google вызывает их сам.
  • onOpen обычно строит пользовательское меню: getUi().createMenu(...).addItem(подпись, 'имяФункцииСтрокой').addToUi().
  • onEdit получает объект события e: e.range, e.value, e.oldValue, e.source. e.value/e.oldValue есть только при правке одной ячейки.
  • Первое, что должен делать onEdit, — проверить лист, колонку и строку и выйти, если событие не его.
  • Простой триггер работает без авторизации: не более 30 секунд, никакой почты, внешних запросов и чужих файлов.
  • Он реагирует только на правку человеком: ни setValue из скрипта, ни формулы, ни отправка формы его не разбудят.
  • Нужно письмо, расписание или внешний сервис — переходите на устанавливаемые триггеры.
Проверьте себя
1. Менеджер правит статус в таблице, и вы хотите отправить ему письмо-подтверждение прямо из onEdit. Что произойдёт?
AПисьмо уйдёт, но с задержкой в несколько минут
BСкрипт упадёт с ошибкой прав доступа: простые триггеры не могут вызывать MailApp
CGoogle покажет пользователю окно с запросом разрешения на отправку почты
DПисьмо уйдёт только владельцу таблицы
2. В каком случае простой триггер onEdit НЕ сработает?
AПользователь вручную ввёл текст в ячейку
BПользователь вставил скопированные данные в диапазон
CGoogle Форма добавила в лист новую строку с ответом
DПользователь удалил содержимое ячейки клавишей Delete