Concept
Simple vs installable
| Simple trigger | Installable trigger | |
|---|---|---|
| Setup | Just name the function | Create it once (UI or code) |
| Can send email / use Drive | No | Yes |
| Runs as | The user at that moment | The person who created the trigger |
| Time-based | No | Yes |
| Max run time | 30 seconds | ~6 minutes |
| Event types | open, edit, selection | open, edit, change, form submit, time, calendar |
Installable triggers run with your permissions, even when nobody has the file open — that's why they can send emails at 9 a.m. every day.
Creating a trigger from the editor
- Click the Triggers (clock) icon on the left.
- + Add Trigger (bottom right).
- Choose the function, event source (Time-driven or From spreadsheet), and the type.
- Optionally set failure notifications — choose Notify me immediately for important jobs.
- Save and authorize.
Creating a trigger from code
// Run dailyReport every day between 9 and 10 a.m. (project time zone)
function createDailyTrigger() {
ScriptApp.newTrigger('dailyReport')
.timeBased()
.everyDays(1)
.atHour(9)
.create();
}
// Run syncRates every hour
function createHourlyTrigger() {
ScriptApp.newTrigger('syncRates')
.timeBased()
.everyHours(1)
.create();
}
// Run weeklySummary every Monday around 8 a.m.
function createWeeklyTrigger() {
ScriptApp.newTrigger('weeklySummary')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(8)
.create();
}
Run the create… function once. Each run adds another trigger, so running it twice gives you two emails every morning.
Note: atHour(9) means "sometime between 9:00 and 10:00", not exactly 9:00. Minute-based triggers allow everyMinutes(1), 5, 10, 15 or 30.
Installable onEdit (with email)
function createEditTrigger() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
ScriptApp.newTrigger('onBigOrder')
.forSpreadsheet(ss)
.onEdit()
.create();
}
function onBigOrder(e) {
const r = e.range;
if (r.getSheet().getName() !== 'Orders' || r.getColumn() !== 4) return;
if (Number(e.value) > 100000) {
MailApp.sendEmail('owner@example.com', 'Big order', `Order in row ${r.getRow()}: ₹${e.value}`);
}
}
Give the handler a different name than onEdit — otherwise it runs twice (once as a simple trigger, once as installable), and the simple one fails on MailApp.
Safe setup: avoid duplicates
function resetTriggers() {
// Delete existing triggers for this project that call dailyReport
ScriptApp.getProjectTriggers()
.filter(t => t.getHandlerFunction() === 'dailyReport')
.forEach(t => ScriptApp.deleteTrigger(t));
ScriptApp.newTrigger('dailyReport').timeBased().everyDays(1).atHour(9).create();
}
Run resetTriggers whenever you want to (re)install — it always leaves exactly one.
Key points
- Installable triggers can email, use Drive and run on a schedule.
- They run as the creator, so they work while everyone is offline.
- Create them once; use
getProjectTriggersto clean up duplicates. - Check Executions to see each triggered run.