Устанавливаемые триггеры
Как заставить скрипт работать ночью, когда таблица закрыта, а вы спите.
Устанавливаемый триггер (installable trigger) — связка «событие → функция», которую вы явно регистрируете: мышкой в редакторе или строчкой кода. В отличие от простого, он выполняется от вашего имени, с вашими разрешениями, и умеет всё, что умеет обычный скрипт: слать письма, ходить в API, открывать чужие файлы.
Зачем это нужно
Простой триггер упирается в стену ровно там, где начинается настоящая польза: отправить письмо, подтянуть курс валют, собрать отчёт из трёх таблиц. Всё это требует разрешений, а простые триггеры работают без них.
Устанавливаемый триггер решает обе проблемы сразу. Во-первых, при его создании Google один раз спрашивает у вас согласие («скрипту нужен доступ к вашей почте и таблицам»), и дальше все запуски идут с этими правами. Во-вторых, он умеет то, чего простой не умеет в принципе — срабатывать по времени. Файл закрыт, никто в него не заходил третью неделю, а в 9:00 отчёт всё равно уходит директору.
Создание из интерфейса
Самый быстрый путь — мышкой, из редактора Apps Script:
- Слева в редакторе нажмите значок будильника — «Триггеры».
- Внизу справа — «Добавить триггер».
- Функция — какую запускать (например,
ezhednevnyyOtchet). - Источник события — «Из таблицы» или «Триггер по времени».
- Тип события — «При изменении», «При отправке формы», «Ежедневно» и т. д.
- Для времени выбираете интервал и час: например, «Таймер по дням» → «9:00–10:00».
- Уведомления об ошибках — оставьте «Немедленно». Это ваш единственный способ узнать, что ночной скрипт упал.
После сохранения 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 часов |
| Триггеров на скрипт на пользователя | 20 | 20 |
| Писем в сутки | 100 | 1500 |
Отсюда два практических вывода. Первый: триггер каждую минуту — почти всегда плохая идея. 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 триггеров, лимит писем.
- Включайте уведомления об ошибках — ночной сбой иначе останется незамеченным.