{
  "id": "IxqVFDv8v4qKO24G",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "WhatsApp AI CRM \u2014 Whapi + Ollama",
  "tags": [],
  "nodes": [
    {
      "id": "e5a09e9a-3413-456d-85dd-013aced66a04",
      "name": "Sticky Note \u2014 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 620,
        "content": "## \ud83d\udcf1 WhatsApp AI CRM \u2014 Whapi + Ollama\n\nAutomatically respond to WhatsApp messages with a **local AI model (Ollama)**, log every conversation in **Google Sheets**, and capture leads \u2014 all without paying for cloud LLMs.\n\n**Use cases:** Real estate, service businesses, agencies, appointment booking, customer support.\n\n---\n### \u2699\ufe0f Setup Checklist\n1. Connect **Google Sheets OAuth2** credentials\n2. Replace the Google Sheet ID in all Sheets nodes\n3. Add your **Whapi API token** to the *Send Reply* node\n4. Add your **Ollama** credentials (base URL of your Ollama server)\n5. Point your **Whapi webhook** to this workflow's webhook URL\n6. Create the two sheets: `History` and `Leads` (see sticky notes below)\n7. \u270f\ufe0f Edit the system prompt in **Build AI Prompt** to match your business"
      },
      "typeVersion": 1
    },
    {
      "id": "403b4472-5305-4c9a-9eb9-db2be0fae157",
      "name": "Sticky Note \u2014 Sheets Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        896
      ],
      "parameters": {
        "color": 5,
        "width": 420,
        "height": 280,
        "content": "### \ud83d\udcca Google Sheet Structure\n\nCreate **one Google Sheet** with two tabs:\n\n**Tab 1 \u2014 `History`** (conversation log)\n| Phone | Name | Role | Message | Timestamp |\n\n**Tab 2 \u2014 `Leads`** (CRM)\n| Phone | Name | Last Message | Last Contact | Status |\n\n> Copy the Sheet ID from the URL:\n> `docs.google.com/spreadsheets/d/**[SHEET_ID]**/edit`\n> Paste it into all 4 Google Sheets nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "006d9f66-657a-4a9c-8200-ff43d164effa",
      "name": "Sticky Note \u2014 Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1264,
        480
      ],
      "parameters": {
        "color": 4,
        "width": 300,
        "height": 232,
        "content": "### \ud83d\udd17 Step 1 \u2014 Receive WhatsApp Message\nThis webhook receives all incoming events from **Whapi.cloud**.\n\n**To connect:**\n1. Copy this node's **Production URL**\n2. In your Whapi dashboard \u2192 Settings \u2192 Webhook URL \u2192 paste it\n3. Enable the `messages` event type"
      },
      "typeVersion": 1
    },
    {
      "id": "9eab2a42-27eb-4489-8e1b-142fd3e16058",
      "name": "Sticky Note \u2014 Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1728,
        432
      ],
      "parameters": {
        "color": 4,
        "width": 260,
        "height": 254,
        "content": "### \ud83d\udd0d Step 2 \u2014 Filter\nOnly processes **inbound text messages** from customers.\n\nIgnores:\n- Messages sent by you (fromMe = true)\n- Media, voice, or sticker messages\n- Empty payloads"
      },
      "typeVersion": 1
    },
    {
      "id": "b56ee197-b2e6-43e6-af9b-5bbcb6a2ce0b",
      "name": "Sticky Note \u2014 AI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2048,
        288
      ],
      "parameters": {
        "color": 3,
        "width": 420,
        "height": 292,
        "content": "### \ud83e\udde0 Step 3 \u2014 Build Prompt + AI Reply\nFetches past conversation history for this contact, then builds a full prompt with:\n- System instructions (your business persona)\n- Conversation history\n- The new message\n\n\u270f\ufe0f **Customise the system prompt** inside the *Build AI Prompt* code node to match your industry (real estate, bookings, support, etc.)\n\nThe **Ollama** node runs a local model \u2014 default is `gemma3:1b`. Change it to `llama3`, `mistral`, or any model you have pulled."
      },
      "typeVersion": 1
    },
    {
      "id": "e74e29da-2404-41fb-b666-b27a0829690f",
      "name": "Sticky Note \u2014 CRM & Send",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2768,
        864
      ],
      "parameters": {
        "color": 6,
        "width": 420,
        "height": 278,
        "content": "### \ud83d\udcbe Step 4 \u2014 Log + CRM + Send\n- **Log User Message** \u2014 appends the customer message to History sheet\n- **Log AI Reply** \u2014 appends the AI response to History sheet\n- **Upsert Lead** \u2014 creates or updates the lead record in the Leads sheet (matched by phone number)\n- **Send Reply via Whapi** \u2014 delivers the AI reply back to the customer on WhatsApp\n\n> \u26a0\ufe0f Replace the `Bearer` token in the *Send Reply* HTTP node with your Whapi API key."
      },
      "typeVersion": 1
    },
    {
      "id": "1c487dac-cd45-4ba2-93a8-dbff7fef4abc",
      "name": "Receive WhatsApp Message",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1360,
        784
      ],
      "parameters": {
        "path": "whapi-crm",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "fa7ebcb1-7bcd-4185-b85b-884050e3eb11",
      "name": "Extract Message Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1600,
        784
      ],
      "parameters": {
        "jsCode": "// Extract data from Whapi.cloud webhook payload\nconst body = $input.first().json.body || $input.first().json;\n\n// Whapi sends a messages array\nconst messages = body.messages || [];\n\nif (messages.length === 0) {\n  return [{ json: { skip: true, phone: '', name: '', messageText: '', fromMe: true, messageType: 'unknown' } }];\n}\n\nconst msg = messages[0];\n\n// Extract phone number (remove @s.whatsapp.net if present)\nconst rawPhone = msg.chatId || msg.from || '';\nconst phone = rawPhone.replace('@s.whatsapp.net', '').replace('@c.us', '');\n\nreturn [{\n  json: {\n    skip: false,\n    phone: phone,\n    name: msg.pushName || msg.senderName || 'Unknown',\n    messageText: msg.body || (msg.text && msg.text.body) || '',\n    fromMe: msg.fromMe === true,\n    messageType: msg.type || 'unknown',\n    messageId: msg.id || '',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "209fa1ad-637b-44d9-9093-9323bb0e191e",
      "name": "Is Text From Customer?",
      "type": "n8n-nodes-base.if",
      "position": [
        1840,
        784
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-1",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.fromMe }}",
              "rightValue": false
            },
            {
              "id": "cond-2",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.messageType }}",
              "rightValue": "text"
            },
            {
              "id": "cond-3",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.skip }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "05357c62-23f3-4e14-999c-a3d5a8e22ecc",
      "name": "Read Conversation History",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2080,
        688
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $('Is Text From Customer?').first().json.phone }}",
              "lookupColumn": "Phone"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "History"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5,
      "alwaysOutputData": true
    },
    {
      "id": "f09be5f9-0b71-440c-94cc-c84c3652d7a2",
      "name": "Build AI Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        688
      ],
      "parameters": {
        "jsCode": "// Get data from earlier nodes\nconst msgData = $('Is Text From Customer?').first().json;\nconst historyRows = $input.all();\n\n// Build conversation history string from Google Sheets rows\nlet historyText = '';\nfor (const row of historyRows) {\n  if (row.json.Role && row.json.Message) {\n    const label = row.json.Role === 'user' ? msgData.name : 'Assistant';\n    historyText += `${label}: ${row.json.Message}\\n`;\n  }\n}\n\n// \u270f\ufe0f CUSTOMISE THIS SYSTEM PROMPT for your business\nconst systemPrompt = `You are a friendly and professional real estate assistant available 24/7. Your job is to:\n- Welcome potential buyers/renters warmly\n- Understand their property needs (location, budget, type, size)\n- Answer questions about available properties\n- Collect their contact info naturally during conversation\n- Offer to schedule property viewings\n- Keep responses short and conversational (2-4 sentences max)\n- Always reply in the SAME LANGUAGE the customer writes in\n\nCustomer name: ${msgData.name}\nCustomer phone: ${msgData.phone}`;\n\nconst fullPrompt = `${systemPrompt}\n\n--- Conversation History ---\n${historyText || '(This is the first message)'}---\n\n${msgData.name}: ${msgData.messageText}\nAssistant:`;\n\nreturn [{\n  json: {\n    ...msgData,\n    prompt: fullPrompt\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f7d2e632-97d2-4710-92db-66908a6ff896",
      "name": "Generate AI Reply",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2512,
        608
      ],
      "parameters": {
        "text": "={{ $json.prompt }}",
        "promptType": "define"
      },
      "typeVersion": 1.4
    },
    {
      "id": "680c228d-9727-4da1-a78e-e69473b249e0",
      "name": "Ollama Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOllama",
      "position": [
        2576,
        928
      ],
      "parameters": {
        "model": "gemma3:1b",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "b83f682a-68a2-4a35-bd6c-0b14755d50b5",
      "name": "Log User Message",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2800,
        688
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Role",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Role",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Message",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Timestamp",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "History"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "496cbb67-eef1-460c-aa6c-99ebe9c09422",
      "name": "Log AI Reply",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3040,
        688
      ],
      "parameters": {
        "columns": {
          "value": {
            "Name": "={{ $('Build AI Prompt').first().json.name }}",
            "Role": "assistant",
            "Phone": "={{ $('Build AI Prompt').first().json.phone }}",
            "Message": "={{ $('Generate AI Reply').first().json.text }}",
            "Timestamp": "={{ $now.toISO() }}"
          },
          "schema": [
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Role",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Role",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Message",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "History"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "467eb3c8-7b2d-48df-87dc-f74c2d5ac11e",
      "name": "Upsert Lead in CRM",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3280,
        688
      ],
      "parameters": {
        "columns": {
          "value": {
            "Name": "={{ $('Build AI Prompt').first().json.name }}",
            "Phone": "={{ $('Build AI Prompt').first().json.phone }}",
            "Status": "New Lead",
            "Last Contact": "={{ $now.toISO() }}",
            "Last Message": "={{ $('Build AI Prompt').first().json.messageText }}"
          },
          "schema": [
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Last Message",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Last Message",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Last Contact",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Last Contact",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Phone"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "593de6dc-af8f-4238-a663-50917a2054ce",
      "name": "Send Reply via Whapi",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3520,
        688
      ],
      "parameters": {
        "url": "https://gate.whapi.cloud/messages/text",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ to: $('Build AI Prompt').first().json.phone, body: $('Generate AI Reply').first().json.text }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "0823fded-22d8-4724-9c0f-92ece36b3b61",
  "connections": {
    "Log AI Reply": {
      "main": [
        [
          {
            "node": "Upsert Lead in CRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ollama Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate AI Reply",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Build AI Prompt": {
      "main": [
        [
          {
            "node": "Generate AI Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log User Message": {
      "main": [
        [
          {
            "node": "Log AI Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI Reply": {
      "main": [
        [
          {
            "node": "Log User Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Lead in CRM": {
      "main": [
        [
          {
            "node": "Send Reply via Whapi",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Message Data": {
      "main": [
        [
          {
            "node": "Is Text From Customer?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Text From Customer?": {
      "main": [
        [
          {
            "node": "Read Conversation History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive WhatsApp Message": {
      "main": [
        [
          {
            "node": "Extract Message Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Conversation History": {
      "main": [
        [
          {
            "node": "Build AI Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}