{
  "name": "Monitor competitor mentions in Google News with Apify and Slack",
  "nodes": [
    {
      "parameters": {
        "content": "## Competitor news monitor\n\n**What it does**\nSearches Google News for a topic and posts each new article to Slack or Discord.\n\n**Setup (2 minutes)**\n1. Create a Header Auth credential named `Apify` (name `Authorization`, value `Bearer YOUR_APIFY_TOKEN`) from Apify Console -> Settings -> Integrations.\n2. Open **Configure search** and set `query` and `max_items` - that is the only place you need to edit.\n3. Paste your Slack or Discord webhook URL into **Send the alert**.\n\n**How it behaves**\n- Runs on a schedule; every run asks the Actor for the current rows.\n- Rows already sent are skipped, so you are alerted once per record rather than\n  once per run. The seen-list lives in the workflow's static data and is trimmed\n  to the newest 1,000 keys.\n- A row with no article_url or title is dropped rather than written, because a\n  record with no identifier cannot be deduplicated later.\n- If the run fails or returns nothing, the failure branch posts an alert instead\n  of ending silently.\n\n**Actor used:** https://apify.com/captainhandsome/captainhandsome/google-news-search\n",
        "height": 420,
        "width": 1000
      },
      "id": "note-1",
      "name": "How this works",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        200,
        40
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "id": "trig-1",
      "name": "Every 24 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        220,
        520
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "query",
              "value": "OpenAI OR Anthropic",
              "type": "string"
            },
            {
              "id": "cfg-2",
              "name": "max_items",
              "value": 10,
              "type": "number"
            }
          ]
        }
      },
      "id": "cfg-1",
      "name": "Configure search",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        440,
        520
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/captainhandsome~google-news-search/run-sync-get-dataset-items?clean=true&format=json&timeout=300&maxItems=10",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={\n  \"query\": \"={{ $json.query }}\",\n  \"language\": \"en\",\n  \"country\": \"US\",\n  \"max_items\": \"={{ $json.max_items }}\"\n}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "http-1",
      "name": "Run Apify Actor",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        660,
        520
      ],
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput",
      "notes": "Retries twice, then routes to the failure branch."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "has-rows",
              "leftValue": "={{ $input.all().length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-1",
      "name": "Any records returned?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.0,
      "position": [
        880,
        520
      ]
    },
    {
      "parameters": {
        "jsCode": "const store = $getWorkflowStaticData('global');\nstore.seen = store.seen || {};\nconst output = [];\nfor (const item of $input.all()) {\n  const rows = Array.isArray(item.json) ? item.json : [item.json];\n  for (const row of rows) {\n    const key = row.article_url || row.title;\n    if (key && !store.seen[key]) {\n      store.seen[key] = Date.now();\n      output.push({ json: row });\n    }\n  }\n}\nconst newest = Object.entries(store.seen).sort((a, b) => b[1] - a[1]).slice(0, 1000);\nstore.seen = Object.fromEntries(newest);\nreturn output;"
      },
      "id": "code-1",
      "name": "Keep only new rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        400
      ],
      "notes": "Deduplicates against previous executions using workflow static data."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "id-present",
              "leftValue": "={{ $json.article_url }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "filt-1",
      "name": "Drop rows with no identifier",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        1320,
        400
      ],
      "notes": "A record without an identifier cannot be deduplicated on the next run."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_OR_DISCORD_WEBHOOK_URL",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ text: '*New competitor news*\\n*Title:* ' + $json.title + '\\n*Source:* ' + $json.source_name + '\\n*Published:* ' + $json.published_at + '\\n*Link:* ' + $json.article_url }) }}",
        "options": {}
      },
      "id": "dest-1",
      "name": "Send the alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        1540,
        400
      ],
      "notes": "Paste your Slack or Discord webhook URL here."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_OR_DISCORD_WEBHOOK_URL",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ text: ':warning: Apify run failed or returned no records for ' + 'Monitor competitor mentions in Google News with Apify and Slack' }) }",
        "options": {}
      },
      "id": "alert-1",
      "name": "Alert on failure",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        1100,
        660
      ],
      "onError": "continueRegularOutput",
      "notes": "Runs when the Actor call errors or returns nothing."
    }
  ],
  "connections": {
    "Every 24 Hours": {
      "main": [
        [
          {
            "node": "Configure search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure search": {
      "main": [
        [
          {
            "node": "Run Apify Actor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Apify Actor": {
      "main": [
        [
          {
            "node": "Any records returned?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Alert on failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any records returned?": {
      "main": [
        [
          {
            "node": "Keep only new rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Alert on failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep only new rows": {
      "main": [
        [
          {
            "node": "Drop rows with no identifier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Drop rows with no identifier": {
      "main": [
        [
          {
            "node": "Send the alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
