appscript.dev
Guide Intermediate

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.

TaskFunction to useNote
See what existsgetProjectTriggers()Read-only, always safe
Add a triggernewTrigger(...).create()Always guard with an existence check
Remove onedeleteTrigger(t)Takes the Trigger object, not a name
Reset everythinggetProjectTriggers() + deleteTriggerPair 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 an ensure function 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, as ensureHourly() 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”.