{
  "id": "EVTOyLitCKuudkS3",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Confirm restaurant orders from WhatsApp with Groq and Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "a068f197-276f-48f4-b3ae-8958d4be0aee",
      "name": "Template Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        112
      ],
      "parameters": {
        "color": 4,
        "width": 700,
        "height": 1008,
        "content": "## \ud83c\udf5b Confirm Restaurant Orders from WhatsApp with Groq and Google Sheets\n\n## Who's it for\nSmall restaurants and cloud kitchens taking orders over WhatsApp who want orders automatically validated against their menu, priced, logged, and confirmed, without a staff member manually reading and tallying every message.\n\n## How it works\nA customer sends their order over WhatsApp. Groq parses the items and quantities, then the workflow checks each item against your live menu sheet (name match and availability) and calculates the total. If anything doesn't match the menu or is unavailable, the customer gets an automatic reply asking them to correct it, nothing gets logged as a real order until it's valid. Once valid, the order is logged to Google Sheets, the customer gets a confirmation with the total, and if the order is above a configurable amount, the owner gets a separate WhatsApp alert for VIP handling.\n\n## How to set up\n1. Connect your WhatsApp Business Cloud credentials to the trigger and both send nodes.\n2. Connect your Groq API credentials to the Chat Model node.\n3. Connect your Google Sheets credentials and create two tabs: Menu and Orders (see the sticky notes near each Sheets node for required columns).\n4. Open the Set Configuration node and fill in your Sheet ID, WhatsApp phone number ID, owner notification number, and VIP order threshold.\n5. Activate the workflow.\n\n## Requirements\n- WhatsApp Business Cloud API access\n- Groq API key\n- Google Sheets account\n\n## How to customize\nEdit the confirmation and clarification message templates to match your restaurant's tone. Adjust the VIP threshold in Set Configuration, or remove that branch entirely if you don't need it."
      },
      "typeVersion": 1
    },
    {
      "id": "0fcbb70d-ac1c-4fc6-bbfb-c0e23e432332",
      "name": "Trigger Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 208,
        "content": "## 1\ufe0f\u20e3 Trigger\nFires on every incoming WhatsApp message. Connect your restaurant's WhatsApp Business number here."
      },
      "typeVersion": 1
    },
    {
      "id": "4eb73868-2732-4814-9d3d-31918fd90649",
      "name": "New WhatsApp Order",
      "type": "n8n-nodes-base.whatsAppTrigger",
      "position": [
        928,
        704
      ],
      "parameters": {
        "options": {},
        "updates": [
          "messages"
        ]
      },
      "credentials": {
        "whatsAppTriggerApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d0e98056-0ade-46f6-9d1f-5c57b7d98d6f",
      "name": "Set Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        1200,
        704
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "restaurant_name",
              "type": "string",
              "value": "REPLACE_WITH_RESTAURANT_NAME"
            },
            {
              "id": "cfg-2",
              "name": "whatsapp_phone_number_id",
              "type": "string",
              "value": "REPLACE_WITH_WHATSAPP_PHONE_NUMBER_ID"
            },
            {
              "id": "cfg-3",
              "name": "owner_notify_number",
              "type": "string",
              "value": "REPLACE_WITH_OWNER_WHATSAPP_NUMBER"
            },
            {
              "id": "cfg-4",
              "name": "vip_order_threshold",
              "type": "number",
              "value": 1000
            },
            {
              "id": "cfg-5",
              "name": "sheet_id",
              "type": "string",
              "value": "REPLACE_WITH_GOOGLE_SHEET_ID"
            },
            {
              "id": "cfg-6",
              "name": "menu_sheet_name",
              "type": "string",
              "value": "Menu"
            },
            {
              "id": "cfg-7",
              "name": "orders_sheet_name",
              "type": "string",
              "value": "Orders"
            },
            {
              "id": "cfg-8",
              "name": "raw_message",
              "type": "string",
              "value": "={{ $json.messages[0].text.body }}"
            },
            {
              "id": "cfg-9",
              "name": "sender_number",
              "type": "string",
              "value": "={{ $json.messages[0].from }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "85c3a245-83b9-4e53-bffe-768cbc974037",
      "name": "Extraction Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1408,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 192,
        "content": "## 2\ufe0f\u20e3 AI Extraction\nGroq parses the WhatsApp message into a list of items and quantities. It doesn't validate against your menu, that happens next."
      },
      "typeVersion": 1
    },
    {
      "id": "32f6e8b1-fc34-4ba9-a4bf-61d526c854b6",
      "name": "Extract Order Items",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        1440,
        704
      ],
      "parameters": {
        "text": "=Extract the food items and quantities the customer is ordering from this WhatsApp message. If a quantity isn't mentioned, assume 1.\n\nMessage: {{ $('Set Configuration').item.json.raw_message }}",
        "batching": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.8
    },
    {
      "id": "35a36f85-101f-4e4a-a136-7e9f28f138ef",
      "name": "Groq Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        1408,
        912
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ad3fc9cb-a692-427a-9c0e-25c7119403f7",
      "name": "Order Items Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1600,
        912
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"items\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"item_name\": {\n            \"type\": \"string\"\n          },\n          \"quantity\": {\n            \"type\": \"number\"\n          }\n        },\n        \"required\": [\n          \"item_name\",\n          \"quantity\"\n        ]\n      }\n    }\n  },\n  \"required\": [\n    \"items\"\n  ]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "d58308ee-25d2-41da-8b8f-bd3286b0de18",
      "name": "Menu Read Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1776,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 212,
        "content": "## 3\ufe0f\u20e3 Read Live Menu\nReads your Menu sheet fresh on every order, so price/availability changes take effect immediately. Required columns: item_name, price, available."
      },
      "typeVersion": 1
    },
    {
      "id": "dd27b273-184a-4fab-8e61-8c4fd3644da3",
      "name": "Read Menu Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1840,
        704
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Set Configuration').item.json.menu_sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.sheet_id }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "0ad486f3-9481-4db7-b9db-ce9f1e7bf792",
      "name": "Validation Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2128,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 216,
        "content": "## 4\ufe0f\u20e3 Validate + Price\nMatches each parsed item against the menu, checks availability, and calculates the total. Anything that doesn't match becomes an issue that blocks the order until it's corrected."
      },
      "typeVersion": 1
    },
    {
      "id": "b460bd6e-2b74-4299-b63c-13dbc7cb0c98",
      "name": "Validate & Price Order",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        704
      ],
      "parameters": {
        "jsCode": "const menuRows = $('Read Menu Sheet').all().map(item => item.json);\nconst orderedItems = $json.output.items || [];\n\nlet total = 0;\nlet issues = [];\nlet lineItems = [];\n\nfor (const ordered of orderedItems) {\n  const menuItem = menuRows.find(m =>\n    String(m.item_name).toLowerCase().trim() === String(ordered.item_name).toLowerCase().trim()\n  );\n\n  if (!menuItem) {\n    issues.push(`\"${ordered.item_name}\" isn't on the menu`);\n    continue;\n  }\n\n  const isAvailable = String(menuItem.available).toLowerCase() !== 'false';\n  if (!isAvailable) {\n    issues.push(`\"${ordered.item_name}\" is currently unavailable`);\n    continue;\n  }\n\n  const price = parseFloat(menuItem.price) || 0;\n  const qty = parseInt(ordered.quantity) || 1;\n  const subtotal = price * qty;\n  total += subtotal;\n\n  lineItems.push({\n    item_name: ordered.item_name,\n    quantity: qty,\n    price: price,\n    subtotal: subtotal\n  });\n}\n\nreturn [{\n  json: {\n    total_price: total,\n    line_items: lineItems,\n    order_summary: lineItems.map(li => `${li.quantity}x ${li.item_name}`).join(', '),\n    issues: issues,\n    issues_summary: issues.join('; '),\n    has_issues: issues.length > 0\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "833b17f4-be43-4d2e-b9bc-1897f6f6cab7",
      "name": "Are There Issues?",
      "type": "n8n-nodes-base.if",
      "position": [
        2400,
        704
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1fe1f5a-fb14-4f17-a577-6dcc2856450c",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.has_issues }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b302cce6-ebd5-4f04-9fb5-6bc89999a636",
      "name": "Issues Branch Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2656,
        976
      ],
      "parameters": {
        "color": 7,
        "width": 316,
        "height": 192,
        "content": "## \u274c If Something's Off\nAsks the customer to correct their order and logs the attempt so nothing valid gets silently dropped, but nothing invalid gets confirmed either."
      },
      "typeVersion": 1
    },
    {
      "id": "8e42729b-435f-42e2-b748-13df0a5ac64b",
      "name": "Ask for Clarification",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        2688,
        1200
      ],
      "parameters": {
        "textBody": "=Sorry, a couple of things in your order need fixing: {{ $json.issues_summary }}. Could you resend the corrected order?",
        "operation": "send",
        "phoneNumberId": "={{ $('Set Configuration').item.json.whatsapp_phone_number_id }}",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Set Configuration').item.json.sender_number }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2860a2bf-f83d-4479-b70c-67821f2deee5",
      "name": "Log Clarification Needed",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2944,
        1200
      ],
      "parameters": {
        "columns": {
          "value": {
            "notes": "={{ $json.issues_summary }}",
            "status": "needs_clarification",
            "total_price": "={{ $('Validate & Price Order').item.json.total_price }}",
            "order_summary": "={{ $('Validate & Price Order').item.json.order_summary }}",
            "customer_phone": "={{ $('Set Configuration').item.json.sender_number }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Set Configuration').item.json.orders_sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.sheet_id }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "dfddbf8a-e1a8-41bf-bb45-8f6f80f25d5b",
      "name": "Confirmed Branch Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2624,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 196,
        "content": "## \u2705 If Valid\nLogs the order, confirms it to the customer, and separately checks whether it clears the VIP threshold for an owner alert. Both happen off the same log step."
      },
      "typeVersion": 1
    },
    {
      "id": "ce2ee97c-58db-42f8-b28a-82b760b862d7",
      "name": "Log Confirmed Order",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2688,
        688
      ],
      "parameters": {
        "columns": {
          "value": {
            "notes": "",
            "status": "confirmed",
            "total_price": "={{ $json.total_price }}",
            "order_summary": "={{ $json.order_summary }}",
            "customer_phone": "={{ $('Set Configuration').item.json.sender_number }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Set Configuration').item.json.orders_sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.sheet_id }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "72d1dd44-f808-4568-a9fc-c1c669377e3d",
      "name": "Send Order Confirmation",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        3088,
        688
      ],
      "parameters": {
        "textBody": "=Order confirmed: {{ $('Validate & Price Order').item.json.order_summary }}. Total: \u20b9{{ $('Validate & Price Order').item.json.total_price }}. We'll have it ready shortly!",
        "operation": "send",
        "phoneNumberId": "={{ $('Set Configuration').item.json.whatsapp_phone_number_id }}",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Set Configuration').item.json.sender_number }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b28a6540-a3bd-4e9f-869a-1971b103472b",
      "name": "Is VIP Order?",
      "type": "n8n-nodes-base.if",
      "position": [
        2928,
        816
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5662eead-7687-4a57-8303-b1d76dc85442",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $('Validate & Price Order').item.json.total_price }}",
              "rightValue": "={{ $('Set Configuration').item.json.vip_order_threshold }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "16a53905-af7f-44c6-b586-be4b628c548c",
      "name": "Notify Owner - VIP Order",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        3248,
        800
      ],
      "parameters": {
        "textBody": "=Large order just came in: \u20b9{{ $('Validate & Price Order').item.json.total_price }} from {{ $('Set Configuration').item.json.sender_number }}. {{ $('Validate & Price Order').item.json.order_summary }}",
        "operation": "send",
        "phoneNumberId": "={{ $('Set Configuration').item.json.whatsapp_phone_number_id }}",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Set Configuration').item.json.owner_notify_number }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "dcd828cf-1f40-44b0-9b40-7ba31643fa4b",
      "name": "Sheets Schema Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3184,
        1120
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 224,
        "content": "## Sheets Schema\nMenu tab: item_name, price, available (TRUE/FALSE).\nOrders tab: customer_phone, order_summary, total_price, status, notes."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "03e1edca-9d73-442e-96f9-06005895ee21",
  "nodeGroups": [],
  "connections": {
    "Is VIP Order?": {
      "main": [
        [
          {
            "node": "Notify Owner - VIP Order",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Extract Order Items",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Read Menu Sheet": {
      "main": [
        [
          {
            "node": "Validate & Price Order",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Are There Issues?": {
      "main": [
        [
          {
            "node": "Log Confirmed Order",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ask for Clarification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration": {
      "main": [
        [
          {
            "node": "Extract Order Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New WhatsApp Order": {
      "main": [
        [
          {
            "node": "Set Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Order Items Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Extract Order Items",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Extract Order Items": {
      "main": [
        [
          {
            "node": "Read Menu Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Confirmed Order": {
      "main": [
        [
          {
            "node": "Send Order Confirmation",
            "type": "main",
            "index": 0
          },
          {
            "node": "Is VIP Order?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask for Clarification": {
      "main": [
        [
          {
            "node": "Log Clarification Needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate & Price Order": {
      "main": [
        [
          {
            "node": "Are There Issues?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}