{
  "name": "Automated Review Responder (Claude + GPT)",
  "nodes": [
    {
      "id": "1",
      "name": "Cron Trigger",
      "type": "n8n-nodes-base.cron",
      "typeVersion": 1,
      "position": [
        200,
        200
      ],
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "hour": "*",
              "minute": "*/5"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "List Reviews - Google Business Profile",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        480,
        200
      ],
      "parameters": {
        "method": "GET",
        "url": "https://mybusiness.googleapis.com/v4/accounts/YOUR_ACCOUNT_ID/locations/YOUR_LOCATION_ID/reviews",
        "responseFormat": "json"
      },
      "credentials": {
        "httpHeaderAuth": "<your credential>"
      }
    },
    {
      "id": "3",
      "name": "Parse Reviews",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        720,
        200
      ],
      "parameters": {
        "functionCode": "// Flatten Google response into one review per item\nconst data = items[0].json || {};\nconst reviews = data.reviews || [];\nreturn reviews.map(r => {\n  const reviewId = r.name ? r.name.split('/').pop() : null;\n  const ratingRaw = r.starRating || r.rating || null;\n  let rating = null;\n  if (typeof ratingRaw === 'string') {\n    const m = ratingRaw.match(/\\d+/);\n    rating = m ? parseInt(m[0], 10) : null;\n  } else if (typeof ratingRaw === 'number') {\n    rating = ratingRaw;\n  }\n  return {\n    json: {\n      reviewId,\n      comment: r.comment || '',\n      rating,\n      reviewer: r.reviewer?.displayName || ''\n    }\n  };\n});"
      }
    },
    {
      "id": "4",
      "name": "Split Reviews",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 1,
      "position": [
        960,
        200
      ],
      "parameters": {
        "batchSize": 1
      }
    },
    {
      "id": "5",
      "name": "Check Processed",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1200,
        200
      ],
      "parameters": {
        "functionCode": "const item = items[0].json;\nconst staticData = getWorkflowStaticData('global');\nstaticData.processedReviews = staticData.processedReviews || {};\nconst already = staticData.processedReviews[item.reviewId] ? 'yes' : 'no';\nreturn [{ json: { ...item, alreadyProcessed: already } }];"
      }
    },
    {
      "id": "6",
      "name": "If Not Processed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1440,
        200
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json[\"alreadyProcessed\"]}}",
              "operation": "equal",
              "value2": "no"
            }
          ]
        }
      }
    },
    {
      "id": "7",
      "name": "If Positive (>=4)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1680,
        200
      ],
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{$json[\"rating\"]}}",
              "operation": "greaterThanOrEqual",
              "value2": 4
            }
          ]
        }
      }
    },
    {
      "id": "8",
      "name": "OpenAI GPT Reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        1920,
        120
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "headerAuth",
        "sendBody": true,
        "jsonParameters": true,
        "body": {
          "model": "gpt-4o-mini",
          "messages": [
            {
              "role": "system",
              "content": "You write friendly replies to positive reviews. Thank warmly, keep it casual yet professional, under 80 words."
            },
            {
              "role": "user",
              "content": "Review: {{$json[\"comment\"]}} | Rating: {{$json[\"rating\"]}}"
            }
          ],
          "max_tokens": 200
        }
      },
      "credentials": {
        "httpHeaderAuth": "<your credential>"
      }
    },
    {
      "id": "9",
      "name": "Claude Reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        1920,
        280
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "headerAuth",
        "sendBody": true,
        "jsonParameters": true,
        "body": {
          "model": "claude-3-5-sonnet-20240620",
          "max_tokens": 350,
          "system": "You are a professional customer success manager writing replies to critical reviews. Apologize, acknowledge, invite them to continue privately. Under 120 words.",
          "messages": [
            {
              "role": "user",
              "content": "Review: {{$json[\"comment\"]}} | Rating: {{$json[\"rating\"]}}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": "<your credential>"
      }
    },
    {
      "id": "10",
      "name": "Merge Positive",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        2160,
        120
      ],
      "parameters": {
        "mode": "mergeByIndex"
      }
    },
    {
      "id": "11",
      "name": "Merge Negative",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        2160,
        300
      ],
      "parameters": {
        "mode": "mergeByIndex"
      }
    },
    {
      "id": "12",
      "name": "Normalize Reply",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        2440,
        200
      ],
      "parameters": {
        "functionCode": "const d = items[0].json;\nlet reply = '';\nif (d.choices?.[0]?.message?.content) reply = d.choices[0].message.content;\nif (d.content?.[0]?.text) reply = d.content[0].text;\nreturn [{ json: { reviewId: d.reviewId, replyText: reply } }];"
      }
    },
    {
      "id": "13",
      "name": "Post Reply - GBP",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        2720,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://mybusiness.googleapis.com/v4/accounts/YOUR_ACCOUNT_ID/locations/YOUR_LOCATION_ID/reviews/{{$json[\"reviewId\"]}}/reply",
        "sendBody": true,
        "jsonParameters": true,
        "body": {
          "comment": "={{$json[\"replyText\"]}}"
        }
      },
      "credentials": {
        "httpHeaderAuth": "<your credential>"
      }
    },
    {
      "id": "14",
      "name": "Mark Processed",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        3000,
        200
      ],
      "parameters": {
        "functionCode": "const id = items[0].json.reviewId;\nif (id) {\n  const s = getWorkflowStaticData('global');\n  s.processedReviews = s.processedReviews || {};\n  s.processedReviews[id] = new Date().toISOString();\n}\nreturn items;"
      }
    }
  ],
  "connections": {
    "Cron Trigger": {
      "main": [
        [
          {
            "node": "List Reviews - Google Business Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Reviews - Google Business Profile": {
      "main": [
        [
          {
            "node": "Parse Reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Reviews": {
      "main": [
        [
          {
            "node": "Split Reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Reviews": {
      "main": [
        [
          {
            "node": "Check Processed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Processed": {
      "main": [
        [
          {
            "node": "If Not Processed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Not Processed": {
      "main": [
        [
          {
            "node": "If Positive (>=4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Positive (>=4)": {
      "main": [
        [
          {
            "node": "OpenAI GPT Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Claude Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT Reply": {
      "main": [
        [
          {
            "node": "Merge Positive",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Claude Reply": {
      "main": [
        [
          {
            "node": "Merge Negative",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Positive": {
      "main": [
        [
          {
            "node": "Normalize Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Negative": {
      "main": [
        [
          {
            "node": "Normalize Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Reply": {
      "main": [
        [
          {
            "node": "Post Reply - GBP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Reply - GBP": {
      "main": [
        [
          {
            "node": "Mark Processed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}