{
  "name": "Run scheduled recurring scans from Google Sheets",
  "nodes": [
    {
      "parameters": {
        "content": "## Run Scheduled Recurring Scans from Google Sheets\n\n**Who is this for:** Users who want to manage recurring scan schedules in a spreadsheet rather than creating individual campaigns.\n\n**What this workflow does:**\n1. Reads scan configuration from Google Sheets\n2. Runs scans on your specified schedule\n3. Supports different frequencies per location\n4. Logs results back to the sheet\n5. Sends summary notification\n\n**How to set up:**\n1. Add your Local Falcon API credentials (get your key at https://www.localfalcon.com/api/credentials)\n2. Create a Google Sheet with columns: place_id, keyword, lat, lng, frequency, last_scan, next_scan\n3. Add your scan configurations\n4. Activate the workflow\n\n**Requirements:**\n- Local Falcon account with API access and credits\n- Google Sheets with scan configuration\n- Sufficient credits for recurring scans\n\n**How to customize:**\n- Add priority levels for critical locations\n- Implement credit budgeting logic\n- Create different schedules by day of week\n- Add error retry logic",
        "height": 500,
        "width": 460,
        "color": 5
      },
      "id": "sticky-main",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        100,
        -160
      ]
    },
    {
      "parameters": {
        "content": "### Sheet Format\n\n| place_id | keyword | lat | lng | frequency | last_scan |\n|----------|---------|-----|-----|-----------|------------|\n| ChIJ... | pizza | 40.7 | -74 | daily | 2024-01-15 |\n| ChIJ... | dentist | 34.0 | -118 | weekly | 2024-01-10 |",
        "height": 160,
        "width": 340
      },
      "id": "sticky-format",
      "name": "Sticky Note Format",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        580,
        -160
      ]
    },
    {
      "parameters": {
        "content": "### Step 1: Schedule\nRuns hourly to check due scans.",
        "height": 100,
        "width": 200
      },
      "id": "sticky-step1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        40,
        640
      ]
    },
    {
      "parameters": {
        "content": "### Step 2: Read Config\nGets scan configuration.",
        "height": 100,
        "width": 200
      },
      "id": "sticky-step2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        300,
        640
      ]
    },
    {
      "parameters": {
        "content": "### Step 3: Filter Due\nFinds scans that are due.",
        "height": 100,
        "width": 200
      },
      "id": "sticky-step3",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        560,
        640
      ]
    },
    {
      "parameters": {
        "content": "### Step 4: Run Scans\nExecutes due scans.",
        "height": 100,
        "width": 200
      },
      "id": "sticky-step4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        820,
        640
      ]
    },
    {
      "parameters": {
        "content": "### Step 5: Update\nLogs results to sheet.",
        "height": 100,
        "width": 200
      },
      "id": "sticky-step5",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1080,
        640
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      },
      "id": "schedule",
      "name": "Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        140,
        420
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SCAN_CONFIG_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Scans"
        },
        "options": {}
      },
      "id": "read-config",
      "name": "Read Scan Config",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        400,
        420
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const configs = $input.all();\nconst now = new Date();\nconst dueScans = [];\n\nfor (const config of configs) {\n  const data = config.json;\n  const lastScan = data.last_scan ? new Date(data.last_scan) : new Date(0);\n  const frequency = (data.frequency || 'daily').toLowerCase();\n  \n  let isDue = false;\n  const hoursSinceLastScan = (now - lastScan) / (1000 * 60 * 60);\n  \n  switch (frequency) {\n    case 'hourly':\n      isDue = hoursSinceLastScan >= 1;\n      break;\n    case 'daily':\n      isDue = hoursSinceLastScan >= 24;\n      break;\n    case 'weekly':\n      isDue = hoursSinceLastScan >= 168;\n      break;\n    case 'monthly':\n      isDue = hoursSinceLastScan >= 720;\n      break;\n  }\n  \n  if (isDue) {\n    dueScans.push({ json: data });\n  }\n}\n\nreturn dueScans;"
      },
      "id": "filter-due",
      "name": "Filter Due Scans",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        420
      ]
    },
    {
      "parameters": {
        "resource": "scan",
        "operation": "run",
        "platform": "={{ $json.platform || 'google' }}",
        "placeId": "={{ $json.place_id }}",
        "keyword": "={{ $json.keyword }}",
        "lat": "={{ $json.lat }}",
        "lng": "={{ $json.lng }}",
        "gridSize": "={{ $json.grid_size || 7 }}",
        "radius": "={{ $json.radius || 1 }}",
        "measurement": "mi"
      },
      "id": "run-scan",
      "name": "Run Due Scan",
      "type": "@local-falcon/n8n-nodes-localfalcon.localFalcon",
      "typeVersion": 1,
      "position": [
        920,
        420
      ],
      "credentials": {
        "localFalconApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SCAN_CONFIG_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Scans"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "last_scan": "={{ $now.toISO() }}",
            "last_result": "={{ $json.avg_rank || 'N/A' }}",
            "report_key": "={{ $json.report_key }}"
          }
        },
        "options": {}
      },
      "id": "update-sheet",
      "name": "Update Last Scan",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1180,
        420
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Every Hour": {
      "main": [
        [
          {
            "node": "Read Scan Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Scan Config": {
      "main": [
        [
          {
            "node": "Filter Due Scans",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Due Scans": {
      "main": [
        [
          {
            "node": "Run Due Scan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Due Scan": {
      "main": [
        [
          {
            "node": "Update Last Scan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}