Time-Based Triggers in Google Apps Script: Schedule Automations to Run on Autopilot
Learn how to use time-based triggers in Google Apps Script to schedule automations that run without any manual intervention — daily reports, weekly cleanups, hourly data syncs, and more.
Most Apps Script automations start as manual processes. You write a function that sends a report, cleans up a spreadsheet, or syncs data from an external source. You run it when you remember. Then you realise the whole point was to stop doing it manually.
Time-based triggers solve that problem. They tell Apps Script to run a specific function on a schedule you define — every hour, every day at 7am, every Monday morning — without anyone clicking a button. Once a trigger is set, the automation runs on Google's infrastructure whether you are at your desk, on a call, or asleep.
This guide covers everything you need to know about time-based triggers in Apps Script: the different trigger intervals available, how to create triggers using both the UI and code, how to manage and delete them, the quota limits that apply, and three production-ready examples built around common Australian business workflows. Timezone handling for AEST and AEDT is covered in its own section, because getting that wrong is one of the most common — and most avoidable — trigger mistakes.
What Are Time-Based Triggers?
A trigger in Apps Script is an instruction that tells Google when to automatically run one of your functions. There are several trigger types — on form submit, on edit, on open — but time-based triggers are the most broadly useful because they fire on a clock schedule regardless of what any user is doing.
Under the hood, Google's trigger infrastructure behaves like a cron job: a scheduled background process that executes at defined intervals. You do not need to maintain a server, write a scheduler, or configure any external service. Everything runs within your Google Workspace account using your existing credentials and permissions.
Time-based triggers support six interval types:
| Trigger Type | Options | Typical Use Case |
|---|---|---|
| Specific date/time | Single future datetime | One-off scheduled tasks |
| Minutes timer | Every 1, 5, 10, 15, or 30 minutes | Near-real-time data polling |
| Hour timer | Every 1, 2, 4, 6, 8, or 12 hours | Periodic data sync or monitoring |
| Day timer | Daily at a chosen hour window | Morning reports, nightly cleanups |
| Week timer | Weekly on a chosen day and hour | Weekly summaries, Monday briefings |
| Month timer | Monthly on a chosen day | Monthly reports, billing reminders |
Each trigger is attached to a specific script project and fires the function you nominate. One project can have multiple triggers pointing to different functions, or the same function can be triggered on multiple schedules.
Creating Triggers Via the Apps Script UI
The trigger management interface is built into the Apps Script editor and requires no coding to use. It is the fastest path from "I wrote a function" to "that function now runs automatically".
Step-by-step process:
- Open your Apps Script project at script.google.com, or via Extensions > Apps Script from inside a Google Sheet, Doc, or Form.
- In the left sidebar, click the clock icon labelled Triggers.
- In the bottom-right corner of the Triggers panel, click Add Trigger.
- Configure the trigger in the dialog that appears:
- Choose which function to run: Select the function name from the dropdown. Only functions defined in your project are listed.
- Choose which deployment should run: Select Head (the current saved version of your script).
- Select event source: Choose Time-driven.
- Select type of time based trigger: Choose the interval type — Minutes timer, Hour timer, Day timer, Week timer, or Month timer.
- Select time of day / interval: The available options change depending on the trigger type. For a Day timer, you select an hour window (e.g., 7am to 8am). For a Week timer, you additionally choose the day of the week.
- Optionally, set Failure notification settings to Notify me immediately so that you receive an email if the trigger throws an unhandled exception.
- Click Save.
The trigger appears in the Triggers list. You can edit or delete it at any time by clicking the three-dot menu on the right side of the trigger row.
Important note on "hour windows" for Day and Week timers: Google does not guarantee execution at an exact minute within the window you choose. If you select "7am to 8am", the trigger will fire sometime between 7:00 and 7:59. This is intentional — Google distributes trigger load across the hour to prevent bursts of concurrent executions. If your automation requires execution at a precise minute (for example, 7:00am sharp), you need to use the programmatic approach with a specific-datetime trigger or accept a small window of variability.

Creating Triggers Programmatically with ScriptApp.newTrigger()
Programmatic trigger creation is essential when you need to deploy triggers as part of a script setup process, create triggers dynamically based on user input, or set up identical triggers across multiple script projects.
The ScriptApp.newTrigger() method creates a TriggerBuilder object. You chain methods onto it to configure the schedule, then call .create() to register the trigger.
Minutes and Hour Timers
function createMinuteTrigger() {
// Run every 15 minutes
ScriptApp.newTrigger('syncDataFromAPI')
.timeBased()
.everyMinutes(15)
.create();
}
function createHourTrigger() {
// Run every 4 hours
ScriptApp.newTrigger('checkInventoryLevels')
.timeBased()
.everyHours(4)
.create();
}
Valid values for everyMinutes() are: 1, 5, 10, 15, 30.
Valid values for everyHours() are: 1, 2, 4, 6, 8, 12.
Day Timer
function createDailyTrigger() {
// Run daily between 7am and 8am in the script's timezone
ScriptApp.newTrigger('sendDailyReport')
.timeBased()
.everyDays(1)
.atHour(7)
.create();
}
The atHour() method accepts an integer from 0 to 23 (midnight is 0, 11pm is 23). The trigger fires within the nominated hour window. You can call everyDays(1) for daily, or everyDays(2) for every two days.
Week Timer
function createWeeklyTrigger() {
// Run every Monday between 8am and 9am
ScriptApp.newTrigger('sendWeeklySummary')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(8)
.create();
}
ScriptApp.WeekDay is an enum with values: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY.
Month Timer
function createMonthlyTrigger() {
// Run on the 1st of each month between 6am and 7am
ScriptApp.newTrigger('generateMonthlyBillingReport')
.timeBased()
.onMonthDay(1)
.atHour(6)
.create();
}
onMonthDay() accepts an integer from 1 to 28. Google caps this at 28 to ensure it works in February.
Running the Setup Function Once
You only need to run a trigger-creation function once. Call it manually from the Apps Script editor (select it in the function dropdown and click Run), or include it as part of an onInstall() handler if you are distributing the script as an add-on. Running it multiple times will create duplicate triggers, which causes the target function to execute multiple times per interval.
Managing and Deleting Triggers
Viewing Triggers in the UI
All triggers for a project are visible in the Triggers panel (clock icon in the left sidebar). Each row shows the function name, the trigger type, the schedule, the owner, and the last run time and status. If a trigger failed its last execution, the status column will show an error indicator.
Deleting Triggers via the UI
Click the three-dot menu on any trigger row and select Delete trigger. Confirm the deletion in the dialog. The trigger is immediately removed — any future scheduled executions are cancelled, but any currently-running execution is not interrupted.
Listing and Deleting Triggers Programmatically
When you need to reset all triggers (for example, during a script update), it is safer to delete all existing triggers before creating new ones rather than accumulating duplicates:
function deleteAllTriggers() {
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
ScriptApp.deleteTrigger(triggers[i]);
}
Logger.log('Deleted ' + triggers.length + ' trigger(s).');
}
To delete only triggers that call a specific function, filter by handler function name:
function deleteTriggersByFunction(functionName) {
var triggers = ScriptApp.getProjectTriggers();
var deleted = 0;
for (var i = 0; i < triggers.length; i++) {
if (triggers[i].getHandlerFunction() === functionName) {
ScriptApp.deleteTrigger(triggers[i]);
deleted++;
}
}
Logger.log('Deleted ' + deleted + ' trigger(s) for function: ' + functionName);
}
// Usage: deleteTriggersByFunction('sendDailyReport');
Getting Trigger Metadata
You can inspect any trigger's configuration using the Trigger object returned by ScriptApp.getProjectTriggers():
function listTriggers() {
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
var t = triggers[i];
Logger.log(
'Function: ' + t.getHandlerFunction() +
' | Type: ' + t.getEventType() +
' | Source: ' + t.getTriggerSource()
);
}
}

Trigger Quotas and Limits
Google imposes quotas on Apps Script triggers to prevent abuse and ensure fair usage across all accounts. Understanding these limits helps you design automations that stay within bounds.
Key quota limits (as of 2026):
| Quota | Consumer Google Accounts | Google Workspace Accounts |
|---|---|---|
| Total trigger run time per day | 90 minutes | 6 hours |
| Maximum triggers per project | 20 | 20 |
| Maximum triggers per user across all projects | 20 | 20 |
The most important limit to watch is total trigger runtime per day. Every time a time-based trigger fires and your function executes, the execution time counts against your daily quota. A function that takes 30 seconds to run, triggered every 15 minutes, uses 2 minutes of runtime per hour — that is 48 minutes per day, well within the Workspace quota but already more than half the consumer account limit.
For scripts that process large data sets, optimise your functions to finish quickly. Techniques include:
- Batch API calls: Read or write data in a single
getValues()/setValues()call rather than row-by-row to reduce API round trips. - Use
PropertiesServiceto checkpoint progress: If a script needs more time than a single execution allows, store progress state in Script Properties and resume on the next trigger. - Avoid unnecessary sleep:
Utilities.sleep()consumes execution time against your quota while doing nothing useful. - Short-circuit early: Return immediately if there is nothing to process (e.g., no new rows since the last run).
Minute-based triggers are the most quota-intensive. A trigger firing every minute that runs for 5 seconds consumes 120 seconds per hour — 48 minutes of runtime per 8-hour workday. Use minute-based triggers only when the use case genuinely requires near-real-time execution. For most business workflows, hourly or daily triggers are sufficient.
If a trigger's execution throws an unhandled exception, that execution still counts against your quota. Add try/catch blocks around the main logic of any triggered function to prevent unnecessary failures.
Timezone Considerations for Australian Businesses
Timezone handling is where many Apps Script trigger configurations go wrong. Apps Script trigger schedules are evaluated relative to the script timezone, which is set in the project settings. If your script timezone does not match your business timezone, triggers will fire at unexpected local times.
Setting the Script Timezone
- In the Apps Script editor, click the Project Settings gear icon in the left sidebar.
- Under General settings, find the Time zone dropdown.
- Select the appropriate Australian timezone:
- Australia/Sydney — NSW, ACT, Victoria, Tasmania (observes AEDT in summer)
- Australia/Brisbane — Queensland (AEST year-round, no daylight saving)
- Australia/Adelaide — South Australia (observes ACDT in summer)
- Australia/Perth — Western Australia (AWST year-round)
- Australia/Darwin — Northern Territory (ACST year-round)
- Click Save project settings.
Understanding AEST vs AEDT
Eastern Australia observes Australian Eastern Standard Time (AEST, UTC+10) in winter and Australian Eastern Daylight Time (AEDT, UTC+11) in summer. Daylight saving starts on the first Sunday in October and ends on the first Sunday in April.
If your script timezone is set to Australia/Sydney and you create a trigger to fire at 7am, it will fire at 7am local time year-round — whether that is AEST or AEDT — because the timezone setting accounts for daylight saving transitions automatically. This is the correct approach.
What not to do: Avoid setting your script timezone to a fixed UTC offset like UTC+10. A fixed offset does not adjust for daylight saving, which means a trigger configured to fire at 7am AEST will fire at 8am AEDT when clocks spring forward. Use the named timezone (Australia/Sydney, Australia/Melbourne) rather than a UTC offset to get automatic daylight saving adjustment.
Queensland-Based Businesses
Queensland does not observe daylight saving. If your trigger schedule needs to align with Queensland business hours year-round, use Australia/Brisbane as the script timezone. A 7am trigger will always fire at 7am AEST, regardless of what other states are doing with their clocks.
For organisations with staff across multiple Australian states, the safest approach is to set the script timezone to Australia/Brisbane (fixed AEST) and document clearly that all trigger times are AEST. This eliminates the biannual confusion that occurs when NSW and Victoria change clocks but Queensland does not.
Using Timezone-Aware Date Formatting in Scripts
When your triggered function generates output that includes timestamps — report dates, log entries, email subject lines — always format dates using the script timezone rather than JavaScript's default UTC-based methods:
function getAESTTimestamp() {
var now = new Date();
// Always format using the script's configured timezone
return Utilities.formatDate(now, Session.getScriptTimeZone(), 'dd/MM/yyyy HH:mm:ss z');
}
// Example output: "19/02/2026 07:34:22 AEDT"
Session.getScriptTimeZone() returns the timezone string configured in Project Settings, so changing the project timezone automatically updates all formatted dates without requiring code changes.
Practical Example 1: Daily Summary Report via Email
This script reads a Google Sheet containing sales or task data and emails a daily summary to a nominated address every morning at 7am. It is built to run from a day timer trigger.
function sendDailyReport() {
var SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
var SHEET_NAME = 'Tasks';
var REPORT_EMAIL = 'manager@yourdomain.com.au';
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
Logger.log('Sheet not found: ' + SHEET_NAME);
return;
}
var lastRow = sheet.getLastRow();
if (lastRow < 2) {
// No data rows — nothing to report
return;
}
// Read all data (assumes headers in row 1)
var data = sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).getValues();
var total = data.length;
var complete = 0;
var overdue = 0;
var today = new Date();
today.setHours(0, 0, 0, 0);
for (var i = 0; i < data.length; i++) {
var status = data[i][2]; // Column C: Status
var dueDate = data[i][1]; // Column B: Due Date
if (status === 'Complete') {
complete++;
} else if (dueDate instanceof Date && dueDate < today) {
overdue++;
}
}
var inProgress = total - complete - overdue;
var reportDate = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'EEEE dd MMMM yyyy');
var subject = 'Daily Task Summary — ' + reportDate;
var body = [
'Good morning,',
'',
'Here is your task summary for ' + reportDate + ':',
'',
' Total tasks: ' + total,
' Complete: ' + complete,
' In progress: ' + inProgress,
' Overdue: ' + overdue,
'',
'View the full sheet: ' + ss.getUrl(),
'',
'This report was generated automatically by Google Apps Script.'
].join('\n');
MailApp.sendEmail(REPORT_EMAIL, subject, body);
Logger.log('Daily report sent to ' + REPORT_EMAIL);
}
To deploy this automation:
- Paste the function into a new Apps Script project.
- Replace
YOUR_SPREADSHEET_IDwith the ID from your spreadsheet URL. - Update
SHEET_NAMEandREPORT_EMAILto match your setup. - Run the function once manually to authorise Gmail and Sheets access.
- Create a Day timer trigger set to 7am to 8am, pointing to
sendDailyReport.
The report will arrive in the nominated inbox every morning within the 7am hour.
Practical Example 2: Weekly Data Cleanup
This script scans a Google Sheet for rows marked "Archived" or "Cancelled" and moves them to a separate archive sheet. Running it weekly on Sunday nights keeps the active sheet lean without permanently deleting records.
function weeklyDataCleanup() {
var SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
var ACTIVE_SHEET = 'Active';
var ARCHIVE_SHEET = 'Archive';
var STATUS_COLUMN = 3; // Column C contains the status
var ARCHIVE_STATUSES = ['Archived', 'Cancelled'];
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var active = ss.getSheetByName(ACTIVE_SHEET);
var archive = ss.getSheetByName(ARCHIVE_SHEET);
if (!active || !archive) {
Logger.log('Required sheet(s) not found. Check sheet names.');
return;
}
var lastRow = active.getLastRow();
if (lastRow < 2) return; // Nothing to process
var movedCount = 0;
// Iterate from the bottom up to avoid row index shifting when rows are deleted
for (var i = lastRow; i >= 2; i--) {
var statusCell = active.getRange(i, STATUS_COLUMN).getValue();
if (ARCHIVE_STATUSES.indexOf(statusCell) !== -1) {
var rowData = active.getRange(i, 1, 1, active.getLastColumn()).getValues();
// Append to archive sheet
archive.appendRow(rowData[0]);
// Delete from active sheet
active.deleteRow(i);
movedCount++;
}
}
Logger.log('Weekly cleanup: moved ' + movedCount + ' row(s) to archive.');
if (movedCount > 0) {
MailApp.sendEmail(
Session.getActiveUser().getEmail(),
'Weekly Cleanup Complete — ' + movedCount + ' row(s) archived',
'The weekly data cleanup ran successfully on ' +
Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'EEEE dd MMMM yyyy') +
'.\n\n' + movedCount + ' row(s) were moved from "' + ACTIVE_SHEET + '" to "' + ARCHIVE_SHEET + '".'
);
}
}
Trigger configuration: Create a Week timer trigger set to SUNDAY at 11pm to midnight. This runs the cleanup before the Monday morning workday begins, so the active sheet is clean when staff arrive.
Key technique — iterating from the bottom up: When deleting rows inside a loop, always iterate from the last row upward. If you iterate downward and delete row 5, what was row 6 becomes row 5 -- and the next iteration skips it. Iterating upward means each deletion only affects rows you have already processed.
Practical Example 3: Hourly Data Sync from an External Source
This script fetches data from an external JSON API and writes the response into a Google Sheet. Running it every hour keeps the sheet reasonably current without hitting the quota limits of a minute-based trigger.
function hourlyDataSync() {
var API_URL = 'https://api.yourservice.com.au/data'; // Replace with your API endpoint
var SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
var SHEET_NAME = 'Live Data';
var API_KEY = PropertiesService.getScriptProperties().getProperty('API_KEY');
try {
// Fetch data from the external API
var options = {
method: 'get',
headers: {
'Authorization': 'Bearer ' + API_KEY,
'Accept': 'application/json'
},
muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(API_URL, options);
var statusCode = response.getResponseCode();
if (statusCode !== 200) {
Logger.log('API returned status ' + statusCode + ': ' + response.getContentText());
return;
}
var jsonData = JSON.parse(response.getContentText());
if (!jsonData || !jsonData.items || jsonData.items.length === 0) {
Logger.log('No data returned from API.');
return;
}
// Write to Google Sheets
var ss = SpreadsheetApp.openById(SPREADSHEET_ID);
var sheet = ss.getSheetByName(SHEET_NAME);
// Clear existing data below the header row
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).clearContent();
}
// Build the data rows
var rows = jsonData.items.map(function(item) {
return [
item.id,
item.name,
item.value,
item.updatedAt,
Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'dd/MM/yyyy HH:mm')
];
});
// Write all rows in a single call (much faster than row-by-row)
sheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
Logger.log('Sync complete. ' + rows.length + ' row(s) written.');
} catch (e) {
Logger.log('Sync error: ' + e.message);
MailApp.sendEmail(
Session.getActiveUser().getEmail(),
'[ALERT] Hourly Data Sync Failed',
'The hourly sync encountered an error at ' +
Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'dd/MM/yyyy HH:mm z') +
'.\n\nError: ' + e.message
);
}
}
Storing the API key securely: Never hardcode API keys or passwords directly in script code. Use PropertiesService.getScriptProperties() instead. To set a property, run the following once from the editor:
function setApiKey() {
PropertiesService.getScriptProperties().setProperty('API_KEY', 'your-actual-api-key-here');
}
The key is stored encrypted in Google's infrastructure and is not visible in the script source code. Delete the setApiKey function after running it.
Trigger configuration: Create an Hour timer trigger set to fire every 1 hour. With an average execution time of 10–15 seconds per run, this consumes roughly 6 minutes of daily runtime quota — well within both consumer and Workspace account limits.
Affiliate and Partner Programs
Automating your Google Workspace workflows is far more powerful when you are on the right plan. All Apps Script features, including time-based triggers and the 6-hour daily trigger quota, are available on every paid Google Workspace plan.
- Google Workspace — Google's official referral program for new Workspace customers. Business Starter starts at around AUD $10 per user per month and includes full Apps Script access with no additional charge. If your team is growing and you are considering upgrading from a consumer Google account or an older G Suite plan, this link covers current Australian pricing and plan comparisons. Supporting this link helps us keep producing free, practical guides like this one at no extra cost to you.
Conclusion
Time-based triggers are what separate a useful script from a genuine automation. Writing the function is the first step. Scheduling it is what makes it actually save you time.
The key points to take away from this guide: use the Apps Script editor's Triggers UI for simple setups, use ScriptApp.newTrigger() when you need to deploy triggers programmatically or create them dynamically. Keep your script timezone set to a named Australian timezone (Australia/Sydney, Australia/Brisbane) rather than a fixed UTC offset to handle daylight saving correctly. Monitor your daily trigger runtime quota if you are running minute-based or hour-based triggers that execute substantial logic. And delete old triggers before creating new ones during updates to avoid duplicate executions.
The three examples in this guide — daily report email, weekly data cleanup, and hourly data sync — cover the majority of scheduled automation patterns used by Australian SMBs. Each one is production-ready and can be adapted to your specific data structures with minimal changes.
Automations that run on autopilot free up the human time that was previously spent remembering to do them. That is the whole point.
Need help with your Google Workspace setup? Contact our team for a free consultation.
Ash Ganda is the founder of CloudGeeks, a Google Workspace consultancy helping Australian SMBs get more from their cloud tools. He writes about practical technology strategies at insights.cloudgeeks.com.au.