# Apps Script là gì? Cách dùng Apps Script tự động hoá Google Sheets cho người mới

Apps Script là ngôn ngữ lập trình (dựa trên JavaScript) của Google, chạy trên máy chủ Google, dùng để tự động hoá Google Sheets: thêm hàm mới dùng như công thức, tự ghi thời gian khi sửa ô, chạy việc theo giờ và gửi email. Bạn mở nó trong file Google Sheets ở **Tiện ích mở rộng › Apps Script**, không cần cài gì. Bài này đi từ hàm đầu tiên tới hàm tuỳ chỉnh, onEdit, trigger hằng ngày, gửi email, màn hình cấp quyền "chưa xác minh", hạn mức của Google và cách kiểm tra một đoạn script lạ trước khi chép.

Đăng 2026-10-26 · 13 phút đọc · Apps Script, Google Sheets, Tự động hoá, Trigger, Hàm tuỳ chỉnh

## Apps Script là gì, làm được gì trong Google Sheets?

Google Apps Script là môi trường viết mã miễn phí đi kèm tài khoản Google. Mã viết bằng JavaScript, lưu ngay trong file (gọi là script gắn với file) hoặc thành dự án riêng, và chạy trên máy chủ Google bằng chính tài khoản của bạn — tắt máy tính thì trigger theo giờ vẫn chạy.

*Nguyên tắc: công thức trước, Apps Script sau. Công thức tính lại tức thì và ai cũng đọc được.*

| Bạn muốn | Công thức làm được? | Apps Script |
|---|---|---|
| Tính toán trên dữ liệu trong bảng | Được — nên dùng công thức trước | Chỉ khi công thức quá rối |
| Ghi lại thời điểm một ô được sửa | Không (NOW() luôn đổi) | onEdit ghi một lần, giữ nguyên |
| Gửi email, tạo file Docs, PDF | Không | MailApp, DocumentApp, DriveApp |
| Chạy việc mỗi sáng 7 giờ | Không | Trigger theo giờ |
| Thêm menu, hộp thoại riêng | Không | onOpen, HtmlService |
| Lấy dữ liệu từ file Sheets khác | Được — [IMPORTRANGE](https://www.hitava.cloud/blog/importrange-google-sheets) | Không cần |

Apps Script **chỉ chạy trên Google Sheets**. File .xlsx tải về mở bằng Excel thì script không chạy (Excel có VBA, ngôn ngữ khác). File .xlsx tải lên Drive cũng phải lưu thành định dạng Google Sheets thì mới gắn script được. So sánh hai bên ở bài [Google Sheets hay Excel](https://www.hitava.cloud/blog/google-sheets-hay-excel).

## Cách mở Apps Script và viết hàm đầu tiên

1. Mở file Google Sheets › **Tiện ích mở rộng › Apps Script**. Một tab mới mở ra với dự án gắn với file, có sẵn tệp Mã.gs chứa hàm trống myFunction.
2. Xoá đoạn mẫu, dán hàm xinChao bên dưới, bấm biểu tượng lưu (hoặc Ctrl + S).
3. Chọn hàm xinChao ở ô chọn hàm trên thanh công cụ, bấm **Chạy** (Run).
4. Lần đầu chạy, Google hỏi quyền — làm theo phần cấp quyền ở dưới. Xong quay lại tab bảng tính: ô A1 có dòng chữ, khung nhật ký thực thi có dòng Logger.log.

**Hàm đầu tiên**: SpreadsheetApp là cửa vào bảng tính; getRange('A1') chọn ô; setValue ghi giá trị; Logger.log in ra nhật ký để kiểm tra.

```
function xinChao() {
  const sheet = SpreadsheetApp.getActiveSheet();
  sheet.getRange('A1').setValue('Xin chào từ Apps Script');
  Logger.log('Đã ghi vào ' + sheet.getName() + ' lúc ' + new Date());
}
```

Ba thứ bạn sẽ gặp trong mọi script: **SpreadsheetApp** (file, trang tính, ô), **getValues / setValues** (đọc, ghi cả vùng ô dưới dạng mảng hai chiều), **Logger.log** hoặc console.log (in giá trị để dò lỗi). Mảng getValues đánh số từ 0: cột A là r[0], cột D là r[3]; còn getRange(dòng, cột) đánh số từ 1.

**Đặt múi giờ trước khi làm gì với ngày giờ:** Script có múi giờ riêng, có thể khác múi giờ của bảng tính. Vào Cài đặt dự án (biểu tượng bánh răng bên trái trình soạn) đặt múi giờ (GMT+07:00) Bangkok, Hà Nội — giống trong Tệp › Cài đặt của bảng tính. Lệch múi giờ là lý do hay gặp nhất khiến ngày ghi ra bị sai một ngày.

## Hàm tuỳ chỉnh (custom function): tự tạo hàm dùng như công thức

Hàm bạn viết trong Apps Script dùng được ngay trong ô như hàm có sẵn. Ví dụ hàm bỏ dấu tiếng Việt — việc mà công thức thường phải lồng hàng chục SUBSTITUTE:

**Hàm BODAU**: normalize('NFD') tách chữ và dấu, replace xoá các dấu, riêng đ/Đ phải đổi tay. Dòng Array.isArray cho phép truyền cả vùng ô. Thẻ @customfunction làm hàm hiện trong gợi ý khi gõ.

```
/**
 * Bỏ dấu tiếng Việt: "Nguyễn Thu Hà" → "Nguyen Thu Ha".
 * @param {string|Array<Array<string>>} text Ô hoặc vùng ô cần bỏ dấu
 * @return Chữ không dấu
 * @customfunction
 */
function BODAU(text) {
  if (Array.isArray(text)) return text.map(row => row.map(BODAU));
  return String(text)
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/đ/g, 'd')
    .replace(/Đ/g, 'D');
}
```

Thử lại: B2 = Nguyễn Thu Hà → **Nguyen Thu Ha**; B2 = Đặng Văn Đức → **Dang Van Duc**. Truyền vùng =BODAU(B2:B100) thì kết quả tràn xuống các ô bên dưới, miễn các ô đó trống.

```
=BODAU(B2)
```

- Tên hàm không được kết thúc bằng dấu gạch dưới và không trùng hàm có sẵn; viết hoa hay thường đều gọi được.
- Mỗi lần gọi phải xong trong **30 giây**.
- Hàm tuỳ chỉnh **chỉ trả kết quả** vào ô của nó (và các ô tràn ra); không ghi được ô khác, không gửi email, không dùng dịch vụ cần cấp quyền.
- Đối số phải cố định: đừng đưa NOW(), RAND() vào làm đối số — ô sẽ báo đang tải mãi. Hàm chỉ tính lại khi ô đối số thay đổi.
- Dùng hàng nghìn ô =BODAU(...) riêng lẻ thì chậm; truyền một vùng vào một ô sẽ nhanh hơn nhiều — giống tinh thần của [ARRAYFORMULA](https://www.hitava.cloud/blog/cach-dung-arrayformula-google-sheets).

## onEdit: tự động ghi thời gian khi sửa ô

onEdit là **trigger đơn giản**: chỉ cần đặt tên hàm đúng là onEdit, Google tự gọi mỗi khi có người sửa một ô, không cần cấp quyền. Ví dụ tab Công việc: cột E Trạng thái, cột F Ngày xong. Khi ai đó chọn Xong ở cột E, cột F cùng dòng tự ghi ngày giờ — và không đổi nữa, khác hẳn hàm NOW().

e.range là ô vừa sửa, e.value là giá trị mới (chỉ có khi sửa một ô). Hàm thoát ngay nếu không phải tab Công việc, cột E — để không chạy thừa trên cả file.

```
function onEdit(e) {
  const range = e.range;
  const sheet = range.getSheet();
  if (sheet.getName() !== 'Công việc') return;            // chỉ tab Công việc
  if (range.getColumn() !== 5 || range.getRow() < 2) return; // chỉ cột E, bỏ dòng tiêu đề
  const o = range.offset(0, 1);                            // ô cột F cùng dòng
  if (e.value === 'Xong' && o.getValue() === '') {
    o.setValue(new Date());
  }
}
```

![Bảng Công việc 4 dòng với cột Việc, Dự án, Người phụ trách, Hạn, Trạng thái, Ngày xong: dòng 2 Gửi báo giá khách Minh Phát vừa đổi sang Xong và cột F tự ghi 26/10/2026 09:15; dòng 3 Đối chiếu công nợ hạn 23/10 vẫn Đang làm](https://www.hitava.cloud/img/blog/cach-dung-apps-script-google-sheets/onedit.webp)
*Thử lại: Lan chọn Xong ở E2 lúc 09:15 ngày 26/10/2026 → F2 = 26/10/2026 09:15. Định dạng cột F là Ngày giờ.*

**Bẫy hay gặp với onEdit:** onEdit không chạy khi ô bị đổi bởi script khác, bởi công thức tính lại hay bởi IMPORTRANGE — chỉ chạy khi người dùng sửa. Dán một lúc nhiều ô thì e.value không có giá trị, nên ví dụ trên bỏ qua. Bấm Chạy hàm onEdit ngay trong trình soạn sẽ báo lỗi vì không có e — muốn thử thì sửa một ô trong bảng. Trigger đơn giản cũng tối đa 30 giây và không gửi được email.

Cùng nhóm với onEdit là **onOpen**: chạy khi mở file, hay dùng để thêm một menu riêng cạnh menu Trợ giúp, bấm vào là gọi hàm của bạn — người dùng không phải mở trình soạn.

Menu tự hiện sau khi tải lại trang bảng tính (F5). Hai hàm guiNhacHan và caiNhacHangNgay ở phần dưới.

```
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Tiện ích của tôi')
    .addItem('Gửi nhắc việc trễ hạn ngay', 'guiNhacHan')
    .addItem('Bật nhắc mỗi sáng', 'caiNhacHangNgay')
    .addToUi();
}
```

## Trigger theo giờ và gửi email nhắc hạn tự động

Gửi email cần quyền, nên không làm được trong onEdit. Cách đúng: viết một hàm gửi email, rồi cài **trigger theo giờ** (installable trigger) để Google gọi hàm đó mỗi sáng bằng tài khoản của bạn.

**Gửi danh sách việc trễ hạn**: tab Công việc như hình trên (A Việc, C Người phụ trách, D Hạn, E Trạng thái). Thử lại với bảng trên ngày 26/10/2026: chỉ dòng Đối chiếu công nợ tháng 10 (hạn 23/10, Đang làm) bị lọc ra → email tiêu đề "1 việc trễ hạn".

```
const EMAIL_NHAN = 'ban@congty.vn';   // đổi thành email của bạn

function guiNhacHan() {
  const sheet = SpreadsheetApp.getActive().getSheetByName('Công việc');
  const rows = sheet.getDataRange().getValues();   // đọc cả bảng MỘT lần
  const homNay = new Date();
  homNay.setHours(0, 0, 0, 0);
  const tre = rows.slice(1).filter(r =>
    r[4] !== 'Xong' && r[3] instanceof Date && r[3] < homNay);
  if (tre.length === 0) return;                     // không có việc trễ thì thôi
  const tz = Session.getScriptTimeZone();
  const noiDung = tre.map(r =>
    '- ' + r[0] + ' (' + r[2] + '), hạn ' + Utilities.formatDate(r[3], tz, 'dd/MM/yyyy')
  ).join('\n');
  MailApp.sendEmail(EMAIL_NHAN, tre.length + ' việc trễ hạn', noiDung);
}
```

**Cài nhắc mỗi sáng**: chạy hàm này một lần (từ trình soạn hoặc menu Tiện ích của tôi). atHour(7) nghĩa là chạy vào một lúc nào đó trong khoảng 7 – 8 giờ theo múi giờ của script, không đúng từng phút.

```
function caiNhacHangNgay() {
  // xoá trigger cũ của hàm này để không bị gửi trùng
  ScriptApp.getProjectTriggers()
    .filter(t => t.getHandlerFunction() === 'guiNhacHan')
    .forEach(t => ScriptApp.deleteTrigger(t));
  ScriptApp.newTrigger('guiNhacHan')
    .timeBased()
    .everyDays(1)
    .atHour(7)
    .create();
}
```

![So sánh ba cách cho script tự chạy: hàm tuỳ chỉnh không cần quyền, không ghi ô khác, 30 giây; trigger đơn giản onOpen, onEdit ghi ô được nhưng không gửi email, 30 giây; trigger cài đặt cần quyền, gửi email được, 6 phút mỗi lần, 90 phút mỗi ngày với Gmail](https://www.hitava.cloud/img/blog/cach-dung-apps-script-google-sheets/trigger.webp)

Trigger đã cài cũng xem và xoá được trong trình soạn, ở mục trigger (biểu tượng đồng hồ) trên thanh bên trái. Mỗi trigger hiện tên hàm, loại sự kiện, tài khoản chạy; lần chạy lỗi được ghi ở mục lần thực thi để bạn xem lý do.

**Cảnh báo khi gửi email bằng script:** Email gửi đi **từ chính tài khoản Gmail của bạn**. Gửi nhầm vòng lặp có thể gửi hàng trăm thư trong vài giây; Google còn giới hạn số người nhận mỗi ngày (phần hạn mức bên dưới). Luôn thử với EMAIL_NHAN là email của chính bạn trước, và đừng gửi cho khách hàng danh sách lấy từ cột chưa kiểm tra.

## Cấp quyền cho Apps Script: màn hình "Google chưa xác minh ứng dụng này"

Lần đầu chạy một hàm cần quyền (ghi bảng tính, gửi email, tạo trigger), Google hiện cửa sổ cần cấp quyền. Các bước:

1. Bấm Tiếp tục (hoặc Xem lại quyền) › chọn đúng tài khoản Google đang giữ file.
2. Màn hình "Google chưa xác minh ứng dụng này" › bấm **Nâng cao** (Advanced) ở góc dưới bên trái › **Chuyển đến … (không an toàn)**.
3. Đọc danh sách quyền script xin › Cho phép (Allow).

Google chỉ xác minh những ứng dụng nộp hồ sơ để phát hành công khai. Script nằm trong file của bạn, chạy bằng tài khoản của bạn, không phát hành cho ai — nên nó luôn hiện dòng "chưa xác minh", kể cả hàm xinChao bạn tự viết. Đây không phải lỗi. Nhưng chính vì vậy, **bước 3 là lúc phải đọc kỹ**: bạn đang cho đoạn mã đó quyền làm mọi thứ trong danh sách bằng tài khoản của mình.

Tài khoản Google Workspace của công ty có thể không có nút Nâng cao nếu quản trị viên chặn script chưa duyệt — khi đó nhờ quản trị viên cho phép. Quyền đã cấp xem và thu hồi được ở trang quản lý tài khoản Google, phần ứng dụng bên thứ ba có quyền truy cập: [myaccount.google.com/connections](https://myaccount.google.com/connections).

## Giới hạn (quota) của Apps Script cần biết

Google giới hạn mỗi tài khoản một lượng chạy mỗi ngày. Các con số hay đụng tới nhất, theo trang hạn mức chính thức của Apps Script (Google ghi rõ có thể thay đổi bất kỳ lúc nào không báo trước):

*Nguồn: developers.google.com/apps-script/guides/services/quotas, kiểm tra ngày 26/10/2026. Hạn mức tính theo người dùng, đặt lại sau 24 giờ kể từ yêu cầu đầu tiên.*

| Hạn mức | Tài khoản Gmail cá nhân | Google Workspace |
|---|---|---|
| Người nhận email mỗi ngày (MailApp) | 100 | 1.500 |
| Thời gian chạy mỗi lần thực thi | 6 phút | 6 phút |
| Hàm tuỳ chỉnh mỗi lần gọi | 30 giây | 30 giây |
| Tổng thời gian trigger chạy mỗi ngày | 90 phút | 6 giờ |
| Số trigger mỗi người mỗi script | 20 | 20 |
| Số lần gọi UrlFetch mỗi ngày | 20.000 | 100.000 |

Xem còn gửi được bao nhiêu người nhận trong hôm nay trước khi gửi hàng loạt.

```
Logger.log('Còn gửi được: ' + MailApp.getRemainingDailyQuota() + ' người nhận hôm nay');
```

Đụng giới hạn 6 phút thường là do đọc ghi **từng ô một** trong vòng lặp. Mỗi lần getValue, setValue là một lần gọi tới bảng tính; 1.000 dòng là 2.000 lần gọi. Đọc cả vùng một lần, tính trong bộ nhớ, ghi lại một lần thì cùng việc đó chỉ mất vài giây:

Hai đoạn cho cùng kết quả: cột G = cột D × 2 cho dòng 2 tới 1000. Đoạn dưới chỉ gọi bảng tính 2 lần.

```
// CHẬM: mỗi ô một lần gọi tới bảng tính
for (let i = 2; i <= 1000; i++) {
  sheet.getRange(i, 7).setValue(sheet.getRange(i, 4).getValue() * 2);
}

// NHANH: đọc một lần, tính trong bộ nhớ, ghi một lần
const d = sheet.getRange(2, 4, 999, 1).getValues();
sheet.getRange(2, 7, 999, 1).setValues(d.map(r => [r[0] * 2]));
```

## Bảo mật: kiểm tra gì trước khi chép một đoạn Apps Script lạ?

Script chạy với **đúng quyền của tài khoản bạn**. Một đoạn mã chép từ nhóm Facebook, diễn đàn hay do người lạ gửi qua Zalo có thể đọc file Drive, gửi thư từ Gmail của bạn, hoặc gửi dữ liệu ra một máy chủ ngoài. Trước khi bấm Cho phép, kiểm tra:

| Thấy gì trong mã hoặc màn hình quyền | Vì sao đáng ngờ |
|---|---|
| UrlFetchApp.fetch tới một địa chỉ lạ | Dữ liệu trong file có thể bị gửi ra ngoài |
| GmailApp, quyền đọc hoặc xoá thư | Script nhắc hạn chỉ cần gửi thư (MailApp), không cần đọc hộp thư |
| DriveApp.getFiles(), quyền xem và xoá mọi tệp Drive | Script cho một bảng tính hiếm khi cần đụng tới mọi file |
| Mã bị làm rối: chuỗi dài vô nghĩa, eval, atob | Cố tình che việc thật sự làm |
| Quyền không khớp với việc script hứa làm | Ví dụ hàm bỏ dấu mà xin quyền gửi email |

- Thử script lạ trên **bản sao** của file, bằng tài khoản phụ, trước khi đưa vào file thật.
- Không dán mật khẩu, mã API, token ngân hàng vào thẳng mã — người được quyền sửa file đều xem được.
- Chia sẻ quyền chỉnh sửa file cẩn thận: người sửa được file cũng sửa được script gắn với file. Khoá các vùng quan trọng theo bài [khoá ô công thức Google Sheets](https://www.hitava.cloud/blog/khoa-o-cong-thuc-google-sheets).
- Không dùng script nữa thì xoá trigger và thu hồi quyền ở trang quản lý tài khoản Google.

## Mẫu Google Sheets có sẵn Apps Script

Viết vài hàm như trên mất một buổi. Viết cả một hộp thoại có Kanban kéo thả, email nhắc hạn, báo cáo tuần, kết nối Google Calendar thì mất nhiều tuần. Các mẫu PRO của HiTaVa đi kèm gói Apps Script viết sẵn, mã nguồn để mở trong Code.gs và các file .html, cài bằng cách dán vào Tiện ích mở rộng › Apps Script theo hướng dẫn từng bước.

![Màn hình Tự động hoá của Bảng theo dõi công việc PRO: dòng trạng thái Trigger của bạn 0/20, tài khoản chạy, múi giờ bảng tính Asia/Ho_Chi_Minh, mail còn gửi được hôm nay 97; các thẻ Nhắc và báo cáo, Quy tắc, Kết nối, Chia sẻ, Dữ liệu; khung Google Calendar, Google Chat và Telegram](https://www.hitava.cloud/img/blog/cach-dung-apps-script-google-sheets/tu-dong-hoa.webp)
*Hộp thoại Tự động hoá hiện luôn số trigger đã dùng trên 20 và số mail còn gửi được trong ngày — hai hạn mức ở phần trên.*

→ [Bảng theo dõi công việc PRO làm sẵn những thứ bài này hướng dẫn tự viết: nhắc hạn qua email, báo cáo tuần, việc định kỳ, đồng bộ Google Calendar, tin nhắn Google Chat hoặc Telegram, Kanban kéo thả — bật tắt trong hộp thoại, không phải mở trình soạn. Phần tự động hoá chỉ chạy trên Google Sheets; mở bằng Excel vẫn dùng được công thức và biểu đồ.](https://www.hitava.cloud/san-pham/bang-theo-doi-cong-viec-pro)

Chủ nhà trọ thì [Quản lý phòng trọ PRO](https://www.hitava.cloud/san-pham/quan-ly-phong-tro-pro) dùng Apps Script để tạo hoá đơn có mã QR VietQR, PDF lưu Drive, gửi email hàng loạt, đối soát sao kê ngân hàng và email nhắc việc mỗi sáng; phần tính tiền, công nợ vẫn chạy bằng công thức trên cả Excel.

Video: [Hướng dẫn cài đặt và dùng Bảng theo dõi công việc PRO, gồm phần cài Apps Script](https://youtu.be/lYWfsigra6Y)

## Đọc tiếp

- [Hàm ARRAYFORMULA trong Google Sheets](https://www.hitava.cloud/blog/cach-dung-arrayformula-google-sheets): Một công thức cho cả cột — thử trước khi viết script
- [Hàm IMPORTRANGE](https://www.hitava.cloud/blog/importrange-google-sheets): Lấy dữ liệu từ file khác không cần mã
- [Khoá ô công thức Google Sheets](https://www.hitava.cloud/blog/khoa-o-cong-thuc-google-sheets): Chừa ô nhập liệu, chặn sửa nhầm
- [Hướng dẫn cài Apps Script cho mẫu HiTaVa](https://www.hitava.cloud/huong-dan/bang-theo-doi-cong-viec-pro): Từng bước, có hình, gồm cấp quyền

## Câu hỏi thường gặp về Apps Script

**Apps Script có mất phí không?**

Không. Apps Script miễn phí với mọi tài khoản Google, kể cả Gmail cá nhân; chỉ khác nhau ở hạn mức (ví dụ Gmail gửi được 100 người nhận mỗi ngày, Google Workspace 1.500).

**Mở Apps Script ở đâu trong Google Sheets?**

Trong file Google Sheets, menu Tiện ích mở rộng › Apps Script. Nếu không thấy, kiểm tra file có đang ở chế độ xem hoặc là file .xlsx chưa lưu thành định dạng Google Sheets không.

**Apps Script có chạy trên Excel không?**

Không. Apps Script chỉ chạy trong Google Sheets (và các ứng dụng Google khác). Excel dùng VBA hoặc Office Scripts, cú pháp khác hẳn; file tải về dạng .xlsx sẽ không mang theo script.

**Vì sao onEdit không chạy?**

Hay gặp nhất: tên hàm gõ sai (phải đúng onEdit), ô bị đổi bởi công thức hoặc script khác thay vì người sửa, hoặc bạn bấm Chạy trong trình soạn nên không có e. onEdit cũng không gửi được email — việc đó cần trigger cài đặt.

**Màn hình Google chưa xác minh ứng dụng này có nguy hiểm không?**

Màn hình này luôn hiện với script tự viết hoặc gắn trong file, nên bản thân nó không phải dấu hiệu nguy hiểm. Rủi ro nằm ở đoạn mã: chỉ bấm Cho phép khi bạn biết script đến từ đâu và danh sách quyền khớp với việc nó làm.

**Trigger theo giờ có chạy khi tắt máy tính không?**

Có. Trigger chạy trên máy chủ Google bằng tài khoản người cài, không cần mở file hay bật máy. Giờ chạy lệch trong khoảng một tiếng so với giờ đặt.

## Nguồn

- [Google — Quotas for Google Services (hạn mức Apps Script)](https://developers.google.com/apps-script/guides/services/quotas)
- [Google — Simple triggers (onOpen, onEdit và giới hạn)](https://developers.google.com/apps-script/guides/triggers)
- [Google — Installable triggers](https://developers.google.com/apps-script/guides/triggers/installable)
- [Google — Custom functions in Google Sheets](https://developers.google.com/apps-script/guides/sheets/functions)
- [Google — Best practices (đọc ghi theo lô)](https://developers.google.com/apps-script/guides/support/best-practices)

## Mẫu liên quan

- https://www.hitava.cloud/san-pham/bang-theo-doi-cong-viec-pro
- https://www.hitava.cloud/san-pham/quan-ly-phong-tro-pro

Trang bài viết: https://www.hitava.cloud/blog/cach-dung-apps-script-google-sheets