N8n localhost doesn't run schedule trigger

Describe the problem/error/question

  • I’m using n8n on localhost, with both Docker and npm, and the Schedule Trigger doesn’t work. Howev

    er, executing the entire workflow manually runs perfectly fine. The command node -e "const { DateTime } = require('luxon'); console.log(DateTime.now().zoneName)" returns Asia/Bangkok, but the Schedule Trigger seems to be executing in the America/New_York timezone (UTC-04:00).
  • I left my laptop running for two full days over the weekend, but the workflow never executed at all. What I mean is, even with the timezone difference, shouldn’t the workflow have been triggered at some point during the day?
    With this workflow, it run well with n8n cloud (I used trial version and it expired now)

What is the error message (if any)?

Please share your workflow

{
“nodes”: [
{
“parameters”: {
“documentId”: {
“__rl”: true,
“value”: “DVKT-Bao cao ngay n8n - Google Sheets”,
“mode”: “url”
},
“sheetName”: {
“__rl”: true,
“value”: “gid=0”,
“mode”: “list”,
“cachedResultName”: “Sheet1”,
“cachedResultUrl”: “DVKT-Bao cao ngay n8n - Google Sheets”
},
“options”: {
“dataLocationOnSheet”: {
“values”: {
“rangeDefinition”: “detectAutomatically”
}
}
}
},
“type”: “n8n-nodes-base.googleSheets”,
“typeVersion”: 4.6,
“position”: [
-160,
-20
],
“id”: “07b2b6b6-c483-4a33-adce-2b48b9458f25”,
“name”: “Get row(s) in sheet”,
“credentials”: {
“googleSheetsOAuth2Api”: {
“id”: “LT5jY43rSkTbb0XC”,
“name”: “Google Sheets account”
}
}
},
{
“parameters”: {
“jsCode”: “// Lấy dữ liệu từ node trước\nconst items = $items("Get row(s) in sheet"); \t//Nếu đổi tên node google sheet khác với “Get row(s) in sheet” thì phải update trong dòng code này.\n\n// Nếu không có dữ liệu, trả về thông báo\nif (!items || items.length === 0) {\n return [{ json: { htmlTable: "Không có dữ liệu để hiển thị." } }];\n}\n\n// Dòng đầu tiên là tiêu đề (tên cột), các dòng sau là dữ liệu\nconst headers = items[0].json;\nconst dataRows = items.slice(1);\n\n// === Tìm key ứng với tiêu đề "% Hoàn thành" ===\nlet percentKey = ‘’;\nfor (const [key, val] of Object.entries(headers)) {\n if (val.toString().trim() === ‘% Hoàn thành’) {\n percentKey = key;\n break;\n }\n}\n// === CẬP NHẬT: TỰ ĐỘNG LẤY NGÀY & THÁNG ===\nconst today = new Date();\nconst day = today.getDate(); \t\t// Lấy ra số ngày, ví dụ: 9\nconst month = today.getMonth()+1; \t//Lấy ra số tháng, vì Java script lấy từ tháng 0\nconst currentDay = ${day}/${month};\t//Ghép ngày tháng\n\n// === Tiêu đề cha ===\nconst headerStructure = [\n { title: ‘Trung tâm’, colspan: 1, rowspan: 2 },\t//1 cột 2 dòng (merged)\n { title: ‘Mã tỉnh’, colspan: 1, rowspan: 2 },\n { title: ‘Số trạm’, colspan: 1, rowspan: 2 },\n { title: ‘Khảo sát’, colspan: 4 },\t\t\t//1 dòng 4 cột (merged)\n { title: ‘Tồn tại khảo sát’, colspan: 5 },\n { title: ‘Chuyển hàng’, colspan: 2 },\n { title: ‘Lắp đặt’, colspan: 2 },\n { title: ‘Tích hợp và Phát sóng’, colspan: 4 }\n];\n\n// === Tiêu đề con ===\nconst subHeaders = [\n Kế hoạch đến ngày ${currentDay}, ‘Thực tế’, ‘Submit TSSR’, ‘Duyệt TSSR’,\n ‘Trang bị từ nguồn mới’, ‘Trang bị thêm CB 125A’, ‘Trang bị thêm Rectifier’, ‘Trang bị thêm bộ gá’, ‘Hạ độ cao anten 2G hiện hữu để lắp 5G’,\n ‘Xuất kho’, ‘Đã chuyển đến trạm’,\n Kế hoạch đến ngày ${currentDay}, ‘Thực tế’,\n Kế hoạch đến ngày ${currentDay}, ‘Tích hợp’, ‘Phát sóng’, ‘% Hoàn thành’\n]; \t\t//Dấu `` (backtick) cho phép chúng ta chèn giá trị của biến vào bên trong chuỗi.\n\n// === Danh sách key thực tế theo Google Sheet ===\nconst dataKeys = Object.keys(headers).filter(key => key !== ‘row_number’);\n\n// === Tạo bảng HTML ===\nlet html = ‘’;\nhtml += ‘table {width: 100%; border-collapse: collapse; border: 1px solid black;}’;\nhtml += ‘th, td {border: 1px solid black; text-align: center; padding: 8px; vertical-align: middle; width: 70px; font-size: 70%;}’;\t\t//Định cỡ columnWidth=70 pixel, zoom nhỏ lại còn 70%\nhtml += ‘th { background-color: #DAF2D0; }’;\nhtml += ‘.sub-header-row th { height: 25px; }’; \t//Định cỡ rowHeight=15 pixel, fill màu Hex=#DAF2D0\n// === CẬP NHẬT: Thêm định dạng cho dòng cuối cùng ===\nhtml += ‘.last-row td { font-weight: bold; }’;\nhtml += ‘.highlight-yellow { background-color: #FFFF00 !important; }’; //Chuyển màu cho cột nào thì phải set nó important để nó có thể đè lên màu khác ** xem phần giải thích bên dưới\nhtml += ‘’;\nhtml += ‘

’;\n\n// Tạo phần tiêu đề\nhtml += ‘’;\nfor (const header of headerStructure) {\n const highlightClass = header.title === ‘Chuyển hàng’ ? ‘class="highlight-yellow"’ : ‘’;\n html += <th colspan=\"${header.colspan}\" rowspan=\"${header.rowspan}\" ${highlightClass}>${header.title}</th>; \t//set màu vàng (highlight-yellow) mạnh hơn background Hex=#DAF2D0 ** xem phần giải thích bên dưới\n}\nhtml += ‘<tr class="sub-header-row">’;\nfor (const subHeader of subHeaders) {\n const highlightClass = (subHeader === ‘Xuất kho’ || subHeader === ‘Đã chuyển đến trạm’) ? ‘class="highlight-yellow"’ : ‘’;\n html += <th ${highlightClass}>${subHeader}</th>;\t//set màu vàng (highlight-yellow) mạnh hơn background Hex=#DAF2D0 ** xem phần giải thích bên dưới\n}\nhtml += ‘’;\n\n// Tạo phần thân bảng\nhtml += ‘’;\n// Lặp qua các hàng dữ liệu cùng với chỉ số của chúng\nfor (const [index, item] of dataRows.entries()) {\n // === CẬP NHẬT: Kiểm tra nếu là dòng cuối cùng thì thêm class ‘last-row’ ===\n const isLastRow = index === dataRows.length - 1;\n html += <tr class=\"${isLastRow ? 'last-row' : ''}\">;\n \n for (const key of dataKeys) {\n let value = item.json[key] ?? ‘’;\n\n // === Định dạng phần trăm nếu là cột "% Hoàn thành" ===\n if (key === percentKey) {\n const numValue = parseFloat(value);\n if (!isNaN(numValue)) {\n value = (numValue * 100).toFixed(0) + ‘%’;\n }\n }\n\n html += <td>${value}</td>;\n }\n html += ‘’;\n}\nhtml += ‘
’;\n\n// Trả kết quả HTML bảng\nreturn [{ json: { htmlTable: html } }];\n”
},
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [
120,
-20
],
“id”: “ac9283c4-2baa-435e-9c76-1325aae510ef”,
“name”: “Code”
},
{
“parameters”: {
“sendTo”: “anhlpt@vnpt-technology.vn”,
“subject”: “Auto Report”,
“message”: “={{ $(‘Code’).item.json.htmlTable }}”,
“options”: {}
},
“type”: “n8n-nodes-base.gmail”,
“typeVersion”: 2.1,
“position”: [
380,
-20
],
“id”: “37a558cc-2f7d-482c-b3fc-9766755584ab”,
“name”: “Send a message”,
“webhookId”: “e94e392c-e5a1-4459-8506-a142ecd6a2e8”,
“credentials”: {
“gmailOAuth2”: {
“id”: “EOy51lGk5fjWgUPM”,
“name”: “Gmail account”
}
}
},
{
“parameters”: {
“rule”: {
“interval”: [
{
“triggerAtHour”: 15,
“triggerAtMinute”: 30
}
]
}
},
“type”: “n8n-nodes-base.scheduleTrigger”,
“typeVersion”: 1.2,
“position”: [
-400,
-20
],
“id”: “a28749b8-f538-4684-8a53-ee08407aa611”,
“name”: “Schedule Trigger1”
}
],
“connections”: {
“Get row(s) in sheet”: {
“main”: [
[
{
“node”: “Code”,
“type”: “main”,
“index”: 0
}
]
]
},
“Code”: {
“main”: [
[
{
“node”: “Send a message”,
“type”: “main”,
“index”: 0
}
]
]
},
“Schedule Trigger1”: {
“main”: [
[
{
“node”: “Get row(s) in sheet”,
“type”: “main”,
“index”: 0
}
]
]
}
},
“pinData”: {},
“meta”: {
“templateCredsSetupCompleted”: true,
“instanceId”: “02ab579d82fe1f2b4608eb9790602dba55f622a9f9b91df8517bd419e3479bed”
}
}

(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

## Share the output returned by the last node
<!-- If you need help with data transformations, please also share your expected output. -->

## Information on your n8n setup
n8n version: 1.101.2
Docker version 28.3.0, build 38b7060
npm version: 10.9.2
Windows 11
Many thanks!!!

Have you activated flow?

Yes, sure. But nothing happens.

I calculate the time difference and adjust the trigger time accordingly. It works. Thanks for your help!