{
  "name": "Negative Play review alerts (apihq skeleton)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days"
            }
          ]
        }
      },
      "id": "trigger",
      "name": "Every day",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        200,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/apihq~google-play-reviews-scraper/run-sync-get-dataset-items",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_APIFY_TOKEN"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"appIds\": [\n    \"com.spotify.music\"\n  ],\n  \"maxReviews\": 100,\n  \"sort\": \"newest\",\n  \"country\": \"us\",\n  \"language\": \"en\"\n}",
        "options": {}
      },
      "id": "get-new-reviews",
      "name": "Get new reviews",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        430,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "success-check",
              "leftValue": "={{ $json.success }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "split-by-success",
      "name": "Split by success",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        650,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "low-score-check",
              "leftValue": "={{ $json.score }}",
              "rightValue": 2,
              "operator": {
                "type": "number",
                "operation": "lte"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "only-negative",
      "name": "Only 1-2 star reviews",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        880,
        200
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Suppress already-alerted reviews (at-least-once, not exactly-once:\n// static data saves only when the whole execution succeeds, so a\n// failed run can repeat alerts, and the first scheduled run alerts\n// the current backlog). Static data persists only on an ACTIVE\n// workflow; manual test runs always alert. A durable processed-ID\n// store (DB/Sheets upsert on review_id) reduces repeats and preserves\n// dedup state across failed runs; this template still does not\n// guarantee exactly-once Slack delivery.\nconst memory = $getWorkflowStaticData('global');\nmemory.alertedReviewIds = memory.alertedReviewIds || [];\nconst fresh = [];\nfor (const item of $input.all()) {\n  const id = item.json.review_id;\n  if (!id || memory.alertedReviewIds.includes(id)) continue;\n  memory.alertedReviewIds.push(id);\n  fresh.push(item);\n}\n// n8n asks that static data stay small \u2014 keep the memory bounded.\nif (memory.alertedReviewIds.length > 2000) {\n  memory.alertedReviewIds = memory.alertedReviewIds.slice(-1000);\n}\nreturn fresh;"
      },
      "id": "new-reviews-only",
      "name": "New reviews only",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1110,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_INCOMING_WEBHOOK_URL",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: `${$json.score} star review on ${$json.app_id} (${$json.posted_at}):\\n${$json.text || '(rating only, no text)'}\\n${$json.url}` }) }}",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 1100
            }
          }
        }
      },
      "id": "slack-alert",
      "name": "Slack alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        200
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a0",
              "name": "app_id",
              "value": "={{ $json.app_id }}",
              "type": "string"
            },
            {
              "id": "a1",
              "name": "code",
              "value": "={{ $json.code }}",
              "type": "string"
            },
            {
              "id": "a2",
              "name": "request_id",
              "value": "={{ $json.request_id }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "failure-row",
      "name": "Failure row",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        880,
        420
      ]
    }
  ],
  "connections": {
    "Every day": {
      "main": [
        [
          {
            "node": "Get new reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get new reviews": {
      "main": [
        [
          {
            "node": "Split by success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split by success": {
      "main": [
        [
          {
            "node": "Only 1-2 star reviews",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Failure row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Only 1-2 star reviews": {
      "main": [
        [
          {
            "node": "New reviews only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New reviews only": {
      "main": [
        [
          {
            "node": "Slack alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}