Sửa lỗi 'The script does not have permission to perform that action' trong Apps Script Triggers

intermediate📗 Google Sheets2026-05-31| Google Apps Script (V8 runtime), Google Sheets, Google Workspace — tất cả các gói

Error Message

Exception: The script does not have permission to perform that action.
#google-apps-script#triggers#permissions#onEdit

Lỗi Gặp Phải

Bạn viết một đoạn code trong Google Sheets script — gửi email, gọi API ngoài, hoặc chỉ đơn giản là hiển thị một thông báo — rồi trigger bỗng dưng ngừng hoạt động. Mở execution log của Apps Script lên và bạn thấy:

Exception: The script does not have permission to perform that action.

Điều khó chịu nhất: chạy tay đúng cái hàm đó từ editor thì hoàn toàn bình thường. Nhưng hễ trigger kích hoạt nó — là lỗi ngay. Dưới đây là lý do tại sao và cách khắc phục.

Nguyên Nhân

Apps Script có hai loại trigger, và cách hoạt động bên trong của chúng rất khác nhau:

  • Simple trigger — các tên hàm được đặt trước như onEdit(e), onOpen(e), và onChange(e). Chúng tự động kích hoạt nhưng chạy trong môi trường sandbox không có thông tin xác thực của người dùng. Chúng không thể thực hiện bất kỳ thao tác nào yêu cầu OAuth, hoàn toàn không.
  • Installable trigger — trigger bạn tạo thủ công (qua giao diện Triggers hoặc code ScriptApp). Chúng chạy dưới thông tin xác thực của người tạo, với đầy đủ quyền đã được cấp phép lúc thiết lập.

Ranh giới đó là tuyệt đối. Dù appsscript.json của bạn có liệt kê đủ mọi OAuth scope cần thiết, simple trigger vẫn không đụng đến chúng. Sandbox bỏ qua hoàn toàn token của bạn.

Những thứ chắc chắn thất bại bên trong simple trigger:

  • MailApp.sendEmail() hoặc GmailApp.sendEmail()
  • UrlFetchApp.fetch() — bất kỳ lệnh gọi HTTP nào ra ngoài
  • SpreadsheetApp.getUi().alert() hoặc bất kỳ hộp thoại UI nào
  • DriveApp.createFile() hoặc các thao tác Drive tương tự
  • Các lệnh gọi Calendar, Contacts, hoặc Admin SDK

Cách Sửa: Chuyển Sang Installable Trigger

Bước 1 — Đổi tên hàm

Bất kỳ hàm nào tên là onEdit đều bị Apps Script coi là simple trigger — bất kể bạn đăng ký nó như thế nào. Hãy đổi tên thành tên không nằm trong danh sách đặt trước:

// Trước — simple trigger, quyền hạn chế
function onEdit(e) {
  if (e.range.getColumn() === 3) {
    MailApp.sendEmail('boss@company.com', 'Row updated', 'Someone edited column C');
  }
}

// Sau — đổi tên để không còn bị coi là simple trigger
function onEditHandler(e) {
  if (e.range.getColumn() === 3) {
    MailApp.sendEmail('boss@company.com', 'Row updated', 'Someone edited column C');
  }
}

Bước 2 — Tạo installable trigger

Cách A — Qua giao diện (dễ nhất cho thiết lập một lần):

  • Mở script trong Apps Script editor.
  • Nhấp vào biểu tượng đồng hồ báo thức (Triggers) ở thanh bên trái.
  • Nhấp + Add Trigger (góc dưới bên phải).
  • Đặt function là onEditHandler, event source là From spreadsheet, event type là On edit.
  • Nhấp Save — bạn sẽ được yêu cầu cấp quyền cho script.

Cách B — Bằng code (chạy một lần rồi xóa):

function createEditTrigger() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  // Xóa trigger hiện có cho handler này để tránh trùng lặp
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'onEditHandler') {
      ScriptApp.deleteTrigger(t);
    }
  });

  ScriptApp.newTrigger('onEditHandler')
    .forSpreadsheet(ss)
    .onEdit()
    .create();

  console.log('Installable trigger created successfully.');
}

Nhấn Run trên hàm createEditTrigger() từ editor. Chấp nhận popup xác thực quyền. Từ đó trở đi, trigger sẽ chạy với đầy đủ OAuth credentials của bạn mỗi khi có chỉnh sửa — không còn lỗi quyền hạn nữa.

Bước 3 — Kiểm tra OAuth scope

Vẫn thấy lỗi sau khi đã cấp quyền? Hãy kiểm tra file manifest appsscript.json. Bật nó qua Project Settings → Show appsscript.json manifest file:

{
  "timeZone": "America/New_York",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/script.external_request"
  ]
}

Sau khi cập nhật manifest, chạy thử bất kỳ hàm nào từ editor một lần để kích hoạt quá trình xác thực lại với danh sách scope mới.

Xác Nhận Đã Sửa Xong

  • Mở trang Triggers (biểu tượng đồng hồ). Hàm onEditHandler của bạn phải xuất hiện với loại On edit, nguồn Spreadsheet, và email của bạn trong cột owner.
  • Chỉnh sửa một ô trong spreadsheet để kích hoạt trigger.
  • Kiểm tra Executions (biểu tượng danh sách ở thanh bên trái). Lần chạy thành công sẽ hiển thị trạng thái Completed. Vẫn thấy lỗi quyền? Quay lại Bước 3 và xác thực lại.
  • Xác nhận hành động thực sự đã xảy ra — email trong hộp thư, phản hồi API trong log, hay bất cứ điều gì script của bạn cần làm.

Muốn kiểm tra nhanh xem trigger có đang chạy dưới tài khoản của bạn không? Thêm tạm dòng này vào:

function onEditHandler(e) {
  console.log('Effective user:', Session.getEffectiveUser().getEmail());
  // ... phần còn lại của code
}

Email của bạn xuất hiện trong Executions log? Installable trigger đã được cài đặt đúng. Không thấy gì, hoặc có lỗi? Nghĩa là nó vẫn đang kích hoạt như một simple trigger ở đâu đó.

Cảnh Báo: Trigger Bị Trùng Lặp

Chạy createEditTrigger() hai lần và bạn sẽ có hai trigger cùng trỏ đến một handler. Mỗi lần chỉnh sửa sẽ kích hoạt nó hai lần — gửi hai email, gọi API hai lần. Hãy kiểm tra trang Triggers trước khi chạy lại hàm thiết lập, hoặc dùng logic loại bỏ trùng lặp đã có sẵn trong đoạn code trên. Để kiểm tra những gì đang được đăng ký lúc này:

function listTriggers() {
  ScriptApp.getProjectTriggers().forEach(t => {
    console.log(
      t.getHandlerFunction(),
      t.getEventType(),
      t.getTriggerSourceId()
    );
  });
}

Lưu Ý Thêm

  • Giữ simple trigger đơn giản. Chỉ dùng onEdit cho những việc nhẹ nhàng, không cần xác thực — định dạng ô, đóng dấu thời gian "Lần sửa cuối", những thứ tương tự. Bất cứ thứ gì liên quan đến dịch vụ bên ngoài đều phải dùng installable trigger.
  • Quyền sở hữu trigger quan trọng trong script dùng chung. Installable trigger chạy dưới tài khoản của người tạo ra nó. Với một sheet nhóm, mỗi người cần tự động hóa dưới tài khoản riêng của họ nên tự tạo trigger — hoặc chọn một tài khoản dịch vụ chung và thiết lập một trigger dùng chung ở đó.
  • Đừng xin quá nhiều scope. Chỉ yêu cầu các OAuth scope mà script thực sự dùng đến. Liệt kê https://mail.google.com/ trong khi gmail.send là đủ sẽ gây khó khăn khi xem xét cấp quyền và gây nhầm lẫn cho người dùng.
  • Time-based trigger không bị ảnh hưởng bởi vấn đề này. Các công việc lên lịch bằng ScriptApp.newTrigger().timeBased() luôn là installable theo bản chất. Hạn chế này chỉ ảnh hưởng đến các tên hàm đặt trước dựa trên sự kiện như onEditonOpen.

Related Error Notes