// Apps Script

Create a time-driven trigger from code with ScriptApp.

How to programmatically install an hourly (or daily) time-driven trigger in Apps Script using ScriptApp.newTrigger(), and how to avoid stacking duplicate triggers every time the installer runs.

I want to install a recurring time-driven trigger from code so I don't have to click through the Apps Script UI every time I deploy.

The script

copy · paste · trigger
triggerInstaller.gs
Apps Script
// Run installTrigger() once (manually or on deploy) to schedule syncData().
// Safe to re-run: deletes any existing trigger for the same function first.

function installTrigger() {
  var fnName = 'syncData';

  // Remove stale triggers for this function before creating a new one.
  var existing = ScriptApp.getProjectTriggers();
  for (var i = 0; i < existing.length; i++) {
    if (existing[i].getHandlerFunction() === fnName) {
      ScriptApp.deleteTrigger(existing[i]);
    }
  }

  ScriptApp.newTrigger(fnName)
    .timeBased()
    .everyHours(1)
    .create();

  Logger.log('Trigger installed for ' + fnName);
}

function syncData() {
  // Your scheduled logic here.
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

Why the trigger builder API exists

The Apps Script UI lets you click through a trigger dialog, but that only works for the person clicking. The moment you share a script or deploy it as a library, every user has to install their own trigger by hand. ScriptApp.newTrigger() solves that: you ship an installTrigger() function, tell users to run it once, and the schedule is set in code.

The builder chains off ScriptApp.newTrigger('functionName'), which takes the handler function's name as a plain string. From there you call .timeBased() to enter the time-trigger branch, pick an interval method (everyHours, everyMinutes, everyDays, everyWeeks), and close with .create(). The trigger is scoped to the Google account that runs the installer, which is why each user needs to run it themselves on a shared script.

The duplicate-trigger trap and how to clear it

The first time I hit this, I ended up with six copies of the same hourly trigger because I kept running installTrigger() during testing. Apps Script does not check for existing triggers before creating a new one. Each call to .create() stacks an independent trigger, and they all fire, which means your handler runs six times per hour and you burn quota proportionally.

The fix is a delete pass before the create. ScriptApp.getProjectTriggers() returns every trigger the current user has installed on the project. Iterate that array, check getHandlerFunction() against your target name, and call ScriptApp.deleteTrigger() on any match. That brings the count back to zero before installing the fresh one. The pattern in the snippet above is safe to run any number of times: idempotent installs are the standard practice for anything that goes in a deploy script or an onOpen handler.

Choosing the right interval method

everyHours(n) accepts 1, 2, 4, 6, 8, or 12. Those are the only valid values; anything else throws an exception at install time, not at fire time, which at least surfaces fast. everyMinutes(n) accepts 1, 5, 10, 15, or 30. everyDays(1) fires once per day and can be paired with .atHour(9) to anchor it near a specific hour in the script's time zone (exact minute is randomized within that hour, by design, so don't rely on it for tight scheduling).

For longer schedules, everyWeeks(1).onWeekDay(ScriptApp.WeekDay.MONDAY) gives you a weekly Monday run. There is no native monthly option; the conventional workaround is a daily trigger whose handler checks if the current date is the first of the month before doing real work. Apps Script's trigger quota is 90 trigger-minutes per day on a free account and 6 hours on Workspace, so a misconfigured high-frequency trigger will hit that ceiling and start silently dropping runs.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
Does the trigger survive if I rename the function?
No. The trigger stores the handler as a plain string. If you rename syncData to syncSheetData, the trigger still fires but tries to call the old name and throws a 'script function not found' error. You need to delete the old trigger and reinstall pointing at the new name.
Can I install a trigger that runs for all users of my script?
Not directly. A trigger is always tied to the account that called installTrigger(). For a shared add-on, each user runs their own install. If you need a single centralized schedule (for example, a server-side sync), you install it under a service account or a dedicated project owner account and have it act on behalf of users via domain delegation.
Why does my trigger fire but Logger.log output disappears?
Time-driven triggers run in a headless context; the log is still written, but it goes to Stackdriver (Execution log) rather than the IDE log panel. Open Extensions > Apps Script > Executions to see the run history and any log output from triggered runs.
How do I delete all triggers for a project at once?
Loop ScriptApp.getProjectTriggers() without the function-name filter and call ScriptApp.deleteTrigger() on each element. One line per trigger is the only API; there is no bulk-delete method.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.