Manage triggers programmatically at scale
Create, audit, and clean up Northwind triggers in code — never click through the UI.
Published Aug 4, 2025
Triggers are what turn a script into an automation — they make functions run on a schedule, on a form submit, or when a sheet is edited. The Apps Script editor lets you create them by clicking through a dialog, and for one trigger that is fine. The trouble starts when you have more than one, or more than one project.
Triggers created in the UI are invisible to anyone reading the code, do not travel when the script is copied, and are easy to forget about. Managing them in code instead makes them auditable, reproducible, and safe to set up the same way every time. This guide covers listing, creating, and tearing them down.
List
Before you change anything, look at what already exists.
ScriptApp.getProjectTriggers() returns every trigger attached to the current
project.
function listTriggers() {
// getProjectTriggers() returns an array of Trigger objects for this project.
for (const t of ScriptApp.getProjectTriggers()) {
// Log the function each trigger calls and what event fires it,
// so the project's automation is visible at a glance.
console.log(t.getHandlerFunction(), t.getEventType());
}
}
Run this whenever you are unsure what a project is doing. It is also worth pasting its output into a pull request so reviewers can see the automation, not just the code.
Create idempotently
The single most important rule: never create a trigger without first checking whether it already exists. Running a setup function twice should not leave you with two hourly triggers both doing the same work.
function ensureHourly(handlerName) {
// Check whether a matching clock trigger is already installed:
// same handler function AND the time-based (CLOCK) event type.
const exists = ScriptApp.getProjectTriggers().some(
(t) =>
t.getHandlerFunction() === handlerName &&
t.getEventType() === ScriptApp.EventType.CLOCK
);
// Only create the trigger if no equivalent one exists yet.
// This makes the function safe to call repeatedly.
if (!exists) {
ScriptApp.newTrigger(handlerName).timeBased().everyHours(1).create();
}
}
This “ensure” pattern is the trigger equivalent of an idempotent setup step. You
can call ensureHourly() from a single setup() function, run it as often as
you like, and always end up with exactly one trigger.
Tear down
When you copy a project, retire an automation, or want to rebuild triggers from scratch, you need a clean slate. This removes every trigger in one pass.
function deleteAll() {
// deleteTrigger() removes one trigger; mapping it over the full list
// clears them all. Useful as the first step of a rebuild.
ScriptApp.getProjectTriggers().forEach((t) => ScriptApp.deleteTrigger(t));
}
A common, robust pattern is to call deleteAll() and then a series of ensure
functions inside one setup(). That guarantees the live triggers always match
exactly what the code declares — no leftovers from an earlier configuration.
| Task | Function to use | Note |
|---|---|---|
| See what exists | getProjectTriggers() | Read-only, always safe |
| Add a trigger | newTrigger(...).create() | Always guard with an existence check |
| Remove one | deleteTrigger(t) | Takes the Trigger object, not a name |
| Reset everything | getProjectTriggers() + deleteTrigger | Pair with ensure calls to rebuild |
Why programmatic
Managing triggers in code, rather than through the editor UI, buys you three concrete things.
- It survives copy-paste. When the script is copied to a new project, its
UI-created triggers do not come along. A
setup()function does — run it once in the new project and the automation is configured identically. - It is auditable. A
listTriggers()call in code review, or anensurefunction in the source, makes the project’s automation visible. Triggers hidden in a dialog are invisible to anyone reading the repository. - It prevents the “I forgot which trigger calls this” problem. When triggers are declared in code, the wiring between schedule and function is right there in the file, not buried in a settings screen.
Common mistakes
- Creating without checking. Running a setup function twice doubles every
trigger, so the work runs twice. Always guard
create()with an existence check, asensureHourly()does. - Matching on handler name alone. Two triggers can call the same function for different events — a time-based one and an on-edit one. Compare the event type as well, or you may treat a different trigger as a match.
- Hitting the trigger limit. A project has a cap on the number of triggers. Accumulating duplicates is the usual way scripts hit it; idempotent creation prevents that.
- Deleting triggers you did not create.
deleteAll()is indiscriminate. In a project with triggers from several features, delete selectively by handler name instead of clearing everything. - Assuming triggers copy with the project. They do not. After copying a script, the new project has no triggers until you run setup — a frequent cause of “the automation just stopped working”.