{
  "id": "digStaysHospitalityTemplate",
  "versionId": "f6c531e0-8f18-4f76-83e3-530ed85d3a78",
  "name": "UK restaurant territory to Google Sheets",
  "nodes": [
    {
      "id": "2bfdbab8-dbff-466e-916d-e8f1ce16db27",
      "name": "Manual start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        200
      ],
      "parameters": {}
    },
    {
      "id": "42ff1709-c713-4361-a1ce-873f56fdba0e",
      "name": "Weekly Monday 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        420
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9,
              "triggerAtMinute": 0
            }
          ]
        }
      }
    },
    {
      "id": "9e4444a8-1271-4ea0-a708-f9babc5108ef",
      "name": "Configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        200
      ],
      "parameters": {
        "jsCode": "return [{json:{spreadsheetId:'REPLACE_WITH_YOUR_GOOGLE_SHEET_ID',resumeRunId:'',outputMode:'changes',localAuthorities:['Manchester'],postcodeDistricts:['M1'],businessTypeIds:[1],maxRecords:10000,maxChargeUsd:4}}];"
      }
    },
    {
      "id": "7a00d25d-c820-44e4-85a3-9a60ac446eb5",
      "name": "Validate configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        470,
        200
      ],
      "parameters": {
        "jsCode": "const c=$json;if(!['snapshot','changes'].includes(c.outputMode))throw new Error('Choose snapshot or changes');if(c.maxRecords!==10000)throw new Error('This template supports maxRecords 10000');if(!Number.isFinite(c.maxChargeUsd)||c.maxChargeUsd<=0)throw new Error('Set a positive run charge limit');if(c.resumeRunId&&!/^[a-zA-Z0-9]+$/.test(c.resumeRunId))throw new Error('Invalid resume run ID');if(!c.spreadsheetId||c.spreadsheetId==='REPLACE_WITH_YOUR_GOOGLE_SHEET_ID')throw new Error('Set your Google Sheet ID before starting a billable run');return [{json:c}];"
      }
    },
    {
      "id": "04778ce6-268b-458a-b895-64a1161fb678",
      "name": "Resume saved run",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        700,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ae5d1648-ec62-4e7d-8c31-8f6fbd38def7",
              "leftValue": "={{ Boolean($json.resumeRunId) }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "9d1d5e54-dbcb-4471-acea-0422ccfe252c",
      "name": "Start Apify run",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        920,
        340
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/acts/dig_stays~uk-hospitality-territory-monitor/runs?memory=256&timeout=300&maxTotalChargeUsd={{ $('Configuration').first().json.maxChargeUsd }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 90000
        },
        "method": "POST",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({outputMode:$('Configuration').first().json.outputMode,localAuthorities:$('Configuration').first().json.localAuthorities,postcodeDistricts:$('Configuration').first().json.postcodeDistricts,businessTypeIds:$('Configuration').first().json.businessTypeIds,maxRecords:10000,resetBaseline:false}) }}"
      },
      "retryOnFail": false,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "id": "27fd511e-1e62-4a8e-a595-f13f1aec878d",
      "name": "Use saved run ID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        40
      ],
      "parameters": {
        "jsCode": "return [{json:{data:{id:$('Configuration').first().json.resumeRunId}}}];"
      }
    },
    {
      "id": "42beda68-3f02-4c74-8740-fba300b494d4",
      "name": "Get run status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1150,
        200
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/actor-runs/{{ $json.data.id }}?waitForFinish=60",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 90000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "id": "b6027850-de51-4d69-8944-f083613339aa",
      "name": "Check run status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1370,
        200
      ],
      "parameters": {
        "jsCode": "const r=$json.data;if(!r?.id)throw new Error('Missing run metadata');if(['FAILED','ABORTED','TIMED-OUT'].includes(r.status))throw new Error('Apify '+r.status+'; inspect run '+r.id);if(r.status!=='SUCCEEDED'&&$runIndex>=9)throw new Error('Polling limit reached. Resume run '+r.id+' rather than starting another run.');return [{json:{...$json,complete:r.status==='SUCCEEDED'}}];"
      }
    },
    {
      "id": "fdd5fcdf-01e0-4b03-830e-ea960363840c",
      "name": "Run completed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1590,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ee1edd18-ece0-4253-80e7-ec04c406e6f4",
              "leftValue": "={{ $json.complete }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "3200fa77-ed09-4639-95cd-0a7b722aa025",
      "name": "Wait before polling",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1590,
        430
      ],
      "parameters": {
        "amount": 10,
        "unit": "seconds"
      },
      "webhookId": "05bff94b-53f3-40b1-a2d5-2d2669b067c8"
    },
    {
      "id": "572c4aca-0c27-4971-9e81-0a6f456dc29a",
      "name": "Read scan summary",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1830,
        100
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/key-value-stores/{{ $json.data.defaultKeyValueStoreId }}/records/SUMMARY",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 90000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "id": "d27beb6d-23cf-4e88-b889-0724ed7e2e44",
      "name": "Validate summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2050,
        100
      ],
      "parameters": {
        "jsCode": "const s=$json;if(s.status!=='success'||s.scanCompleted!==true)throw new Error('Source scan did not complete successfully');if(!Number.isInteger(s.emitted)||s.emitted<0||s.emitted>10000)throw new Error('Unexpected result count');return [{json:s}];"
      }
    },
    {
      "id": "ffb5e5d9-5797-48b8-b37f-0a24b2b6cc29",
      "name": "Has records",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2270,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "8e9d400a-4511-445f-977a-6e13b122a6ab",
              "leftValue": "={{ $json.emitted > 0 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "83dba6f2-4c65-4ba1-83ff-00b5b7892a1f",
      "name": "Download run records",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2510,
        -80
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $('Check run status').last().json.data.defaultDatasetId }}/items?format=json&clean=true&limit=10000",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 90000,
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "id": "372fac19-676e-4415-a4e9-cb10ab28d40c",
      "name": "Prepare business rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2730,
        -80
      ],
      "parameters": {
        "jsCode": "const data=$json.body;const expected=$('Validate summary').first().json.emitted;if(!Array.isArray(data)||data.length!==expected)throw new Error('Incomplete dataset; expected '+expected+' rows');const run=$('Check run status').last().json.data;const ids=new Set();return data.map(r=>{if(!r.fhrsId||ids.has(String(r.fhrsId)))throw new Error('Missing or duplicated FSA ID');ids.add(String(r.fhrsId));return {json:{fhrsId:String(r.fhrsId??''),businessName:String(r.businessName??''),businessType:String(r.businessType??''),address:String(r.address??''),postcode:String(r.postcode??''),localAuthority:String(r.localAuthority??''),rating:String(r.rating??''),ratingDate:String(r.ratingDate??''),changeType:String(r.changeType??''),changedFields:JSON.stringify(r.changedFields??[]),observedAt:String(r.observedAt??''),sourceUrl:String(r.sourceUrl??''),attribution:String(r.attribution??''),runId:run.id}};});"
      }
    },
    {
      "id": "17bf3681-78f6-495a-9f23-9149b5231131",
      "name": "Update Businesses sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2970,
        -80
      ],
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Configuration').first().json.spreadsheetId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Businesses"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "fhrsId": "={{ $json.fhrsId }}",
            "businessName": "={{ $json.businessName }}",
            "businessType": "={{ $json.businessType }}",
            "address": "={{ $json.address }}",
            "postcode": "={{ $json.postcode }}",
            "localAuthority": "={{ $json.localAuthority }}",
            "rating": "={{ $json.rating }}",
            "ratingDate": "={{ $json.ratingDate }}",
            "changeType": "={{ $json.changeType }}",
            "changedFields": "={{ $json.changedFields }}",
            "observedAt": "={{ $json.observedAt }}",
            "sourceUrl": "={{ $json.sourceUrl }}",
            "attribution": "={{ $json.attribution }}",
            "runId": "={{ $json.runId }}"
          },
          "matchingColumns": [
            "fhrsId"
          ],
          "schema": [
            {
              "id": "fhrsId",
              "displayName": "fhrsId",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "businessName",
              "displayName": "businessName",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "businessType",
              "displayName": "businessType",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "address",
              "displayName": "address",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "postcode",
              "displayName": "postcode",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "localAuthority",
              "displayName": "localAuthority",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "rating",
              "displayName": "rating",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ratingDate",
              "displayName": "ratingDate",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "changeType",
              "displayName": "changeType",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "changedFields",
              "displayName": "changedFields",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "observedAt",
              "displayName": "observedAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "sourceUrl",
              "displayName": "sourceUrl",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "attribution",
              "displayName": "attribution",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "runId",
              "displayName": "runId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "cellFormat": "RAW"
        }
      }
    },
    {
      "id": "6fc8ce77-de4b-4f9d-b304-9e651745ba03",
      "name": "Prepare scan log",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3210,
        160
      ],
      "parameters": {
        "jsCode": "const s=$('Validate summary').first().json;const r=$('Check run status').last().json.data;return [{json:{runId:r.id,checkedAt:s.observedAt??s.finishedAt,mode:s.mode,status:s.status,scanned:s.scanned,matched:s.matched,returned:s.emitted,noChanges:s.noChanges,previousBaselineAt:s.previousBaselineAt??'',delivery:s.emitted?'Business rows written':'No business rows to write',runUrl:'https://console.apify.com/actors/'+r.actId+'/runs/'+r.id}}];"
      }
    },
    {
      "id": "d18259dd-3fbe-40ec-88f4-e8bce57f0873",
      "name": "Update Runs sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        3450,
        160
      ],
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Configuration').first().json.spreadsheetId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Runs"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "runId": "={{ $json.runId }}",
            "checkedAt": "={{ $json.checkedAt }}",
            "mode": "={{ $json.mode }}",
            "status": "={{ $json.status }}",
            "scanned": "={{ $json.scanned }}",
            "matched": "={{ $json.matched }}",
            "returned": "={{ $json.returned }}",
            "noChanges": "={{ $json.noChanges }}",
            "previousBaselineAt": "={{ $json.previousBaselineAt }}",
            "delivery": "={{ $json.delivery }}",
            "runUrl": "={{ $json.runUrl }}"
          },
          "matchingColumns": [
            "runId"
          ],
          "schema": [
            {
              "id": "runId",
              "displayName": "runId",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "checkedAt",
              "displayName": "checkedAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "mode",
              "displayName": "mode",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "scanned",
              "displayName": "scanned",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "matched",
              "displayName": "matched",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "returned",
              "displayName": "returned",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "noChanges",
              "displayName": "noChanges",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "previousBaselineAt",
              "displayName": "previousBaselineAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "delivery",
              "displayName": "delivery",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "runUrl",
              "displayName": "runUrl",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "cellFormat": "RAW"
        }
      }
    },
    {
      "id": "91b06247-e427-497f-bda3-45e2977269a0",
      "name": "Read before running",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        100,
        -430
      ],
      "parameters": {
        "content": "## UK restaurant territory → Google Sheets\n1. Create Businesses and Runs tabs with the headers in the setup guide.\n2. Edit Configuration.\n3. Select one Apify Header Auth credential on all HTTP nodes (Authorization: Bearer YOUR_TOKEN).\n4. Select Google Sheets credentials on both Sheets nodes.\n5. Test manually, then publish to enable Monday 09:00 Europe/London.\n\nIf a Sheets step fails: do not start another Actor run. Set resumeRunId to the completed run ID, replay it, then clear resumeRunId. Upserts make replay safe. Pause the schedule until repaired.\n\nBaseline rows are existing businesses; newly seen is not a confirmed opening. No email/phone data. No automatic removals. Run serially with one monitor per scope.",
        "height": 520,
        "width": 680
      }
    }
  ],
  "connections": {
    "Manual start": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Monday 9am": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Validate configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate configuration": {
      "main": [
        [
          {
            "node": "Resume saved run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resume saved run": {
      "main": [
        [
          {
            "node": "Use saved run ID",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Start Apify run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use saved run ID": {
      "main": [
        [
          {
            "node": "Get run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Apify run": {
      "main": [
        [
          {
            "node": "Get run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get run status": {
      "main": [
        [
          {
            "node": "Check run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check run status": {
      "main": [
        [
          {
            "node": "Run completed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run completed": {
      "main": [
        [
          {
            "node": "Read scan summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait before polling",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait before polling": {
      "main": [
        [
          {
            "node": "Get run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read scan summary": {
      "main": [
        [
          {
            "node": "Validate summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate summary": {
      "main": [
        [
          {
            "node": "Has records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has records": {
      "main": [
        [
          {
            "node": "Download run records",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare scan log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download run records": {
      "main": [
        [
          {
            "node": "Prepare business rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare business rows": {
      "main": [
        [
          {
            "node": "Update Businesses sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Businesses sheet": {
      "main": [
        [
          {
            "node": "Prepare scan log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare scan log": {
      "main": [
        [
          {
            "node": "Update Runs sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/London",
    "executionTimeout": 1200,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "pinData": {},
  "tags": []
}