Apps Script Từ A Đến Z - Bài 3: Triggers - Tự Động Hóa Theo Sự Kiện
17 phút đọc

Apps Script Từ A Đến Z - Bài 3: Triggers - Tự Động Hóa Theo Sự Kiện
Triggers là tính năng mạnh mẽ nhất của Apps Script — cho phép script tự chạy mà không cần bạn nhấn nút. Script có thể chạy khi file mở, khi dữ liệu thay đổi, hoặc theo lịch định sẵn. Đây là nền tảng của mọi automation thực sự.
Trong bài này bạn sẽ học:
- Simple triggers: onOpen, onEdit, onSelectionChange, onFormSubmit
- Installable triggers: time-driven và event-driven
- Tạo trigger bằng code (ScriptApp.newTrigger)
- Trigger context object và các thuộc tính
- Debug lỗi trigger thường gặp
- 4 use case thực tế đầy đủ code
Hai Loại Triggers Chính
Simple Triggers
Simple triggers là các function với tên đặc biệt, tự động chạy khi sự kiện tương ứng xảy ra:
| Function name | Khi nào chạy | Giới hạn |
|---|---|---|
onOpen(e) |
Khi file được mở | Không gửi email, không UrlFetch |
onEdit(e) |
Khi người dùng sửa ô | Không gửi email, không UrlFetch |
onSelectionChange(e) |
Khi người dùng chọn ô khác | Rất hạn chế |
onFormSubmit(e) |
Khi form được submit | Không gửi email, không UrlFetch |
doGet(e) |
HTTP GET request đến Web App | Chạy như người dùng anonymous |
// Simple trigger: tự động tạo menu khi mở file
function onOpen(e) {
const ui = SpreadsheetApp.getUi();
ui.createMenu('SheetStore Tools')
.addItem('Tạo báo cáo', 'generateReport')
.addItem('Gửi email', 'sendDailyEmail')
.addSeparator()
.addSubMenu(ui.createMenu('Tiện ích')
.addItem('Làm sạch dữ liệu', 'cleanData')
.addItem('Format bảng', 'formatTable'))
.addToUi();
// KHÔNG làm được trong simple trigger:
// GmailApp.sendEmail(...) → Lỗi!
// UrlFetchApp.fetch(...) → Lỗi!
}
// Simple trigger: theo dõi thay đổi
function onEdit(e) {
// e.range: Range đã được sửa
const range = e.range;
const sheet = range.getSheet();
const row = range.getRow();
const col = range.getColumn();
// Thêm timestamp vào cột cuối khi sửa
if (sheet.getName() === 'Orders' && col !== sheet.getLastColumn()) {
const lastCol = sheet.getLastColumn();
sheet.getRange(row, lastCol).setValue(new Date());
}
// Highlight hàng vừa sửa
sheet.getRange(row, 1, 1, sheet.getLastColumn())
.setBackground('#fff9c4');
}
Installable Triggers
Installable triggers mạnh hơn simple triggers — có thể gửi email, gọi API, và chạy dưới quyền người cài đặt (không phải người dùng hiện tại).
Time-Driven Triggers
// Tạo các loại time-driven triggers bằng code
function createTimeTriggers() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// Chạy mỗi phút (minutely)
ScriptApp.newTrigger('myFunction')
.timeBased()
.everyMinutes(5) // 1, 5, 10, 15, 30
.create();
// Chạy mỗi giờ
ScriptApp.newTrigger('sendHourlyReport')
.timeBased()
.everyHours(1) // 1, 2, 4, 6, 8, 12
.create();
// Chạy mỗi ngày lúc 8 giờ sáng
ScriptApp.newTrigger('sendDailyReport')
.timeBased()
.everyDays(1)
.atHour(8) // 0-23
.inTimezone('Asia/Ho_Chi_Minh')
.create();
// Chạy mỗi tuần thứ 2 lúc 9h
ScriptApp.newTrigger('weeklyBackup')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(9)
.inTimezone('Asia/Ho_Chi_Minh')
.create();
// Chạy mỗi tháng ngày 1
ScriptApp.newTrigger('monthlyReport')
.timeBased()
.onMonthDay(1)
.atHour(7)
.inTimezone('Asia/Ho_Chi_Minh')
.create();
// Chạy 1 lần vào ngày cụ thể
ScriptApp.newTrigger('oneTimeTask')
.timeBased()
.at(new Date('2027-03-15T09:00:00'))
.create();
Logger.log('Đã tạo tất cả time triggers');
}
Event-Driven Installable Triggers
function createEventTriggers() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// onEdit installable (mạnh hơn simple trigger, có thể gửi email)
ScriptApp.newTrigger('onEditInstallable')
.forSpreadsheet(ss)
.onEdit()
.create();
// onChange: khi cấu trúc sheet thay đổi (thêm/xóa sheet, rename...)
ScriptApp.newTrigger('onChangeHandler')
.forSpreadsheet(ss)
.onChange()
.create();
// onFormSubmit (từ Google Form liên kết)
ScriptApp.newTrigger('processFormResponse')
.forSpreadsheet(ss)
.onFormSubmit()
.create();
Logger.log('Event triggers created');
}
Quản Lý Triggers
// Liệt kê tất cả triggers
function listTriggers() {
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
Logger.log('Function: %s | Type: %s | Source: %s',
trigger.getHandlerFunction(),
trigger.getEventType(),
trigger.getTriggerSource()
);
});
Logger.log('Tổng số triggers: ' + triggers.length);
}
// Xóa tất cả triggers của một function
function deleteTriggersByFunction(functionName) {
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getHandlerFunction() === functionName) {
ScriptApp.deleteTrigger(trigger);
Logger.log('Đã xóa trigger: ' + trigger.getUniqueId());
}
});
}
// Xóa tất cả triggers (reset)
function deleteAllTriggers() {
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => ScriptApp.deleteTrigger(trigger));
Logger.log('Đã xóa ' + triggers.length + ' triggers');
}
// Tránh tạo duplicate triggers
function safeCreateTrigger(functionName) {
const triggers = ScriptApp.getProjectTriggers();
// Kiểm tra đã có trigger chưa
const exists = triggers.some(t => t.getHandlerFunction() === functionName);
if (!exists) {
ScriptApp.newTrigger(functionName)
.timeBased()
.everyDays(1)
.atHour(8)
.create();
Logger.log('Tạo trigger mới cho: ' + functionName);
} else {
Logger.log('Trigger đã tồn tại cho: ' + functionName);
}
}
Trigger Context Object (Event Object)
// onEdit event object
function onEditInstallable(e) {
if (!e) {
Logger.log('Chạy thủ công, không có event object');
return;
}
// e.source: Spreadsheet object
const ss = e.source;
Logger.log('File: ' + ss.getName());
// e.range: Range vừa được edit
const range = e.range;
Logger.log('Cell: ' + range.getA1Notation());
Logger.log('Sheet: ' + range.getSheet().getName());
Logger.log('Row: ' + range.getRow());
Logger.log('Column: ' + range.getColumn());
// e.value: Giá trị mới (chỉ có với single cell edit)
Logger.log('New value: ' + e.value);
// e.oldValue: Giá trị cũ (nếu có)
Logger.log('Old value: ' + e.oldValue);
// e.user: Người dùng đã edit
if (e.user) {
Logger.log('User: ' + e.user.getEmail());
}
}
// onFormSubmit event object
function processFormResponse(e) {
const response = e.response;
const namedValues = e.namedValues; // { 'Question': ['Answer'], ... }
Logger.log('Form submitted at: ' + response.getTimestamp());
Logger.log('Email: ' + namedValues['Email'][0]);
Logger.log('Name: ' + namedValues['Họ tên'][0]);
// Xử lý response...
const email = namedValues['Email'][0];
const name = namedValues['Họ tên'][0];
// Gửi email xác nhận (được phép trong installable trigger)
GmailApp.sendEmail(email, 'Xác nhận đăng ký',
'Xin chào ' + name + ', đăng ký của bạn đã được ghi nhận!');
}
// onChange event object
function onChangeHandler(e) {
Logger.log('Change type: ' + e.changeType);
// changeType: INSERT_ROW, INSERT_COLUMN, REMOVE_ROW, REMOVE_COLUMN,
// INSERT_GRID, REMOVE_GRID, FORMAT, OTHER
if (e.changeType === 'INSERT_ROW') {
Logger.log('Có hàng mới được thêm vào');
}
}
Debug Triggers: Các Lỗi Phổ Biến
Lỗi #1: Trigger không chạy
Nguyên nhân thường gặp: (1) Quota hết, (2) Script lỗi nhưng không có notification, (3) Sai tên function.
Nguyên nhân thường gặp: (1) Quota hết, (2) Script lỗi nhưng không có notification, (3) Sai tên function.
// Debug pattern chuẩn cho trigger
function myTriggerFunction(e) {
try {
// Logic chính
doMainWork(e);
} catch (error) {
// Log lỗi vào sheet để dễ debug
logError(error, 'myTriggerFunction', e);
// Optionally gửi email báo lỗi
// GmailApp.sendEmail('admin@company.com', 'Trigger Error', error.toString());
}
}
function logError(error, functionName, eventObj) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let logSheet = ss.getSheetByName('ErrorLog');
if (!logSheet) {
logSheet = ss.insertSheet('ErrorLog');
logSheet.appendRow(['Timestamp', 'Function', 'Error', 'Event']);
}
logSheet.appendRow([
new Date(),
functionName,
error.toString(),
JSON.stringify(eventObj || {})
]);
}
// Tránh infinite loop trong onEdit
function onEditSafe(e) {
// Lỗi phổ biến: onEdit chỉnh sửa sheet → kích hoạt onEdit lại → loop!
const range = e.range;
const sheet = range.getSheet();
// Guard: chỉ xử lý sheet 'Data', không phải 'ErrorLog' hay 'Timestamp'
if (sheet.getName() !== 'Data') return;
// Guard: không xử lý cột timestamp (cột cuối) để tránh loop
const lastCol = sheet.getLastColumn();
if (range.getColumn() === lastCol) return;
// Guard: chỉ xử lý single cell edit
if (range.getNumRows() > 1 || range.getNumColumns() > 1) return;
// An toàn để ghi timestamp
sheet.getRange(range.getRow(), lastCol).setValue(new Date());
}
Xem Trigger Execution Logs
// Trigger logs xem tại: Executions (menu bên trái trong IDE)
// Hoặc View → Stackdriver Logging
// Tất cả console.log trong trigger đều vào Stackdriver
function myTriggerWithLogs(e) {
console.log('Trigger started:', new Date().toISOString());
console.log('Event:', JSON.stringify(e));
try {
// Work here
console.log('Work completed successfully');
} catch (err) {
console.error('Error in trigger:', err.message, err.stack);
}
}
Use Case 1: Auto Backup Hàng Ngày
// Setup: Chạy setupDailyBackup() một lần để tạo trigger
function setupDailyBackup() {
deleteTriggersByFunction('dailyBackup');
ScriptApp.newTrigger('dailyBackup')
.timeBased()
.everyDays(1)
.atHour(2) // 2 giờ sáng
.inTimezone('Asia/Ho_Chi_Minh')
.create();
Logger.log('Daily backup trigger đã được tạo');
}
function dailyBackup() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const today = Utilities.formatDate(new Date(), 'Asia/Ho_Chi_Minh', 'yyyy-MM-dd');
// Tạo copy file
const sourceFile = DriveApp.getFileById(ss.getId());
const backupFolder = getOrCreateFolder('SheetStore Backups');
const backup = sourceFile.makeCopy('Backup_' + today + '_' + ss.getName(), backupFolder);
// Xóa backup cũ hơn 30 ngày
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const files = backupFolder.getFiles();
let deletedCount = 0;
while (files.hasNext()) {
const file = files.next();
if (file.getDateCreated() < thirtyDaysAgo && file.getName() !== backup.getName()) {
file.setTrashed(true);
deletedCount++;
}
}
console.log('Backup created: ' + backup.getName() + ', deleted ' + deletedCount + ' old backups');
}
function getOrCreateFolder(folderName) {
const folders = DriveApp.getFoldersByName(folderName);
if (folders.hasNext()) return folders.next();
return DriveApp.createFolder(folderName);
}
Use Case 2: Gửi Email Khi Form Submit
// Setup: Chạy setupFormTrigger() một lần
function setupFormTrigger() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
deleteTriggersByFunction('onFormSubmitHandler');
ScriptApp.newTrigger('onFormSubmitHandler')
.forSpreadsheet(ss)
.onFormSubmit()
.create();
Logger.log('Form trigger created');
}
function onFormSubmitHandler(e) {
const namedValues = e.namedValues;
// Lấy dữ liệu từ form
const customerName = namedValues['Họ và tên'] ? namedValues['Họ và tên'][0] : '';
const customerEmail = namedValues['Email'] ? namedValues['Email'][0] : '';
const orderType = namedValues['Loại đơn hàng'] ? namedValues['Loại đơn hàng'][0] : '';
const notes = namedValues['Ghi chú'] ? namedValues['Ghi chú'][0] : 'Không có';
if (!customerEmail) {
console.error('Không có email khách hàng');
return;
}
// Email cho khách hàng
const customerEmailBody =
'Xin chào ' + customerName + ',
' +
'Chúng tôi đã nhận được yêu cầu của bạn:
' +
'- Loại: ' + orderType + '
' +
'- Ghi chú: ' + notes + '
' +
'Chúng tôi sẽ liên hệ trong 24 giờ làm việc.
' +
'Trân trọng,
Đội ngũ SheetStore';
GmailApp.sendEmail(customerEmail,
'[SheetStore] Xác nhận yêu cầu của bạn',
customerEmailBody);
// Email cho admin
const adminEmail = 'admin@sheetstore.vn';
GmailApp.sendEmail(adminEmail,
'[NEW] Yêu cầu mới từ ' + customerName,
'Khách hàng: ' + customerName + '
' +
'Email: ' + customerEmail + '
' +
'Loại: ' + orderType + '
' +
'Ghi chú: ' + notes + '
' +
'Thời gian: ' + new Date()
);
console.log('Emails sent for: ' + customerEmail);
}
Use Case 3: Alert Khi Giá Trị Vượt Ngưỡng
function onEditAlertThreshold(e) {
if (!e) return;
const range = e.range;
const sheet = range.getSheet();
// Chỉ theo dõi sheet 'KPIs', cột C (Target), cột D (Actual)
if (sheet.getName() !== 'KPIs') return;
if (range.getColumn() !== 4) return; // Chỉ khi cập nhật cột D (Actual)
const row = range.getRow();
if (row < 2) return; // Bỏ qua header
const metricName = sheet.getRange(row, 1).getValue();
const target = parseFloat(sheet.getRange(row, 3).getValue() || 0);
const actual = parseFloat(e.value || 0);
if (target === 0) return;
const achievement = (actual / target) * 100;
// Alert nếu đạt dưới 80% target
if (achievement < 80) {
const adminEmail = Session.getActiveUser().getEmail();
GmailApp.sendEmail(adminEmail,
'[CẢNH BÁO] KPI thấp: ' + metricName,
'Cảnh báo KPI:
' +
'Chỉ số: ' + metricName + '
' +
'Mục tiêu: ' + target.toLocaleString() + '
' +
'Thực tế: ' + actual.toLocaleString() + '
' +
'Tỷ lệ đạt: ' + achievement.toFixed(1) + '%
' +
'Vui lòng kiểm tra và có hành động kịp thời.'
);
// Highlight ô màu đỏ
range.setBackground('#ffcdd2');
} else if (achievement >= 100) {
// Đạt target: màu xanh
range.setBackground('#c8e6c9');
} else {
// Bình thường: màu vàng
range.setBackground('#fff9c4');
}
}
Use Case 4: Tự Động Format Khi Thêm Row Mới
function onEditAutoFormat(e) {
if (!e) return;
const range = e.range;
const sheet = range.getSheet();
if (sheet.getName() !== 'Orders') return;
// Chỉ xử lý khi thêm dữ liệu vào cột A (SKU/ID đơn hàng)
if (range.getColumn() !== 1 || range.getRow() < 2) return;
if (!e.value || e.value === '') return;
const row = range.getRow();
const numCols = 8; // Số cột trong bảng orders
const fullRow = sheet.getRange(row, 1, 1, numCols);
// Alternating row colors
const bgColor = (row % 2 === 0) ? '#f8f9fa' : '#ffffff';
fullRow.setBackground(bgColor);
// Format cột ngày (cột 2)
sheet.getRange(row, 2).setNumberFormat('dd/MM/yyyy HH:mm');
// Auto-fill ngày hiện tại nếu cột 2 trống
const dateCell = sheet.getRange(row, 2);
if (!dateCell.getValue()) {
dateCell.setValue(new Date());
}
// Format cột tiền (cột 5, 6, 7)
sheet.getRange(row, 5, 1, 3).setNumberFormat('#,##0 "đ"');
// Auto-generate Order ID nếu cần
const orderIdCell = sheet.getRange(row, 1);
if (orderIdCell.getValue() === 'AUTO') {
const orderId = 'ORD-' + Utilities.formatDate(new Date(), 'Asia/Ho_Chi_Minh', 'yyyyMMdd')
+ '-' + String(row - 1).padStart(4, '0');
orderIdCell.setValue(orderId);
}
console.log('Auto-formatted row ' + row);
}
Tóm Tắt: Chọn Trigger Phù Hợp
| Nhu cầu | Dùng trigger nào |
|---|---|
| Thêm menu khi mở file | Simple onOpen |
| Gửi email khi form submit | Installable onFormSubmit |
| Backup hàng ngày lúc 2h sáng | Time-driven daily |
| Alert khi ô thay đổi + gửi email | Installable onEdit |
| Format nhanh không cần email | Simple onEdit |
| Sync data mỗi 5 phút | Time-driven minutely |
Bài tiếp theo: Bài 4 — GmailApp: Gửi Email, Đọc Inbox và Tạo Draft
Sau Triggers, chúng ta sẽ học GmailApp để xử lý email tự động: gửi email HTML, đọc inbox, tạo draft và quản lý labels. Kết hợp với Triggers từ bài này để tạo email automation hoàn chỉnh.
Sau Triggers, chúng ta sẽ học GmailApp để xử lý email tự động: gửi email HTML, đọc inbox, tạo draft và quản lý labels. Kết hợp với Triggers từ bài này để tạo email automation hoàn chỉnh.
📚 Bài Viết Liên Quan
- Template Google Sheets Báo Cáo Bán Hàng Theo Vùng và Đại Lý 2027: Phân Tích Đa Chiều
- Google Sheets Nâng Cao Bài 9: Bảo Mật, Phân Quyền và Chia Sẻ Chuyên Nghiệp
- Google Sheets Nâng Cao Bài 4: Hàm QUERY - Lọc và Phân Tích Dữ Liệu Chuyên Nghiệp
- Template Google Sheets Quản Lý Phòng Khám và Bệnh Viện Nhỏ 2027
Chia sẻ bài viết:
Tuân Hoang
Đội ngũ SheetStore
Bạn thấy bài viết hữu ích?
Đăng ký nhận thông báo khi có bài viết mới.