{
  "name": "Weekly new-business monitor: Google Maps to Google Sheets (Hubertino)",
  "nodes": [
    {
      "id": "56610d78-22e6-4e99-8a01-ddf36fedba54",
      "parameters": {
        "content": "## Weekly new-business monitor → Google Sheets\n**Who it's for:** anyone who wants to catch businesses as they appear on Google Maps: new restaurants for a food distributor, new clinics for a medical supplier, new shops for a local agency.\n\n**What it does:** every Monday at 08:00 it re-runs the same [Hubertino](https://hubertino.com/?utm_source=n8n&utm_medium=template) Google Maps search, compares the results with the places already in your sheet by `place_id`, and appends only the new ones with a `first_seen` date.\n\n**How it works**\n1. **Every Monday 08:00** starts the run.\n2. **Start scrape** sends your search. The loop polls every 30 s until it's done.\n3. **Fetch all rows** pages through the results.\n4. **Read known places** loads the sheet. **Compare with known places** keeps the places whose `place_id` isn't there yet (*In A only*).\n5. **Append new businesses** adds them with today's date in `first_seen`.\n\nThe first run fills the sheet with everything it finds. Later runs add only what's new. Filter or sort on `first_seen` to see each week's arrivals.\n\n**1. Hubertino API key**\nSign up at [hubertino.com](https://hubertino.com/?utm_source=n8n&utm_medium=template) (100 free credits, no card) → **Dashboard → Settings → API key**. The key starts with `hub_live_` and is shown once.\nIn n8n, create one **Bearer Auth** credential, paste the key as the *Bearer Token*, and select it in every HTTP Request node (Start scrape, Get scrape status, Fetch all rows).\n*Older n8n without Bearer Auth?* Use **Header Auth** with name `Authorization` and value `Bearer hub_live_…`.\n\n**2. Google Sheet**\nCreate a spreadsheet with a tab named `Places` (it can start empty). Connect Google Sheets in **Read known places** and **Append new businesses** and pick the same file in both.\n\n**Credits.** 1 credit = 1 business delivered. A scrape reserves categories × locations × maxResults credits (capped at your balance) and refunds whatever it didn't use when it finishes. Emails and social links cost nothing extra. A weekly run spends credits every week, so keep `maxResults` modest.",
        "height": 1000,
        "width": 560
      },
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -620,
        -420
      ]
    },
    {
      "id": "748878e5-c07c-4125-9d68-8e6d1d44b2cf",
      "parameters": {
        "content": "## Edit your search here\nOpen **Start scrape** and change the JSON body:\n```\n{\n  \"categories\": [\"dentist\"],\n  \"locations\": [\"Austin, TX, United States\"],\n  \"maxResults\": 50\n}\n```\n- `maxResults`: places per category × location pair (1–500)\n- up to 2,000 pairs per scrape\n- optional `\"country\": \"gb\"` picks the Google country edition (otherwise it's inferred from the location)\n- 12 new scrapes per 5 minutes per account",
        "height": 380,
        "width": 380,
        "color": 5
      },
      "name": "Edit your search",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        160,
        160
      ]
    },
    {
      "id": "ef595364-d9b6-487f-925b-71521de6659a",
      "parameters": {
        "content": "## Wait for the scrape\nA scrape takes a few minutes. This loop checks the status every 30 s: `queued` → `running` → `done`. If the scrape ends in `error`, the workflow stops and the reserved credits are refunded automatically.\nThere are no webhooks, so polling is the supported pattern.",
        "height": 260,
        "width": 420,
        "color": 7
      },
      "name": "Polling loop",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        620,
        160
      ]
    },
    {
      "id": "585cdb60-f809-4216-8964-8b65f0daca43",
      "parameters": {
        "content": "## The place_id diff\n*Input A* holds this week's places and *Input B* the rows already in your sheet. Compare Datasets matches them on `place_id` and sends places found only in A to the first output (**In A only**), which is what gets appended.\n**Read known places** runs once (*Execute Once*) and always outputs an item (*Always Output Data*), so an empty sheet on the first run still works.",
        "height": 300,
        "width": 440,
        "color": 6
      },
      "name": "The place_id diff",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1980,
        60
      ]
    },
    {
      "id": "3843fb5f-ce14-42b4-a4b7-f03250b5b3ee",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "name": "Every Monday 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "id": "4a4202de-0a45-4c81-8aef-c077e7ab32ee",
      "parameters": {
        "method": "POST",
        "url": "https://hubertino.com/api/v1/scrapes",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"categories\": [\n    \"restaurant\"\n  ],\n  \"locations\": [\n    \"Austin, TX, United States\"\n  ],\n  \"maxResults\": 100,\n  \"enrichWebsite\": true,\n  \"enrichLinks\": true\n}",
        "options": {
          "timeout": 60000
        }
      },
      "name": "Start scrape",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        220,
        0
      ],
      "notes": "POST /scrapes. Edit the JSON body to change the search."
    },
    {
      "id": "82a4a459-df1d-4adb-b753-0640f573f1c3",
      "parameters": {
        "amount": 30,
        "unit": "seconds"
      },
      "name": "Wait 30 seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        440,
        0
      ],
      "webhookId": "051cd25d-d285-49c7-9cc4-48163eadbd65"
    },
    {
      "id": "02b36b2c-d362-4fb4-9fb7-215677de37fc",
      "parameters": {
        "url": "=https://hubertino.com/api/v1/scrapes/{{ $('Start scrape').first().json.scrape.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 60000
        }
      },
      "name": "Get scrape status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "00b89b04-5441-4614-9b29-2cbcde497000",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "a246a07f-2631-4e40-95b8-e0bc94dda116",
              "leftValue": "={{ $json.scrape.status }}",
              "rightValue": "done",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Scrape finished?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ]
    },
    {
      "id": "6af48012-4fb6-4cb4-b6e8-2ca900387acc",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "f2075e14-0027-4b02-80ae-3a194a703552",
              "leftValue": "={{ $json.scrape.status }}",
              "rightValue": "error",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Scrape failed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        200
      ]
    },
    {
      "id": "b4ed4b39-ba11-4439-b053-43d08d633f64",
      "parameters": {
        "errorMessage": "=Hubertino scrape {{ $json.scrape.id }} failed: {{ $json.scrape.error }}"
      },
      "name": "Stop: scrape failed",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1320,
        180
      ]
    },
    {
      "id": "c6975701-fe9f-41e0-9868-a36eabcc6554",
      "parameters": {
        "url": "=https://hubertino.com/api/v1/scrapes/{{ $('Start scrape').first().json.scrape.id }}/results",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "limit",
              "value": "1000"
            }
          ]
        },
        "options": {
          "timeout": 120000,
          "pagination": {
            "pagination": {
              "parameters": {
                "parameters": [
                  {
                    "type": "qs",
                    "name": "offset",
                    "value": "={{ $pageCount * 1000 }}"
                  }
                ]
              },
              "paginationCompleteWhen": "other",
              "completeExpression": "={{ !$response.body.hasMore }}",
              "limitPagesFetched": true,
              "maxRequests": 100
            }
          }
        }
      },
      "name": "Fetch all rows",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        -200
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "notes": "GET /scrapes/{id}/results, 1000 rows per page, until hasMore is false."
    },
    {
      "id": "135093d9-98b6-4dd4-b2b8-ec70a850c4ea",
      "parameters": {
        "fieldToSplitOut": "rows",
        "options": {}
      },
      "name": "One item per place",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1320,
        -200
      ]
    },
    {
      "id": "eb694ba0-59f4-4a47-b5b3-5fc33099e2a2",
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "8ad9377c-e7b0-43e3-a35f-0e1e022830d8",
              "name": "place_id",
              "value": "={{ $json.place_id || $json.google_id || $json.cid }}",
              "type": "string"
            },
            {
              "id": "1049c520-a87b-44a8-b84d-54932c9e6856",
              "name": "name",
              "value": "={{ $json.name }}",
              "type": "string"
            },
            {
              "id": "1c97feac-b2ce-4f83-babb-4c4faef45554",
              "name": "category",
              "value": "={{ $json.category }}",
              "type": "string"
            },
            {
              "id": "1d344a1b-4965-4fdb-a581-265b00e8dc27",
              "name": "email",
              "value": "={{ $json.email }}",
              "type": "string"
            },
            {
              "id": "59e83102-c4c6-48f2-9ca5-db2e4f6ccf40",
              "name": "phone",
              "value": "={{ $json.phone }}",
              "type": "string"
            },
            {
              "id": "5be05ee6-9b2d-4f19-876e-927b1117a0fb",
              "name": "website",
              "value": "={{ $json.website }}",
              "type": "string"
            },
            {
              "id": "ba7ebf45-0355-4a0e-b8aa-67fdb37b3180",
              "name": "address",
              "value": "={{ $json.address }}",
              "type": "string"
            },
            {
              "id": "5b0cf2f9-8396-4e93-8b82-701cdc16569d",
              "name": "city",
              "value": "={{ $json.city }}",
              "type": "string"
            },
            {
              "id": "606de043-3413-41dd-aa27-4ea04de9d0f1",
              "name": "state",
              "value": "={{ $json.state }}",
              "type": "string"
            },
            {
              "id": "9aeb4708-ac14-4ee3-995c-d803290b942f",
              "name": "postal_code",
              "value": "={{ $json.postal_code }}",
              "type": "string"
            },
            {
              "id": "0867abd7-5fa7-45d5-8c1c-8a331b8ca70e",
              "name": "country_code",
              "value": "={{ $json.country_code }}",
              "type": "string"
            },
            {
              "id": "c0f8ef72-d513-4754-92db-579e5b362918",
              "name": "rating",
              "value": "={{ $json.rating }}",
              "type": "number"
            },
            {
              "id": "4afaa16a-a53c-44d2-87da-2f146adb229c",
              "name": "reviews",
              "value": "={{ $json.reviews }}",
              "type": "number"
            },
            {
              "id": "29977e8b-b4ac-4e45-87d6-432622c2746b",
              "name": "company_facebook",
              "value": "={{ $json.company_facebook }}",
              "type": "string"
            },
            {
              "id": "341e57a6-1c67-47e5-9458-f5486581b96d",
              "name": "company_instagram",
              "value": "={{ $json.company_instagram }}",
              "type": "string"
            },
            {
              "id": "d814e644-101e-4b87-8596-9718da07718c",
              "name": "company_linkedin",
              "value": "={{ $json.company_linkedin }}",
              "type": "string"
            },
            {
              "id": "1bdc3353-97a0-4326-a26f-6f83bd44b4e7",
              "name": "company_x",
              "value": "={{ $json.company_x }}",
              "type": "string"
            },
            {
              "id": "da971316-0389-45c4-b82c-5dd160ba8230",
              "name": "location_link",
              "value": "={{ $json.location_link }}",
              "type": "string"
            },
            {
              "id": "d4d8e7cf-24a2-44f3-a9e0-63ef48aeffc7",
              "name": "query",
              "value": "={{ $json.query }}",
              "type": "string"
            },
            {
              "id": "317f5a56-9d72-4d8a-9a95-f7ad00abd490",
              "name": "hubertino_scrape_id",
              "value": "={{ $('Start scrape').first().json.scrape.id }}",
              "type": "string"
            },
            {
              "id": "3664ab48-dd2e-498b-9d45-d5b0aca61f6e",
              "name": "first_seen",
              "value": "={{ $now.toISODate() }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "name": "Pick columns",
      "position": [
        1540,
        -200
      ]
    },
    {
      "id": "6a384158-849f-4943-abea-de9dfb8071d2",
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Places",
          "mode": "name"
        },
        "options": {}
      },
      "name": "Read known places",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1760,
        -60
      ],
      "executeOnce": true,
      "alwaysOutputData": true
    },
    {
      "id": "f3b5a961-fd61-466e-9425-c0b8154c9559",
      "parameters": {
        "mergeByFields": {
          "values": [
            {
              "field1": "place_id",
              "field2": "place_id"
            }
          ]
        },
        "options": {}
      },
      "name": "Compare with known places",
      "type": "n8n-nodes-base.compareDatasets",
      "typeVersion": 2.3,
      "position": [
        1980,
        -200
      ]
    },
    {
      "id": "b128b37a-6c22-43c5-a900-195777ce5c71",
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Places",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "name": "Append new businesses",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2200,
        -300
      ]
    }
  ],
  "connections": {
    "Every Monday 08:00": {
      "main": [
        [
          {
            "node": "Start scrape",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start scrape": {
      "main": [
        [
          {
            "node": "Wait 30 seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 30 seconds": {
      "main": [
        [
          {
            "node": "Get scrape status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get scrape status": {
      "main": [
        [
          {
            "node": "Scrape finished?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape finished?": {
      "main": [
        [
          {
            "node": "Fetch all rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrape failed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape failed?": {
      "main": [
        [
          {
            "node": "Stop: scrape failed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 30 seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch all rows": {
      "main": [
        [
          {
            "node": "One item per place",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One item per place": {
      "main": [
        [
          {
            "node": "Pick columns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick columns": {
      "main": [
        [
          {
            "node": "Compare with known places",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read known places",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read known places": {
      "main": [
        [
          {
            "node": "Compare with known places",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Compare with known places": {
      "main": [
        [
          {
            "node": "Append new businesses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}
