This workflow corresponds to n8n.io template #14136 — we link there as the canonical source.
This workflow follows the Agent → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "b7299a4b-43b0-4f92-bad4-fb1dfa8bb206",
"name": "Customer Bot Listener",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
7040,
3552
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"typeVersion": 1.1
},
{
"id": "e492c4f7-a371-40fb-9d0d-b6f17f2fc063",
"name": "Route Message",
"type": "n8n-nodes-base.code",
"position": [
7296,
3552
],
"parameters": {
"jsCode": "\nconst msg = $input.first().json;\nconst text = (msg.message?.text || msg.text || '').trim();\nconst chatId = msg.message?.chat?.id || msg.chat?.id || '';\nconst firstName = msg.message?.from?.first_name || msg.from?.first_name || 'Customer';\nconst upper = text.toUpperCase();\nconst statusMatch = upper.match(/^STATUS\\s+(\\d+)/);\nconst cancelMatch = upper.match(/^CANCEL\\s+(\\d+)/);\nconst startMatch = upper.match(/^\\/START/);\nconst helpMatch = upper.match(/^\\/HELP|^HELP/);\nconst menuMatch = upper.match(/^\\/MENU|^MENU/);\nconst myorderMatch = upper.match(/^\\/MYORDERS|^MYORDERS/);\nlet route = 'order';\nif (statusMatch) route = 'status';\nif (cancelMatch) route = 'cancel';\nif (startMatch) route = 'start';\nif (helpMatch) route = 'help';\nif (menuMatch) route = 'menu';\nif (myorderMatch) route = 'myorders';\nif (upper.match(/^READY\\s+\\d+/)) route = 'blocked';\nconsole.log('ChatID:', chatId, '| Route:', route, '| Text:', text);\nreturn [{ json: {\n chat_id: String(chatId), text, first_name: firstName, upper, route,\n queue_match: statusMatch?.[1] || cancelMatch?.[1] || null\n}}];\n"
},
"typeVersion": 2
},
{
"id": "604a199d-c32f-4eed-a5a1-82ed3d9abba5",
"name": "Message Switch",
"type": "n8n-nodes-base.switch",
"position": [
7552,
3456
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s1",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "start"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s2",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "help"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s3",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "order"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s4",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "cancel"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s5",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "status"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s6",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "menu"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s7",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "myorders"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s8",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.route }}",
"rightValue": "blocked"
}
]
}
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "c2920056-7417-479a-aead-cbda24afc2ff",
"name": "Send Welcome",
"type": "n8n-nodes-base.telegram",
"position": [
8144,
3152
],
"parameters": {
"text": "=\ud83d\udc4b Welcome *{{ $('Route Message').first().json.first_name }}*\\!\n\nI am the \ud83c\udf55 *Restaurant Order Bot*\n\nJust type your order to get started:\n_\"2 Margherita pizza \\+ 1 Coke\"_\n\n\ud83d\udccb *Your Commands:*\n\u2022 /menu \u2014 see today's menu\n\u2022 STATUS 1234 \u2014 check your order\n\u2022 CANCEL 1234 \u2014 cancel \\(Pending orders only\\)\n\u2022 /myorders \u2014 your recent orders\n\u2022 /help \u2014 all commands\n\n_Orders are managed by staff via their dashboard_",
"chatId": "={{ $('Route Message').first().json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "0f8cb239-b193-44a2-aaea-71cf82027ca8",
"name": "Send Help",
"type": "n8n-nodes-base.telegram",
"position": [
8144,
3312
],
"parameters": {
"text": "=\ud83d\udcd6 *How to use this bot:*\n\n*Place an order:*\nJust type what you want\n_Example: 2 pizza \\+ 1 coke_\n\n*Check your order status:*\nSTATUS 1234\n\n*Cancel your order \\(Pending only\\):*\nCANCEL 1234\n\n*/menu* \u2014 see today's menu\n*/myorders* \u2014 your last 5 orders\n\n_You will be automatically notified when your order status changes\\!_",
"chatId": "={{ $('Route Message').first().json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "57c9c3eb-27c1-47d1-89fc-8d1a662f06c6",
"name": "AI Parse Order",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
8144,
3504
],
"parameters": {
"text": "=You are a restaurant order parser.\n\nParse this message and return ONLY raw JSON, single line, no markdown.\n\nFormat: {\"items\":\"clean readable order summary\",\"valid\":true}\n\nIf it is a food/drink order \u2192 valid: true, clean up the items text nicely\nIf it is NOT a food order (random text, commands, questions etc) \u2192 valid: false\n\nMessage: {{ $('Route Message').first().json.text }}\n\nReturn JSON only. Nothing else.",
"options": {
"systemMessage": "Restaurant order parser. Return raw JSON only. Single line. No markdown."
},
"promptType": "define"
},
"typeVersion": 3.1
},
{
"id": "969199f6-771c-4662-a5a4-dd223ed9798b",
"name": "Claude Haiku",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
8032,
3680
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-haiku-4-5-20251001",
"cachedResultName": "Claude Haiku 4.5"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "9660e8dd-aacd-418d-b941-f9caecccbc94",
"name": "Build Order Response",
"type": "n8n-nodes-base.code",
"position": [
8448,
3504
],
"parameters": {
"jsCode": "\nconst aiRaw = $input.first().json.output || '';\nconst prev = $('Route Message').first().json;\nlet p = null;\ntry { p = JSON.parse(aiRaw.trim()); } catch(e) {}\nif (!p) { try { const m = aiRaw.match(/\\{[\\s\\S]*\\}/); if(m) p = JSON.parse(m[0]); } catch(e) {} }\nif (!p) { p = { items: prev.text, valid: true }; }\nif (!p.valid) {\n return [{ json: {\n chat_id: prev.chat_id,\n reply: \"Sorry, I didn't understand that as a food order! \ud83e\udd14\\n\\nType your order like:\\n\\\"2 Margherita pizza + 1 Coke\\\"\\n\\nType /menu to see what we offer\\nType /help for all commands.\",\n is_order: false\n }}];\n}\nconst queueNum = Math.floor(Date.now() / 1000) % 9000 + 1000;\nconst waitMins = 10 + Math.floor(Math.random() * 10);\nconst now = new Date();\nconst reply = `\u2705 Order received!\\n\\n\ud83d\udccb Order: ${p.items}\\n\ud83d\udd22 Queue: #${queueNum}\\n\u23f1\ufe0f Wait: ~${waitMins} mins\\n\\nYou'll be notified automatically when ready!\\n\\nCheck anytime: STATUS ${queueNum}\\nTo cancel: CANCEL ${queueNum}`;\nreturn [{ json: {\n chat_id: prev.chat_id, first_name: prev.first_name, reply,\n is_order: true, queue_number: queueNum,\n parsed_order: p.items,\n wait_mins: waitMins, status: 'Pending',\n order_time: now.toISOString(), order_date: now.toLocaleDateString('en-IN')\n}}];\n"
},
"typeVersion": 2
},
{
"id": "9d2a106c-6e29-4ee3-85ff-97a31610581e",
"name": "Save Order",
"type": "n8n-nodes-base.googleSheets",
"position": [
8624,
3504
],
"parameters": {
"columns": {
"value": {
"Name": "={{ $json.first_name }}",
"Order": "={{ $json.parsed_order }}",
"Status": "=Pending",
"Chat ID": "={{ $json.chat_id }}",
"Order Date": "={{ $json.order_date }}",
"Order Time": "={{ $json.order_time }}",
"Queue Number": "={{ $json.queue_number }}"
},
"schema": [
{
"id": "Queue Number",
"type": "string",
"display": true,
"required": false,
"displayName": "Queue Number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Chat ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Chat ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Order",
"type": "string",
"display": true,
"required": false,
"displayName": "Order",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Order Time",
"type": "string",
"display": true,
"required": false,
"displayName": "Order Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Order Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Order Date",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit?usp=drivesdk",
"cachedResultName": "Restaurant Orders"
}
},
"typeVersion": 4.5
},
{
"id": "486eb607-db75-4ef5-b5dc-9d73b4e9c7e1",
"name": "Send Confirmation",
"type": "n8n-nodes-base.telegram",
"position": [
8864,
3504
],
"parameters": {
"text": "={{ $('Build Order Response').first().json.reply }}",
"chatId": "={{ $('Build Order Response').first().json.chat_id }}",
"additionalFields": {}
},
"typeVersion": 1.2
},
{
"id": "8263cbdb-1615-4eb6-aca4-be09ed515d54",
"name": "Find Order to Cancel",
"type": "n8n-nodes-base.googleSheets",
"position": [
8144,
3792
],
"parameters": {
"options": {
"returnFirstMatch": true
},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.queue_match }}",
"lookupColumn": "Queue Number"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit?usp=drivesdk",
"cachedResultName": "Restaurant Orders"
}
},
"executeOnce": false,
"retryOnFail": false,
"typeVersion": 4.5,
"alwaysOutputData": false
},
{
"id": "40e8a6e7-9c83-4fbe-8cd5-5a5f7a9a009b",
"name": "Validate Cancel",
"type": "n8n-nodes-base.code",
"position": [
8384,
3792
],
"parameters": {
"jsCode": "\nconst routeData = $('Route Message').first().json;\nconst orderRow = $input.first().json;\nconst queueNum = routeData.queue_match;\nconst chatId = String(routeData.chat_id);\nconst hasData = orderRow && (orderRow['Queue Number'] || orderRow['Order']);\nif (!hasData) {\n return [{ json: { chat_id: chatId, reply: `\u274c Order #${queueNum} not found.\\n\\nPlease check the queue number and try again.`, can_cancel: false }}];\n}\nconst currentStatus = orderRow['Status'] || 'Unknown';\nconst orderItems = orderRow['Order'] || 'your order';\nconst orderChatId = String(orderRow['Chat ID'] || '');\nif (orderChatId !== chatId) {\n return [{ json: { chat_id: chatId, reply: `\u274c You can only cancel your own orders.`, can_cancel: false }}];\n}\nif (currentStatus === 'Completed') {\n return [{ json: { chat_id: chatId, reply: `\u26a0\ufe0f Order #${queueNum} is already *Completed*.\\n\\n\ud83d\udccb Order: ${orderItems}\\n\\nCompleted orders cannot be cancelled.`, can_cancel: false }}];\n}\nif (currentStatus === 'Cancelled') {\n return [{ json: { chat_id: chatId, reply: `\u26a0\ufe0f Order #${queueNum} is already *Cancelled*.`, can_cancel: false }}];\n}\nif (currentStatus === 'Ready') {\n return [{ json: { chat_id: chatId, reply: `\u26a0\ufe0f Order #${queueNum} is *Ready for collection*!\\n\\n\ud83d\udccb Order: ${orderItems}\\n\\nPlease collect your order from the counter \ud83d\ude0a`, can_cancel: false }}];\n}\nif (currentStatus === 'Preparing') {\n return [{ json: { chat_id: chatId, reply: `\u26a0\ufe0f Order #${queueNum} is currently being *Prepared*.\\n\\n\ud83d\udccb Order: ${orderItems}\\n\\nUnfortunately it cannot be cancelled at this stage.`, can_cancel: false }}];\n}\nreturn [{ json: {\n chat_id: chatId,\n reply: `\u2705 Order #${queueNum} has been *cancelled*.\\n\\n\ud83d\udccb Order: ${orderItems}\\n\\nWe're sorry for any inconvenience.`,\n can_cancel: true, queue_number: String(queueNum), order_items: orderItems\n}}];\n"
},
"typeVersion": 2
},
{
"id": "cdd711a0-62c7-4502-bf33-0bc8a153d279",
"name": "Can Cancel?",
"type": "n8n-nodes-base.if",
"position": [
8624,
3792
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cc1",
"operator": {
"type": "boolean",
"operation": "equals",
"rightType": "boolean"
},
"leftValue": "={{ $json.can_cancel }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.1
},
{
"id": "20d2f37c-e3f2-4287-99d3-e9455c6f5ad2",
"name": "Mark as Cancelled",
"type": "n8n-nodes-base.googleSheets",
"position": [
8864,
3712
],
"parameters": {
"columns": {
"value": {
"Status": "=Cancelled",
"Queue Number": "={{ $('Validate Cancel').first().json.queue_number }}"
},
"schema": [
{
"id": "Queue Number",
"type": "string",
"display": true,
"required": false,
"displayName": "Queue Number",
"defaultMatch": true,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Queue Number"
]
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit?usp=drivesdk",
"cachedResultName": "Restaurant Orders"
}
},
"typeVersion": 4.5
},
{
"id": "49a0e6bc-c4dc-4e81-ae52-f842ba6a3e5c",
"name": "Send Cancel Reply",
"type": "n8n-nodes-base.telegram",
"position": [
8864,
3920
],
"parameters": {
"text": "={{ $('Validate Cancel').first().json.reply }}",
"chatId": "={{ $('Validate Cancel').first().json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "0922b378-6b25-4657-8f07-5d6966d7fd74",
"name": "Find Order Status",
"type": "n8n-nodes-base.googleSheets",
"position": [
8144,
4032
],
"parameters": {
"options": {
"returnFirstMatch": true
},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $json.queue_match }}",
"lookupColumn": "Queue Number"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit?usp=drivesdk",
"cachedResultName": "Restaurant Orders"
}
},
"typeVersion": 4.5
},
{
"id": "f2a8feb2-7bcd-4df2-b10b-ef6dc8dd1cf6",
"name": "Build Status Reply",
"type": "n8n-nodes-base.code",
"position": [
8384,
4032
],
"parameters": {
"jsCode": "\nconst routeData = $('Route Message').first().json;\nconst orderRow = $input.first().json;\nconst queueNum = routeData.queue_match;\nconst hasData = orderRow && (orderRow['Queue Number'] || orderRow['Order']);\nif (!hasData) {\n return [{ json: { chat_id: String(routeData.chat_id), reply: `\u274c Order #${queueNum} not found.\\n\\nPlease check the queue number and try again.` } }];\n}\nconst status = orderRow['Status'] || 'Unknown';\nconst orderItems = orderRow['Order'] || 'Unknown order';\nconst name = orderRow['Name'] || '';\nconst orderTime = orderRow['Order Time'] || '';\nconst emojiMap = { 'Completed': '\u2705', 'Cancelled': '\u274c', 'Pending': '\u23f3', 'Ready': '\ud83c\udf55', 'Preparing': '\ud83d\udc68\u200d\ud83c\udf73' };\nconst emoji = emojiMap[status] || '\ud83d\udccb';\nlet timeStr = '';\nif (orderTime) {\n try {\n const d = new Date(orderTime);\n timeStr = `\\n\ud83d\udd50 Ordered: ${d.toLocaleTimeString('en-IN', {hour:'2-digit', minute:'2-digit'})}`;\n } catch(e) {}\n}\nconst reply = `${emoji} *Order #${queueNum}*\\n\\n\ud83d\udccb Order: ${orderItems}\\n\ud83d\udcca Status: *${status}*${timeStr}${name ? '\\n\ud83d\udc64 Name: ' + name : ''}`;\nreturn [{ json: { chat_id: String(routeData.chat_id), reply } }];\n"
},
"typeVersion": 2
},
{
"id": "63503809-fd44-4f47-9e30-4d925f834750",
"name": "Send Status",
"type": "n8n-nodes-base.telegram",
"position": [
8624,
4032
],
"parameters": {
"text": "={{ $json.reply }}",
"chatId": "={{ $json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "e57c63eb-94ba-48e9-922f-fb1f3a73b23e",
"name": "Send Menu",
"type": "n8n-nodes-base.telegram",
"position": [
8144,
4240
],
"parameters": {
"text": "=\ud83c\udf55 *Today's Menu*\n\n*Pizza*\n\u2022 Margherita \u2014 \u20b9299\n\u2022 Pepperoni \u2014 \u20b9349\n\u2022 Veggie Supreme \u2014 \u20b9329\n\n*Burgers*\n\u2022 Classic Beef \u2014 \u20b9249\n\u2022 Chicken Crispy \u2014 \u20b9229\n\u2022 Veggie Delight \u2014 \u20b9199\n\n*Drinks*\n\u2022 Coke / Pepsi \u2014 \u20b979\n\u2022 Fresh Juice \u2014 \u20b999\n\u2022 Water \u2014 \u20b929\n\n_Just type your order to place it\\!_",
"chatId": "={{ $('Route Message').first().json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "ace66359-2499-4716-8405-85252b0e51c4",
"name": "Read All Orders",
"type": "n8n-nodes-base.googleSheets",
"position": [
8144,
4400
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit?usp=drivesdk",
"cachedResultName": "Restaurant Orders"
}
},
"typeVersion": 4.5
},
{
"id": "45aadc39-9aae-4166-b100-76924bcb09b4",
"name": "Build My Orders",
"type": "n8n-nodes-base.code",
"position": [
8384,
4400
],
"parameters": {
"jsCode": "\nconst routeData = $('Route Message').first().json;\nconst allRows = $input.all();\nconst myChatId = String(routeData.chat_id);\nconst myOrders = allRows.filter(r => String(r.json['Chat ID'] || '') === myChatId).slice(-5).reverse();\nif (myOrders.length === 0) {\n return [{ json: { chat_id: myChatId, reply: \"\ud83d\udccb You have no orders yet.\\n\\nJust type your order to get started!\\nType /menu to see what we offer.\" } }];\n}\nconst emojiMap = { 'Completed': '\u2705', 'Cancelled': '\u274c', 'Pending': '\u23f3', 'Ready': '\ud83c\udf55', 'Preparing': '\ud83d\udc68\u200d\ud83c\udf73' };\nlet reply = `\ud83d\udccb *Your Recent Orders:*\\n\\n`;\nfor (const r of myOrders) {\n const row = r.json;\n const status = row['Status'] || 'Unknown';\n const emoji = emojiMap[status] || '\ud83d\udccb';\n reply += `${emoji} *#${row['Queue Number']}* \u2014 ${row['Order']}\\n ${status} | ${row['Order Date'] || ''}\\n\\n`;\n}\nreturn [{ json: { chat_id: myChatId, reply } }];\n"
},
"typeVersion": 2
},
{
"id": "f1dbb23a-aa98-40f0-b41b-b5cb7edfb421",
"name": "Send My Orders",
"type": "n8n-nodes-base.telegram",
"position": [
8624,
4400
],
"parameters": {
"text": "={{ $json.reply }}",
"chatId": "={{ $json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "2cd124c8-cefa-4bad-abac-d149ecbde775",
"name": "Send Blocked Message",
"type": "n8n-nodes-base.telegram",
"position": [
8144,
4560
],
"parameters": {
"text": "=\ud83d\udeab Staff commands are not available here.\n\nOrders are managed by restaurant staff via their dashboard.\n\nYou will be *automatically notified* when your order status changes\\!",
"chatId": "={{ $('Route Message').first().json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "ff90e51e-be1b-46e6-8ac1-7bd40e993c7b",
"name": "Every 1 Minute",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
7072,
5072
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 1
}
]
}
},
"typeVersion": 1.2
},
{
"id": "6645a964-f1a8-48c3-9613-d731670735d9",
"name": "Read All Rows",
"type": "n8n-nodes-base.googleSheets",
"position": [
7520,
5072
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/edit?usp=drivesdk",
"cachedResultName": "Restaurant Orders"
}
},
"typeVersion": 4.5
},
{
"id": "18387888-c225-4294-b150-b1f7fc5a296b",
"name": "Detect Changed Row",
"type": "n8n-nodes-base.code",
"position": [
7712,
5072
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "\nconst row = $input.item.json;\nconst rowNum = row['row_number'];\nconst queueNum = String(row['Queue Number'] || '').trim();\nconst chatId = String(row['Chat ID'] || '').trim();\nconst status = String(row['Status'] || '').trim();\nconst lastSent = String(row['Last Status Sent'] || '').trim();\nconst order = String(row['Order'] || '').trim();\nconst name = String(row['Name'] || '').trim();\n\n// Skip empty rows\nif (!queueNum || !chatId || chatId === '0') return null;\n\n// Skip Pending \u2014 only notify on staff-set statuses\nconst notifiable = ['Preparing', 'Ready', 'Completed', 'Cancelled'];\nif (!notifiable.includes(status)) return null;\n\n// THE KEY CHECK: only notify if status is different from last time we sent\nif (status === lastSent) return null;\n\nconsole.log(`Row ${rowNum} | Queue #${queueNum} | ${lastSent||'(none)'} \u2192 ${status} \u2705 SEND`);\n\nreturn { json: { row_number: rowNum, queue_number: queueNum, chat_id: chatId, status, order, name } };\n"
},
"typeVersion": 2
},
{
"id": "9a0f7416-4e20-432c-aada-f32e197c73cb",
"name": "Build Message",
"type": "n8n-nodes-base.code",
"position": [
8048,
5072
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "\nconst d = $input.item.json;\nconst msgs = {\n 'Preparing': `\ud83d\udc68\u200d\ud83c\udf73 Your order is being *Prepared*!\\n\\n\ud83d\udccb Order: ${d.order}\\n\ud83d\udd22 Queue: #${d.queue_number}\\n\\nWe'll notify you when it's ready!`,\n 'Ready': `\ud83c\udf55 Your order is *READY*!\\n\\n\ud83d\udccb Order: ${d.order}\\n\ud83d\udd22 Queue: #${d.queue_number}\\n\\nPlease collect from the counter \ud83d\ude0a`,\n 'Completed': `\u2705 Order #${d.queue_number} *Completed*.\\n\\nThank you! \ud83d\ude4f`,\n 'Cancelled': `\u274c Order #${d.queue_number} *Cancelled*.\\n\\n\ud83d\udccb Order: ${d.order}\\n\\nSorry for the inconvenience.`\n};\nreturn { json: { ...d, msg: msgs[d.status] } };\n"
},
"typeVersion": 2
},
{
"id": "d76b83de-7bfb-4826-a0ea-f15e70c90e32",
"name": "Send to Customer",
"type": "n8n-nodes-base.telegram",
"position": [
8320,
5072
],
"parameters": {
"text": "={{ $json.msg }}",
"chatId": "={{ $json.chat_id }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "7103c954-2c79-4f25-b504-ada9c87b5cbd",
"name": "Prep Sheet Update",
"type": "n8n-nodes-base.code",
"position": [
8624,
5072
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "\nconst d = $('Build Message').item.json;\nconst rowNum = d.row_number;\nconst status = d.status;\n// Column H = Last Status Sent (8th column = H)\nconst range = `Sheet1!H${rowNum}`;\nconst url = `https://sheets.googleapis.com/v4/spreadsheets/1_XaI7z844arJWhzMN2MbD6kFzHoK6yUvsY1OyE10ebM/values/${encodeURIComponent(range)}?valueInputOption=RAW`;\nconsole.log(`Writing H${rowNum} = \"${status}\"`);\nreturn { json: { ...d, _url: url, _body: { range, majorDimension:'ROWS', values:[[status]] } } };\n"
},
"typeVersion": 2
},
{
"id": "f77ce233-ea53-4ac2-8996-7f99007fb981",
"name": "Save Last Status Sent",
"type": "n8n-nodes-base.httpRequest",
"position": [
8880,
5072
],
"parameters": {
"url": "={{ $json._url }}",
"method": "PUT",
"options": {},
"jsonBody": "={{ JSON.stringify($json._body) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "e054be03-1337-432d-bd9e-bd2c846efd1c",
"name": "W1 Title",
"type": "n8n-nodes-base.stickyNote",
"position": [
6496,
3120
],
"parameters": {
"width": 804,
"height": 300,
"content": "## \ud83c\udf55 WORKFLOW 1 \u2014 Customer Bot\n**Telegram bot for customers to place and manage orders**\n\n**Bot:** @new_nirav_restaurant_bot\n**Sheet:** Restaurant Orders (1_XaI7z844...)\n\n**How it works:**\nCustomer texts the bot \u2192 Route Message detects what they want \u2192 routes to correct handler"
},
"typeVersion": 1
},
{
"id": "a5449eb9-3145-420e-8bb2-b2e0797ab403",
"name": "Entry Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
6496,
3520
],
"parameters": {
"color": 5,
"width": 972,
"height": 228,
"content": "## \ud83d\udce5 Entry Point\nListens to ALL messages from the customer bot.\n\nExtracts: chat_id, text, first_name\nDetects commands: /start, /help, /menu, /myorders, STATUS, CANCEL\nDefault route = **order** (food order)"
},
"typeVersion": 1
},
{
"id": "b38ffb11-4d73-4bb8-905e-b590eceb7a8a",
"name": "Switch Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
7504,
3184
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## \ud83d\udd00 Message Router\nRoutes to one of 8 paths:\n0. /start \u2192 Welcome message\n1. /help \u2192 Help message\n2. order \u2192 AI Parse Order\n3. cancel \u2192 Cancel flow\n4. status \u2192 Status check\n5. /menu \u2192 Menu\n6. /myorders \u2192 Order history\n7. blocked \u2192 Staff cmd blocked"
},
"typeVersion": 1
},
{
"id": "f5f89365-aeab-4733-9a8c-a9d810d482a1",
"name": "AI Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
7824,
3440
],
"parameters": {
"color": 6,
"width": 580,
"height": 328,
"content": "## \ud83e\udd16 AI Order Parsing\nUses **Claude Haiku** to parse natural language orders.\n\nInput: \"2 pizza + 1 coke\"\nOutput: \n`{\n\"items\": \"2 Margherita Pizza + 1 Coke\", \n\"valid\": true\n}`\n\nIf not a food order \u2192 returns valid:\nfalse \u2192 sends error message to customer"
},
"typeVersion": 1
},
{
"id": "98c8c7aa-2136-40b3-ab1d-ce7615191312",
"name": "Save Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
8592,
3264
],
"parameters": {
"color": 6,
"width": 476,
"height": 388,
"content": "## \ud83d\udcbe Save Order\nAppends new order to Google Sheet with:\n- Queue Number (auto-generated)\n- Chat ID (for later notifications)\n- Name, Order, Status=Pending\n- Order Time, Order Date\n\n\u26a0\ufe0f Column H (Last Status Sent) stays empty \u2014 filled by W2"
},
"typeVersion": 1
},
{
"id": "a40ee7cf-998d-4e1a-883a-8896b6cfd8c7",
"name": "Cancel Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
9040,
3664
],
"parameters": {
"color": 3,
"width": 300,
"height": 396,
"content": "## \u274c Cancel Flow\nValidation rules:\n\u2705 Pending \u2192 can cancel\n\ud83d\udeab Preparing \u2192 cannot cancel\n\ud83d\udeab Ready \u2192 cannot cancel\n\ud83d\udeab Completed \u2192 cannot cancel\n\ud83d\udeab Cancelled \u2192 already cancelled\n\ud83d\udeab Other person's order \u2192 blocked\n\nIf valid \u2192 updates sheet Status to Cancelled"
},
"typeVersion": 1
},
{
"id": "af8df331-4b48-4e85-a69e-19ab93bd7258",
"name": "Status Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
7584,
3984
],
"parameters": {
"color": 5,
"width": 1208,
"height": 176,
"content": "## \ud83d\udcca Order Status Check\nCustomer types: STATUS 6771\n\u2192 Looks up Queue Number in sheet\n\u2192 Returns current status with emoji:\n\u23f3 Pending \ud83d\udc68\u200d\ud83c\udf73 Preparing\n\ud83c\udf55 Ready \u2705 Completed \u274c Cancelled"
},
"typeVersion": 1
},
{
"id": "ec51a4e6-05d4-46c7-916a-bfa2ed0472d6",
"name": "MyOrders Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
7584,
4384
],
"parameters": {
"color": 5,
"width": 1220,
"height": 178,
"content": "## \ud83d\udccb My Orders\nShows last 5 orders for this customer.\nFilters all sheet rows by Chat ID.\nDisplays: queue number, order, status, date"
},
"typeVersion": 1
},
{
"id": "d9f18bfc-369e-47d8-9729-64a4aae7dec3",
"name": "W2 Title",
"type": "n8n-nodes-base.stickyNote",
"position": [
6512,
4752
],
"parameters": {
"width": 800,
"height": 220,
"content": "## \ud83d\udc68\u200d\ud83c\udf73 WORKFLOW 2 \u2014 Staff Status Notifier\n**Auto-notifies customers when staff changes order status in Google Sheet**\n\n**How it works:**\nRuns every minute \u2192 reads all rows \u2192 compares Status vs Last Status Sent (col H)\n\n \u2192 sends Telegram message for changed rows only \u2192 updates col H"
},
"typeVersion": 1
},
{
"id": "4935ce86-66b7-49b9-8bf5-6251c640adec",
"name": "Schedule Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
6512,
5024
],
"parameters": {
"color": 5,
"width": 804,
"height": 236,
"content": "## \u23f1\ufe0f Schedule Trigger\nRuns **every 1 minute**.\n\nCannot use Google Sheets trigger\n\nbecause it cannot detect which specific row changed \u2014 it always returns row 1."
},
"typeVersion": 1
},
{
"id": "091c7f32-fada-488c-9d5e-b61ce334e276",
"name": "Detect Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
7392,
4832
],
"parameters": {
"color": 6,
"width": 540,
"height": 492,
"content": "## \ud83d\udd0d Detect Changed Row\n**runOnceForEachItem** \u2014 processes every row independently.\n\nLogic per row:\n1. Skip if no Queue Number or Chat ID\n2. Skip if Status = Pending (not notifiable)\n3. Skip if Status = Last Status Sent (already notified)\n4. \u2705 Pass through if Status changed\n\nOnly changed rows flow forward."
},
"typeVersion": 1
},
{
"id": "a7cda7e8-8935-4796-8179-c83e77944c66",
"name": "MsgBuild Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
7984,
4832
],
"parameters": {
"color": 6,
"width": 532,
"height": 496,
"content": "## \ud83d\udcac Message Templates\nBuilds message based on new status:\n\ud83d\udc68\u200d\ud83c\udf73 Preparing \u2192 \"Being prepared!\"\n\ud83c\udf55 Ready \u2192 \"READY for collection!\"\n\u2705 Completed \u2192 \"Thank you!\"\n\u274c Cancelled \u2192 \"Order cancelled\""
},
"typeVersion": 1
},
{
"id": "5344c139-6ee8-40d7-83fe-5a53b17fd82d",
"name": "UpdateH Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
8560,
4832
],
"parameters": {
"color": 3,
"width": 540,
"height": 488,
"content": "## \ud83d\udcdd Update Last Status Sent\nWrites current Status into **column H** (Last Status Sent) of the exact row using Google Sheets HTTP API.\n\nThis prevents duplicate notifications \u2014 next minute this row will be skipped because Status = Last Status Sent.\n\n\u26a0\ufe0f Column H must exist in your sheet!"
},
"typeVersion": 1
}
],
"connections": {
"Save Order": {
"main": [
[
{
"node": "Send Confirmation",
"type": "main",
"index": 0
}
]
]
},
"Can Cancel?": {
"main": [
[
{
"node": "Mark as Cancelled",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Cancel Reply",
"type": "main",
"index": 0
}
]
]
},
"Claude Haiku": {
"ai_languageModel": [
[
{
"node": "AI Parse Order",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Build Message": {
"main": [
[
{
"node": "Send to Customer",
"type": "main",
"index": 0
}
]
]
},
"Read All Rows": {
"main": [
[
{
"node": "Detect Changed Row",
"type": "main",
"index": 0
}
]
]
},
"Route Message": {
"main": [
[
{
"node": "Message Switch",
"type": "main",
"index": 0
}
]
]
},
"AI Parse Order": {
"main": [
[
{
"node": "Build Order Response",
"type": "main",
"index": 0
}
]
]
},
"Every 1 Minute": {
"main": [
[
{
"node": "Read All Rows",
"type": "main",
"index": 0
}
]
]
},
"Message Switch": {
"main": [
[
{
"node": "Send Welcome",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Help",
"type": "main",
"index": 0
}
],
[
{
"node": "AI Parse Order",
"type": "main",
"index": 0
}
],
[
{
"node": "Find Order to Cancel",
"type": "main",
"index": 0
}
],
[
{
"node": "Find Order Status",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Menu",
"type": "main",
"index": 0
}
],
[
{
"node": "Read All Orders",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Blocked Message",
"type": "main",
"index": 0
}
]
]
},
"Build My Orders": {
"main": [
[
{
"node": "Send My Orders",
"type": "main",
"index": 0
}
]
]
},
"Read All Orders": {
"main": [
[
{
"node": "Build My Orders",
"type": "main",
"index": 0
}
]
]
},
"Validate Cancel": {
"main": [
[
{
"node": "Can Cancel?",
"type": "main",
"index": 0
}
]
]
},
"Send to Customer": {
"main": [
[
{
"node": "Prep Sheet Update",
"type": "main",
"index": 0
}
]
]
},
"Find Order Status": {
"main": [
[
{
"node": "Build Status Reply",
"type": "main",
"index": 0
}
]
]
},
"Mark as Cancelled": {
"main": [
[
{
"node": "Send Cancel Reply",
"type": "main",
"index": 0
}
]
]
},
"Prep Sheet Update": {
"main": [
[
{
"node": "Save Last Status Sent",
"type": "main",
"index": 0
}
]
]
},
"Build Status Reply": {
"main": [
[
{
"node": "Send Status",
"type": "main",
"index": 0
}
]
]
},
"Detect Changed Row": {
"main": [
[
{
"node": "Build Message",
"type": "main",
"index": 0
}
]
]
},
"Build Order Response": {
"main": [
[
{
"node": "Save Order",
"type": "main",
"index": 0
}
]
]
},
"Find Order to Cancel": {
"main": [
[
{
"node": "Validate Cancel",
"type": "main",
"index": 0
}
]
]
},
"Customer Bot Listener": {
"main": [
[
{
"node": "Route Message",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This professional n8n workflow provides an end-to-end solution for small restaurants. It includes a Telegram Customer Bot for placing orders and an Automated Notification System that updates customers via Google Sheets.
Source: https://n8n.io/workflows/14136/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow creates a multi-talented AI assistant named Simran that interacts with users via Telegram. It can handle text and voice messages, understand the user's intent, and perform various tasks.
Generate AI viral videos with NanoBanana & VEO3, shared on socials via Blotato 2. Uses @blotato/n8n-nodes-blotato, googleSheets, lmChatOpenAi, toolThink. Event-driven trigger; 94 nodes.
This project is a template for building a complete academic virtual assistant using n8n. It connects to Telegram, answers frequently asked questions by querying MongoDB, keeps the community informed a
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This template is designed for marketers, content creators, and e-commerce brands who want to automate the creation of professional ad videos at scale. It’s ideal for teams looking to generate consiste