{
  "name": "Review signal: a competitor's one and two-star Trustpilot reviews to Slack every Monday",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "9977eb5d-36d2-576b-88bc-b319a243abc9",
      "name": "Every Monday at 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "733f1773-a746-5b42-a5c1-6f868f7677e8",
              "name": "companies",
              "value": "octopus.energy\nbritishgas.co.uk\nedfenergy.com\neonnext.com\novoenergy.com\nscottishpower.co.uk\nuw.co.uk\nutilita.co.uk\nso.energy\noutfoxthemarket.co.uk\ngoodenergy.co.uk\necotricity.co.uk\nfuseenergy.com\n100green.com\nsainsburysenergy.com\nbritishgaslite.co.uk\nsseairtricity.com\npowerni.co.uk\nbudgetenergy.co.uk\nfirmusenergy.co.uk\npozitive.energy\nyuenergy.co.uk\nvaldaenergy.com\nsmartestenergy.com\ntotalgp.com\ncoronaenergy.co.uk\nsefe-energy.co.uk\ncrowngas.co.uk\nugp.co.uk\ndyce-energy.co.uk",
              "type": "string"
            },
            {
              "id": "67645d43-296b-59a7-8d3a-0361f2848d94",
              "name": "stars",
              "value": "1, 2",
              "type": "string"
            },
            {
              "id": "62c597f5-ca45-538b-bda9-e7d23fe7c3fe",
              "name": "period",
              "value": "7 days",
              "type": "string"
            },
            {
              "id": "fa91dac3-6784-5489-8fc9-bd1aad3b05a7",
              "name": "maxReviewsPerCompany",
              "value": 200,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "97a1eca0-a14b-5477-bcb9-85684c57aa23",
      "name": "Competitors and filters",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "resource": "Actors",
        "operation": "Run actor and get dataset",
        "actorId": {
          "__rl": true,
          "value": "1YOMddz8Mm2fXDMnW",
          "mode": "id"
        },
        "customBody": "={{ JSON.stringify({\n  companies: String($json.companies).split('\\n').map((s) => s.trim()).filter(Boolean),\n  stars: String($json.stars ?? '1, 2').split(',').map((s) => s.trim()).filter(Boolean),\n  dateFrom: String($json.period ?? '7 days'),\n  sort: 'recency',\n  maxReviewsPerCompany: Number($json.maxReviewsPerCompany ?? 200),\n  includeCompanyInfo: false,\n  onlyNew: true,\n  maxResults: 2000\n}) }}",
        "maxTotalChargeUsd": 1
      },
      "id": "79e75789-657a-5eaa-b54c-ff4ca10dd02b",
      "name": "Find new bad reviews",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 2,
      "position": [
        480,
        0
      ],
      "credentials": {
        "apifyApi": {
          "id": "REPLACE_WITH_YOUR_APIFY_CREDENTIAL",
          "name": "Apify account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// One Slack message a week: per competitor, how many 1-2 star reviews arrived, what they are about, how many got a\n// reply, and links to the newest three. Reviewer names are never read or posted. A quiet week sends nothing.\nconst THEMES = {\n  \"Getting through to someone\": \"on hold|hold for|wait(?:ing)? times?|call ?backs?|no (?:response|reply|answer)|not (?:responded|replied)|get through|got through|hung up|hang up|cut off|disconnected|no one|nobody|chase|chasing|ignored|phone calls|emails?\\\\b|live chat|web ?chat|\\\\bchat\\\\b\",\n  \"Billing and estimated bills\": \"\\\\bbills?\\\\b|billing|billed|overcharg|over-charg|estimated|estimates?\\\\b|statement|back[- ]?bill|charged|charges\",\n  \"Smart meters and readings\": \"smart meter|\\\\bmeters?\\\\b|readings?\\\\b|\\\\bIHD\\\\b|in-home display|display unit\",\n  \"Engineers and appointments\": \"engineers?|appointments?|install(?:ation|ed|er)?\\\\b|no[- ]show|time slot|visit\",\n  \"Direct debit and payments\": \"direct debit|\\\\bDD\\\\b|payment plan|monthly payments?|payments? (?:went|gone|increased|taken)|took .{0,20}payment\",\n  \"Refunds and credit balances\": \"refund|credit balance|money back|owed|owe me|in credit\",\n  \"Switching, moving and closing accounts\": \"switch(?:ed|ing)?\\\\b|moving (?:home|house|out)|moved (?:home|house|out|in)|new supplier|final bill|clos(?:e|ed|ing) (?:my|the|our) account|cancel\",\n  \"Tariffs and prices\": \"tariff|unit rate|standing charge|price|expensive|\\\\brates?\\\\b\",\n  \"Debt collection and court\": \"debt|collectors?|collection agenc|bailiff|court|warrant|\\\\bCCJ\\\\b|credit (?:file|rating|score)\",\n  \"Contracts and renewals\": \"contract|renewal|renewed|rolled over|rollover|out[- ]of[- ]contract|deemed|broker|termination\",\n  \"Rude or unhelpful staff\": \"\\\\brude\\\\b|unhelpful|attitude|incompetent|clueless|no idea|lied|liars?\\\\b|patroni[sz]\",\n  \"Formal complaints and the ombudsman\": \"ombudsman|formal complaint|complaints? (?:team|department|process)|escalat\",\n  \"Prepayment and top-ups\": \"prepay|pre-pay|top[- ]?ups?|pay as you go|\\\\bPAYG\\\\b|emergency credit\",\n  \"Solar, EV and export\": \"solar|export|\\\\bEV\\\\b|electric (?:car|vehicle)|heat pump|batter(?:y|ies)|\\\\bSEG\\\\b\",\n  \"Broadband, phone and other services\": \"broadband|wi-?fi|internet|home phone|landline|mobile|\\\\bsim\\\\b|boiler|home ?care|insurance\"\n};\nconst rx = Object.fromEntries(Object.entries(THEMES).map(([k, v]) => [k, new RegExp(v, 'i')]));\nconst reviews = $input.all().map((i) => i.json).filter((r) => r.type === 'review');\nif (reviews.length === 0) return [];\nconst esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst byCompany = new Map();\nfor (const r of reviews) {\n  const k = r.companyName ?? r.input ?? 'Other';\n  if (!byCompany.has(k)) byCompany.set(k, []);\n  byCompany.get(k).push(r);\n}\nconst blocks = [...byCompany.entries()].sort((a, b) => b[1].length - a[1].length).map(([company, list]) => {\n  const counts = {};\n  for (const r of list) {\n    const text = `${r.title ?? ''}\\n${r.text ?? ''}`;\n    for (const [k, re] of Object.entries(rx)) if (re.test(text)) counts[k] = (counts[k] ?? 0) + 1;\n  }\n  const top = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 3)\n    .map(([k, n]) => `${k} ${Math.round((100 * n) / list.length)}%`).join(' · ');\n  const replied = list.filter((r) => r.replyPublishedAt).length;\n  const newest = [...list].sort((a, b) => String(b.publishedAt).localeCompare(String(a.publishedAt))).slice(0, 3)\n    .map((r) => `   ${'★'.repeat(r.rating)} <${r.reviewUrl}|${esc(String(r.title ?? 'untitled').slice(0, 90))}>`);\n  return [`*${esc(company)}*: ${list.length} one and two-star review${list.length === 1 ? '' : 's'}, ${replied} with a reply so far`,\n          top ? `   ${esc(top)}` : '   no theme matched', ...newest].join('\\n');\n});\nconst title = `${reviews.length} one and two-star reviews at ${byCompany.size} competitor${byCompany.size === 1 ? '' : 's'} in the last 7 days`;\nreturn [{ json: { text: `*${title}*\\n\\n${blocks.slice(0, 15).join('\\n\\n')}${blocks.length > 15 ? `\\n\\n…and ${blocks.length - 15} more companies in the Apify dataset.` : ''}` } }];"
      },
      "id": "5a83fb90-9af2-5154-aed4-200c3dad6243",
      "name": "Build the weekly summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        0
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#competitors",
          "mode": "name"
        },
        "text": "={{ $json.text }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "9f93e4a3-e190-5a17-a774-8c78023ae95f",
      "name": "Post to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        960,
        0
      ],
      "credentials": {
        "slackApi": {
          "id": "REPLACE_WITH_YOUR_SLACK_CREDENTIAL",
          "name": "Slack account"
        }
      }
    },
    {
      "parameters": {
        "content": "## Competitors and filters\nOne website domain per line. Stars are comma-separated; the period is a date (2026-09-01) or a span (7 days).",
        "height": 280,
        "width": 660,
        "color": 7
      },
      "id": "4ca8ef37-9f19-5a06-94ce-f2b4bb52de6a",
      "name": "Companies",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -130
      ]
    },
    {
      "parameters": {
        "content": "## Build and send\nOne message per week, grouped by company, busiest first.",
        "height": 290,
        "width": 480,
        "color": 7
      },
      "id": "0e8d62e9-46c0-508b-b9a0-b7e592ae4474",
      "name": "Send",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        670,
        -130
      ]
    },
    {
      "parameters": {
        "content": "## A competitor's unhappy customers this week, in Slack\n\nThe workflow from locaihost.org/blog/review-signal-uk-energy-suppliers/ . Every Monday it reads the last seven days of one and two-star Trustpilot reviews for the companies you list, and posts one message: per company, how many arrived, the top complaint themes, how many got a reply, and links to the newest three.\n\n### How it works\n1. **Competitors and filters** sends your list to the **trustpilot-reviews** Actor on Apify.\n2. The Actor reads each company's public Trustpilot page as a logged-out visitor and, with `onlyNew`, returns only reviews it has not sent before.\n3. A Code node groups them by company and counts complaint themes with keyword lists (edit `THEMES` at the top to fit your market). Reviewer names are never read or posted. A quiet week sends nothing.\n\n### Setup\n1. Add your Apify API token as an *Apify API* credential.\n2. Pick your Slack credential and channel in **Post to Slack**.\n3. Edit **Competitors and filters**: one website domain per line (`octopus.energy`, `britishgas.co.uk`). It comes with the 30 UK energy suppliers from the article.\n\n### Cost per run\n$0.50 per 1,000 reviews. For the 30 suppliers in the article that was about 950 reviews a week ($0.48). The node caps each run at $1. A company with more than 200 bad reviews a week hits the per-company limit: switch the trigger to daily and the period to `2 days`.\n\n**Use it lawfully:** this is a view of where competitors struggle, not a list of people to contact. Don't contact or profile reviewers.\n\n**Apify node:** this uses the verified Apify node (`@apify/n8n-nodes-apify`). n8n Cloud installs it from the canvas; on self-hosted n8n add it under Settings > Community nodes.\n\nMade by locaihost data (locaihost.org).",
        "height": 900,
        "width": 580
      },
      "id": "d19f587c-6379-5014-ac8b-8cda4920fd95",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -680,
        -240
      ]
    }
  ],
  "connections": {
    "Every Monday at 08:00": {
      "main": [
        [
          {
            "node": "Competitors and filters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Competitors and filters": {
      "main": [
        [
          {
            "node": "Find new bad reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find new bad reviews": {
      "main": [
        [
          {
            "node": "Build the weekly summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build the weekly summary": {
      "main": [
        [
          {
            "node": "Post to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
