{
  "id": "5yXquF2kybZBhbRV",
  "name": "Monitor Google Maps reviews from Apify to Slack and Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "f2753220-ab01-4a39-86c6-d9e7f4de32a0",
      "name": "Every 24 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -496,
        48
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "55818ecf-0a34-4a9f-bc6c-c399879c5f58",
      "name": "CONFIG (Edit Here)",
      "type": "n8n-nodes-base.set",
      "notes": "Settings: Enter your Store URL and Sheet ID here",
      "position": [
        -272,
        48
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "MAPS_URL",
              "value": "https://www.google.com/maps/place/YOUR_STORE_ID"
            },
            {
              "name": "SHOP_NAME",
              "value": "My Store Name"
            },
            {
              "name": "MANAGER_NAME",
              "value": "Manager"
            },
            {
              "name": "SHEET_ID",
              "value": "YOUR_GOOGLE_SHEET_ID"
            },
            {
              "name": "SHEET_NAME",
              "value": "Reviews"
            }
          ]
        },
        "options": {
          "dotNotation": true
        }
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "b40cd37c-e43c-4a81-9e2f-a9208ed41d23",
      "name": "Get Existing IDs",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "For duplicate check",
      "position": [
        -48,
        144
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "",
          "cachedResultName": "Reviews"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.SHEET_ID }}"
        }
      },
      "notesInFlow": true,
      "typeVersion": 4.1
    },
    {
      "id": "19b27e60-9c7f-4c27-ab7e-09945d8e5725",
      "name": "Save to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        976,
        48
      ],
      "parameters": {
        "columns": {
          "value": {
            "text": "={{ $json.text }}",
            "stars": "={{ $json.stars }}",
            "output": "={{ $json.output }}",
            "ai_reply": "={{ $json.ai_reply }}",
            "reviewId": "={{ $json.reviewerId }}",
            "reviewUrl": "={{ $json.reviewerUrl }}",
            "ai_summary": "={{ $json.ai_summary }}",
            "publishedAt": "={{ $json.publishAt }}",
            "reviewerName": "={{ $json.name }}",
            "publishedAt date": "={{ $json.publishedAtDate }}"
          },
          "schema": [
            {
              "id": "publishedAt date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "publishedAt date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reviewId",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reviewId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "publishedAt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "publishedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reviewerName",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reviewerName",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stars",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stars",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "text",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "text",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ai_summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ai_summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ai_reply",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ai_reply",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reviewUrl",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reviewUrl",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "output",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "output",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "",
          "cachedResultName": "Reviews"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('CONFIG (Edit Here)').item.json.SHEET_ID }}"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "48614959-cb01-4602-8c75-54366dfbd987",
      "name": "If Rating < 4",
      "type": "n8n-nodes-base.if",
      "position": [
        1200,
        48
      ],
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.reviewId }}",
              "value2": 4
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f104e08a-2bd0-4445-984b-2be8a4df0ff6",
      "name": "Slack (Alert)",
      "type": "n8n-nodes-base.slack",
      "position": [
        1424,
        -48
      ],
      "parameters": {
        "text": "\ud83d\udea8 Negative review detected",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_CHANNEL_ID",
          "cachedResultName": "negative"
        },
        "otherOptions": {
          "includeLinkToWorkflow": true
        },
        "authentication": "oAuth2"
      },
      "typeVersion": 2.1
    },
    {
      "id": "7abc738b-c7c2-405b-b2a3-e2b9e17c4a4c",
      "name": "Run an Actor and get dataset",
      "type": "@apify/n8n-nodes-apify.apify",
      "position": [
        -48,
        -48
      ],
      "parameters": {
        "actorId": {
          "__rl": true,
          "mode": "list",
          "value": "Xb8osYTtOjlsgI6k9",
          "cachedResultUrl": "https://console.apify.com/actors/Xb8osYTtOjlsgI6k9/input",
          "cachedResultName": "Google Maps Reviews Scraper (compass/Google-Maps-Reviews-Scraper)"
        },
        "operation": "Run actor and get dataset",
        "customBody": "={\n  \"startUrls\": [\n    {\n      \"url\": \"{{ $('CONFIG (Edit Here)').item.json.MAPS_URL }}\"\n    }\n  ],\n  \"maxReviews\": 10,\n  \"language\": \"en\",\n  \"reviewsSort\": \"newest\"\n}",
        "actorSource": "store"
      },
      "typeVersion": 1
    },
    {
      "id": "3bd96b91-da52-490c-921f-1edba358ea5c",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        416,
        48
      ],
      "parameters": {
        "text": "=Review Text: {{ $json.text }}\nStars: {{ $json.stars }}\nReviewer Name: {{ $json.reviewerName }}",
        "options": {
          "systemMessage": "You are an excellent customer support manager for a physical store.\nBased on the \"Google Maps Review\" and \"Star Rating (1-5)\" provided by the user, create the following two items:\n\n### 1. Summary (For Slack Notification)\nSummarize the main point of the review in under 30 characters so the store manager can grasp it immediately via notification.\n\n### 2. Reply Draft (For Spreadsheet)\nCreate an appropriate reply message from the store.\nStrictly follow these rules:\n- 4-5 Stars: Thank them for the high rating and warmly say you look forward to their next visit.\n- 1-3 Stars: Sincerely apologize for the inconvenience, thank them for the valuable feedback, and express a polite attitude towards improvement.\n- Tone: Polite (formal) but sincere and warm.\n- No signature needed.\n\n### Output Format\nDo not include any preamble or greetings. Output only the following format:\n\n[Summary]\n(Summary text here)\n\n[Reply Draft]\n(Reply draft here)"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "a268a424-af7a-43c8-b646-628bfe03d152",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        464,
        272
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "7a6610c6-cdb9-4de1-af03-dd5c08b3df18",
      "name": "Filter Duplicates",
      "type": "n8n-nodes-base.merge",
      "notes": "Pass only new reviews",
      "position": [
        176,
        48
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "keepNonMatches",
        "mergeByFields": {
          "values": [
            {
              "field1": "reviewerId",
              "field2": "reviewId"
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.1
    },
    {
      "id": "3b954670-8d94-4db0-b637-16bf447fdb4a",
      "name": "Slack (Alert)1",
      "type": "n8n-nodes-base.slack",
      "position": [
        1424,
        144
      ],
      "parameters": {
        "text": "\ud83d\udea8 Positive review received!",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_CHANNEL_ID",
          "cachedResultName": "positive"
        },
        "otherOptions": {
          "includeLinkToWorkflow": true
        },
        "authentication": "oAuth2"
      },
      "typeVersion": 2.1
    },
    {
      "id": "d481159e-c7fd-40f0-9695-aecdb3068015",
      "name": "Code in JavaScript",
      "type": "n8n-nodes-base.code",
      "position": [
        752,
        48
      ],
      "parameters": {
        "jsCode": "// Get all original data from the 'Filter Duplicates' node\nconst originalItems = $('Filter Duplicates').all();\n\n// Get all results from the 'AI Agent' node\nconst aiItems = $input.all();\n\n// Combine data in order and return\nreturn aiItems.map((item, index) => {\n  const aiText = item.json.output || item.json.text || \"\";\n\n  // Split the AI text into \"Summary\" and \"Reply Draft\" based on the English markers\n  const summaryMatch = aiText.match(/\\[Summary\\]\\s*([\\s\\S]*?)\\s*(?=\\[Reply Draft\\]|$)/);\n  const replyMatch = aiText.match(/\\[Reply Draft\\]\\s*([\\s\\S]*)/);\n\n  const summary = summaryMatch ? summaryMatch[1].trim() : \"\";\n  const reply = replyMatch ? replyMatch[1].trim() : \"\";\n\n  // Retrieve original data based on index\n  // Assuming AI processing order matches the original data order\n  const originalData = originalItems[index] ? originalItems[index].json : {};\n\n  return {\n    json: {\n      ...originalData,      // Inherit reviewId, stars, text, etc.\n      ai_summary: summary,  // Split summary\n      ai_reply: reply,      // Split reply draft\n      output: aiText        // Full AI output\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "9a9ca427-4026-4661-916d-9e4b0aa10195",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1168,
        -192
      ],
      "parameters": {
        "width": 512,
        "height": 624,
        "content": "## How it works\n1. **Schedule:** Runs every 24 hours (customizable) to fetch the latest data.\n2. **Scrape:** Uses **Apify** to retrieve the latest reviews from a specific Google Maps URL.\n3. **Filter:** Checks the **Google Sheet** database to identify only new reviews and avoid duplicates.\n4. **AI Analysis:** An **AI Agent** (via OpenRouter/OpenAI) analyzes the review text to:\n   - Generate a short summary.\n   - Draft a polite, context-aware reply based on the star rating (e.g., apologies for low stars, gratitude for high stars).\n5. **Alert:** Sends a **Slack** notification.\n   - **Low Rating (&lt;4 stars):** Alerts a specific channel (e.g., #customer-support) with a warning.\n   - **High Rating:** Alerts a general channel (e.g., #wins) to celebrate.\n6. **Save:** Appends the review details, AI summary, and draft reply to the Google Sheet.\n\n## Requirements\n- **n8n:** Cloud or self-hosted (v1.0+).\n- **Apify Account:** To run the *Google Maps Reviews Scraper*.\n- **Google Cloud Platform:** Enabled Google Sheets API.\n- **Slack Workspace:** A webhook URL or OAuth connection.\n- **OpenRouter (or OpenAI) API Key:** For the LLM generation.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5bebe09f-3c19-4a4c-98cc-0ba8593b72eb",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -464,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 1952,
        "height": 592,
        "content": "## How to set up\n1. **Google Sheets:** Create a new sheet with the following headers in the first row:\n   `reviewId`, `publishedAt`, `reviewerName`, `stars`, `text`, `ai_summary`, `ai_reply`, `reviewUrl`, `output`, `publishedAt date`.\n2. **Configure Credentials:** Set up your accounts for Google Sheets, Apify, Slack, and OpenRouter within n8n.\n3. **Edit the \"CONFIG\" Node:**\n   - `MAPS_URL`: Paste the full Google Maps link to your store.\n   - `SHEET_ID`: Paste the ID found in your Google Sheet URL.\n   - `SHOP_NAME`: Your store's name.\n4. **Slack Nodes:** Select the appropriate channels for positive and negative alerts."
      },
      "typeVersion": 1
    },
    {
      "id": "ce00761e-fd4f-4cb4-8743-c6d8ba6c488a",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 208,
        "content": "## How to customize\n- **Change the AI Persona:** Open the **AI Agent** node and modify the \"System Message\" to match your brand's tone of voice (e.g., casual, formal, or witty).\n- **Adjust Alert Thresholds:** Edit the **If Rating &lt; 4** node to change the criteria for what constitutes a \"negative\" review (e.g., strictly &lt; 3 stars).\n- **Multi-Store Support:** You can loop this workflow over a list of URLs to manage multiple locations in a single execution."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "ec0852e5-41e9-4ed5-995e-cda9b88ad7cc",
  "connections": {
    "AI Agent": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Rating < 4": {
      "main": [
        [
          {
            "node": "Slack (Alert)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack (Alert)1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 24 Hours": {
      "main": [
        [
          {
            "node": "CONFIG (Edit Here)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Existing IDs": {
      "main": [
        [
          {
            "node": "Filter Duplicates",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Filter Duplicates": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CONFIG (Edit Here)": {
      "main": [
        [
          {
            "node": "Get Existing IDs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Run an Actor and get dataset",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Save to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Save to Google Sheets": {
      "main": [
        [
          {
            "node": "If Rating < 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run an Actor and get dataset": {
      "main": [
        [
          {
            "node": "Filter Duplicates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}