Устанавливаемые триггеры

Как заставить скрипт работать ночью, когда таблица закрыта, а вы спите.

Устанавливаемый триггер (installable trigger) — связка «событие → функция», которую вы явно регистрируете: мышкой в редакторе или строчкой кода. В отличие от простого, он выполняется от вашего имени, с вашими разрешениями, и умеет всё, что умеет обычный скрипт: слать письма, ходить в API, открывать чужие файлы.

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

Простой триггер упирается в стену ровно там, где начинается настоящая польза: отправить письмо, подтянуть курс валют, собрать отчёт из трёх таблиц. Всё это требует разрешений, а простые триггеры работают без них.

Устанавливаемый триггер решает обе проблемы сразу. Во-первых, при его создании Google один раз спрашивает у вас согласие («скрипту нужен доступ к вашей почте и таблицам»), и дальше все запуски идут с этими правами. Во-вторых, он умеет то, чего простой не умеет в принципе — срабатывать по времени. Файл закрыт, никто в него не заходил третью неделю, а в 9:00 отчёт всё равно уходит директору.

Создание из интерфейса

Самый быстрый путь — мышкой, из редактора Apps Script:

  1. Слева в редакторе нажмите значок будильника — «Триггеры».
  2. Внизу справа — «Добавить триггер».
  3. Функция — какую запускать (например, ezhednevnyyOtchet).
  4. Источник события — «Из таблицы» или «Триггер по времени».
  5. Тип события — «При изменении», «При отправке формы», «Ежедневно» и т. д.
  6. Для времени выбираете интервал и час: например, «Таймер по дням» → «9:00–10:00».
  7. Уведомления об ошибках — оставьте «Немедленно». Это ваш единственный способ узнать, что ночной скрипт упал.

После сохранения Google запросит разрешения. Согласие даётся один раз на проект.

Триггер по времени: отчёт каждый день в 9:00

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

function ustanovitEzhednevnyyTrigger() {
  ScriptApp.newTrigger('ezhednevnyyOtchet')
    .timeBased()
    .atHour(9)
    .nearMinute(15)
    .everyDays(1)
    .inTimezone('Europe/Moscow')
    .create();
}

function ezhednevnyyOtchet() {
  const sheet = SpreadsheetApp.openById('ID_ТАБЛИЦЫ').getSheetByName('Заявки');
  const data = sheet.getDataRange().getValues();
  const novye = data.filter(function (row) { return row[2] === 'Новая'; });

  MailApp.sendEmail({
    to: '[email protected]',
    subject: 'Новых заявок за сутки: ' + novye.length,
    body: 'Подробности в таблице: ' + sheet.getParent().getUrl()
  });
}

Разбор цепочки:

  • ScriptApp.newTrigger('ezhednevnyyOtchet') — имя функции строкой, как и в меню.
  • .timeBased() — тип: по времени, а не по событию документа.
  • .atHour(9) — час запуска. Важно: Google обещает не точное время, а часовое окно — запуск случится где-то между 9:00 и 10:00. Для отчёта это нормально, для «ровно в 9:00:00» — нет; таких гарантий платформа не даёт вообще.
  • .nearMinute(15) — просьба целиться примерно в 9:15, разброс порядка ±15 минут.
  • .everyDays(1) — каждый день.
  • .inTimezone('Europe/Moscow') — часовой пояс. Если не указать, возьмётся пояс проекта, а он по умолчанию может быть американским. Классический сюрприз: «отчёт приходит в четыре утра».
  • .create() — до этого вызова триггера не существует.

Обратите внимание на SpreadsheetApp.openById(...) внутри ezhednevnyyOtchet. В ночном запуске никакой «активной таблицы» нет — никто её не открывал. Поэтому getActiveSpreadsheet() в триггере по времени вернёт null, и код упадёт. Файл нужно открывать по идентификатору явно.

Что нужноКак записать
каждые 10 минут.timeBased().everyMinutes(10) (допустимы 1, 5, 10, 15, 30)
каждый час.timeBased().everyHours(1)
по понедельникам утром.timeBased().onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(8)
1-го числа каждого месяца.timeBased().onMonthDay(1).atHour(7)
один раз в конкретный момент.timeBased().at(new Date('2026-09-01T10:00:00+03:00'))

Триггеры по событиям документа

Второе семейство — события самой таблицы. Их три, и путать их дорого.

function ustanovitTriggeryTablicy() {
  const ss = SpreadsheetApp.getActive();

  // при отправке Google Формы, связанной с этой таблицей
  ScriptApp.newTrigger('obrabotatZayavku')
    .forSpreadsheet(ss)
    .onFormSubmit()
    .create();

  // при правке ячейки — но уже с полными правами
  ScriptApp.newTrigger('reagirovatNaPravku')
    .forSpreadsheet(ss)
    .onEdit()
    .create();
}
СобытиеКогда срабатываетЧто в объекте e
onEdit()правка ячейки человекомe.range, e.value, e.oldValue, e.user
onChange()структурные изменения: вставка/удаление строк и столбцов, добавление листа, смена форматаe.changeType (INSERT_ROW, REMOVE_ROW, EDIT, FORMAT…), e.source; значений ячеек тут нет
onFormSubmit()пришёл ответ Google Формыe.range, e.values, e.namedValues

Устанавливаемый onEdit называется так же, как простой, но это другая вещь: функция может называться как угодно (reagirovatNaPravku), она проходит авторизацию и потому умеет слать письма. Расплата — задержка: устанавливаемый триггер запускается не мгновенно, а через секунду-другую, и всё так же ограничен по времени работы. Правило простое: если письмо не нужно — берите простой onEdit, он отзывчивее.

Дубли — главная грабля

Функция ustanovitTriggeryTablicy из примера выше содержит бомбу замедленного действия: запустите её пять раз — получите пять одинаковых триггеров. Каждый ответ формы вызовет обработчик пятикратно, и клиент получит пять писем. Ошибка тем коварнее, что при разработке функцию установки жмут по нескольку раз почти неизбежно.

Лечится идемпотентной установкой: сначала снести старые триггеры этой функции, потом создать новый.

function pereustanovitTrigger(imyaFunkcii) {
  // 1. Удаляем все существующие триггеры этой функции
  ScriptApp.getProjectTriggers().forEach(function (t) {
    if (t.getHandlerFunction() === imyaFunkcii) {
      ScriptApp.deleteTrigger(t);
    }
  });

  // 2. И только теперь создаём один новый
  ScriptApp.newTrigger(imyaFunkcii)
    .timeBased()
    .atHour(9)
    .everyDays(1)
    .inTimezone('Europe/Moscow')
    .create();
}

function nastroit() {
  pereustanovitTrigger('ezhednevnyyOtchet');
}

Такую функцию можно жать сколько угодно раз: триггер всегда останется ровно один. Заодно полезно уметь посмотреть, что вообще навешано на проект:

function pokazatTriggery() {
  ScriptApp.getProjectTriggers().forEach(function (t) {
    console.log(t.getHandlerFunction() + ' | ' + t.getEventType() + ' | ' + t.getTriggerSource());
  });
}

Результат:

ezhednevnyyOtchet | CLOCK | CLOCK
obrabotatZayavku | ON_FORM_SUBMIT | SPREADSHEETS

Лимиты выполнения

Бесплатность Apps Script имеет цену — квоты. Точные цифры Google периодически меняет и они разные для личных аккаунтов и Workspace, но порядок величин стоит держать в голове.

ОграничениеЛичный аккаунт (gmail.com)Google Workspace
Одно выполнение скриптадо 6 минутдо 6 минут — потом обрыв с «Exceeded maximum execution time»
Суммарное время триггеров в суткиоколо 90 минутоколо 6 часов
Триггеров на скрипт на пользователя2020
Писем в сутки1001500

Отсюда два практических вывода. Первый: триггер каждую минуту — почти всегда плохая идея. 1440 запусков в сутки съедят суточную квоту времени, даже если каждый длится пару секунд. Начинайте с 10–15 минут.

Второй: если обработка не укладывается в 6 минут (скажем, вы разбираете 50 000 строк), задачу режут на порции — обработали 500 строк, запомнили позицию в PropertiesService, поставили одноразовый триггер «продолжить через минуту» и вышли. Подробно этот паттерн разбирается в разделе «Эксплуатация».

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

Устанавливаемый триггер физически живёт не в файле таблицы, а в связке «проект скрипта + конкретный пользователь». Отсюда вся его логика поведения.

  • Коллега, открывший ту же таблицу, не увидит ваших триггеров в списке — там пусто. Ему нужно создать свои.
  • Триггер работает от вашего имени, даже когда вы спите, а файл закрыт. Письма уйдут с вашего адреса.
  • Если вы потеряете доступ к файлу (уволились, отозвали доступ) или отзовёте разрешения скрипта — триггер сломается, а не «перейдёт» к кому-то другому.
  • Ошибку триггера вы не увидите глазами: экрана нет. Google пришлёт письмо-сводку Summary of failures — вот почему уведомления при создании триггера стоит оставить включёнными.

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

  • Плодят дубли триггеров. Симптом: клиент получает 3–4 одинаковых письма. Лечится getProjectTriggers() + deleteTrigger() перед созданием.
  • Используют getActiveSpreadsheet() в триггере по времени. Активной таблицы ночью нет — только openById().
  • Ждут «ровно в 9:00». atHour(9) — это окно 9:00–10:00, а не будильник. Если бизнес-процесс требует минутной точности, Apps Script — не тот инструмент.
  • Забывают про часовой пояс. Пояс берётся из настроек проекта, а не из вашего браузера. Задавайте inTimezone() явно.
  • Создают триггер внутри самого триггера и не удаляют старый: количество растёт лавиной, пока не упрётся в лимит 20.
  • Настраивают триггеры под тестовым аккаунтом, а в бой файл идёт под рабочим. Триггеры не переезжают вместе с файлом — их нужно создать заново тому, от чьего имени они должны работать.

Итоги

  • Устанавливаемый триггер регистрируется явно — мышкой (значок будильника) или через ScriptApp.newTrigger(...).
  • Он проходит авторизацию и работает от вашего имени: письма, внешние API, чужие файлы — всё доступно.
  • Только он умеет запускаться по времени: timeBased().atHour(9).everyDays(1), и только он ловит отправку формы.
  • atHour задаёт часовое окно, а не точную минуту; часовой пояс указывайте через inTimezone().
  • Установку делайте идемпотентной: сначала удалить старые триггеры этой функции, потом создать новый — иначе получите дубли.
  • Помните про квоты: 6 минут на выполнение, ограниченное суммарное время в сутки, 20 триггеров, лимит писем.
  • Включайте уведомления об ошибках — ночной сбой иначе останется незамеченным.
Проверьте себя
1. Функция установки триггера вызывает ScriptApp.newTrigger('obrabotat').forSpreadsheet(ss).onFormSubmit().create(). Разработчик запустил её четыре раза во время отладки. Что произойдёт при следующем ответе формы?
AНичего особенного: Google не создаёт одинаковые триггеры повторно
BФункция obrabotat выполнится четыре раза — например, клиент получит четыре письма
CСработает только последний созданный триггер, остальные игнорируются
DСкрипт упадёт с ошибкой «триггер уже существует»
2. Триггер по времени каждое утро вызывает функцию, которая начинается со строки SpreadsheetApp.getActiveSpreadsheet(). Почему она падает?
AТриггеры по времени вообще не имеют доступа к таблицам
BНужно сначала вызвать SpreadsheetApp.flush()
CПри ночном запуске таблицу никто не открывал, активной таблицы нет — файл надо открывать через openById()
DМетод устарел и заменён на getSpreadsheet()