n8n workflow: Generate Leads with Google Maps - AlexK1919

Nodes:
name: When clicking "Execute Workflow"
type: n8n-nodes-base.manualTrigger
name: Run workflow every hours
type: n8n-nodes-base.scheduleTrigger
disabled: yes
parameters: rule: interval: field: minutes
minutesInterval: 15
name: Execute Workflow Trigger
type: n8n-nodes-base.executeWorkflowTrigger
disabled: yes
name: Check Max Retries1
type: n8n-nodes-base.if
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: number
operation: gt
leftValue: ={{ $('Exponential Backoff').item.json["retryCount"] }}
rightValue: 10
name: Stop and Error1
type: n8n-nodes-base.stopAndError
parameters: errorMessage: Google Sheets API Limit has been triggered and the workflow has stopped
name: GMaps API
type: n8n-nodes-base.httpRequest
parameters: url: https://places.googleapis.com/v1/places:searchText
method: POST
options: response: response: fullResponse: yes
sendBody: yes
sendHeaders: yes
authentication: predefinedCredentialType
bodyParameters: parameters: name: textQuery
value: ={{ $('Subcategory').item.json.Subcategory }} {{ $json.zip }}
headerParameters: parameters: name: X-Goog-FieldMask
value: places.id,places.displayName,places.addressComponents,places.formattedAddress,places.primaryType,places.primaryTypeDisplayName,places.types,places.location,places.nationalPhoneNumber,places.rating,places.userRatingCount,places.websiteUri,places.editorialSummary,places.reviews,places.attributions,places.userRatingCount
nodeCredentialType: googleOAuth2Api
name: Update Status to Success
type: n8n-nodes-base.googleSheets
onError: continueErrorOutput
parameters: columns: value: zip: ={{ $('Set Zip').first().json.zip }}
status: scraped
subcat: ={{ $('Subcategory').first().json.Subcategory }}
schema: type: string
display: yes
removed: no
required: no
displayName: zip
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
removed: no
required: no
displayName: status
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
removed: no
required: no
displayName: subcat
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
removed: no
readOnly: yes
required: no
displayName: row_number
defaultMatch: no
canBeUsedToMatch: yes
mappingMode: defineBelow
matchingColumns: zip
operation: update
sheetName: __rl: yes
mode: name
value: ={{ $('Settings').first().json.sheet }}
documentId: __rl: yes
mode: url
value: ={{ $('Settings').first().json.gs_url }}
executeOnce: yes
alwaysOutputData: yes
name: Add rows in Google Sheets
type: n8n-nodes-base.googleSheets
onError: continueErrorOutput
parameters: columns: value: type: ={{ $('Subcategory').item.json.Subcategory }}
phone: ={{ $json.place.nationalPhoneNumber }}
title: ={{ $json.place.displayName.text }}
types: ={{ $json.place.types }}
rating: ={{ $json.place.rating }}
address: ={{ $json.place.formattedAddress }}
reviews: ={{ $json.place.reviews }}
website: ={{ $json.place.websiteUri }}
place_id: ={{ $json.place.id }}
gps_coordinates: ={"latitude":{{ $json.place.location.latitude }},"longitude":{{ $json.place.location.longitude }}}
schema: type: string
display: yes
required: no
displayName: ACTION
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: STATUS
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: title
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: email
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: name
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: firstname
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: lastname
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: phone
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: clean url
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: website
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: WP API
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: WP
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: facebook
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: instagram
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: youtube
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: tiktok
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: twitter
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: linkedin
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: pinterest
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: reddit
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: rating
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: reviews
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: type
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: address
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: price
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
removed: no
required: no
displayName: place_id
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: position
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: data_id
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: data_cid
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: reviews_link
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: photos_link
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: gps_coordinates
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: place_id_search
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: provider_id
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: types
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: open_state
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: hours
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: operating_hours
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: description
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: service_options
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: order_online
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: thumbnail
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: editorial_reviews
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: unclaimed_listing
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: reserve_a_table
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: user_review
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: amenities
defaultMatch: no
canBeUsedToMatch: yes
type: string
display: yes
required: no
displayName: book_online
defaultMatch: no
canBeUsedToMatch: yes
mappingMode: defineBelow
matchingColumns: place_id
operation: appendOrUpdate
sheetName: __rl: yes
mode: name
value: =Results
documentId: __rl: yes
mode: url
value: ={{ $('Settings').item.json.gs_url }}
alwaysOutputData: yes
name: Set Row Number
type: n8n-nodes-base.set
parameters: assignments: assignments: name: row_number
type: number
value: ={{ $json.row_number }}
includeOtherFields: yes
name: Set Zip
type: n8n-nodes-base.set
parameters: assignments: assignments: name: zip
type: number
value: ={{ $('Loop Zips').first().json.zip }}
includeOtherFields: yes
name: Set Place ID
type: n8n-nodes-base.set
parameters: assignments: assignments: name: places.id
type: string
value: ={{ $json.place.id }}
includeOtherFields: yes
name: Exponential Backoff
type: n8n-nodes-base.code
parameters: mode: runOnceForEachItem
jsCode: // Define the retry count (coming from a previous node or set manually)
const retryCount = $json["retryCount"] || 0;  // If not present, default to 0
const maxRetries = 5;  // Define the maximum number of retries
const initialDelay = 1;  // Initial delay in seconds (1 second)

// If the retry count is less than the max retries, calculate the delay
if (retryCount < maxRetries) {
    const currentDelayInSeconds = initialDelay * Math.pow(2, retryCount);  // Exponential backoff delay in seconds
    
    // Log the delay time for debugging
    console.log(`Waiting for ${currentDelayInSeconds} seconds before retry...`);
    
    return {
        json: {
            retryCount: retryCount + 1,  // Increment retry count
            waitTimeInSeconds: currentDelayInSeconds, // Pass the delay time in seconds
            status: 'retrying',
        }
    };
} else {
    // If max retries are exceeded, return a failure response
    return {
        json: {
            error: 'Max retries exceeded',
            retryCount: retryCount,
            status: 'failed'
        }
    };
}

name: Remove Duplicates
type: n8n-nodes-base.removeDuplicates
parameters: compare: selectedFields
fieldsToCompare: place.id,places.id
name: Wait
type: n8n-nodes-base.wait
parameters: amount: ={{ $json["waitTime"] }}
name: Check Max Retries
type: n8n-nodes-base.if
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: number
operation: gt
leftValue: ={{ $('Exponential Backoff1').item.json["retryCount"] }}
rightValue: 10
name: Stop and Error
type: n8n-nodes-base.stopAndError
parameters: errorMessage: Google Sheets API Limit has been triggered and the workflow has stopped
name: Exponential Backoff1
type: n8n-nodes-base.code
parameters: mode: runOnceForEachItem
jsCode: // Define the retry count (coming from a previous node or set manually)
const retryCount = $json["retryCount"] || 0;  // If not present, default to 0
const maxRetries = 5;  // Define the maximum number of retries
const initialDelay = 1;  // Initial delay in seconds (1 second)

// If the retry count is less than the max retries, calculate the delay
if (retryCount < maxRetries) {
    const currentDelayInSeconds = initialDelay * Math.pow(2, retryCount);  // Exponential backoff delay in seconds
    
    // Log the delay time for debugging
    console.log(`Waiting for ${currentDelayInSeconds} seconds before retry...`);
    
    return {
        json: {
            retryCount: retryCount + 1,  // Increment retry count
            waitTimeInSeconds: currentDelayInSeconds, // Pass the delay time in seconds
            status: 'retrying',
        }
    };
} else {
    // If max retries are exceeded, return a failure response
    return {
        json: {
            error: 'Max retries exceeded',
            retryCount: retryCount,
            status: 'failed'
        }
    };
}

name: Wait1
type: n8n-nodes-base.wait
parameters: amount: ={{ $json["waitTime"] }}
name: Settings
type: n8n-nodes-base.set
parameters: assignments: assignments: name: gs_url
type: string
value: https://docs.google.com/spreadsheets/d/11bzNIQDE4GB2NWkyGv2IfEsTggSIBCKe2z39Ecp2Z4g/edit?gid=853393043#gid=853393043
name: catSheet
type: string
value: Google Maps Categories
name: sheet
type: string
value: AZ Zips
name: GS - Get Subcategory
type: n8n-nodes-base.googleSheets
parameters: sheetName: __rl: yes
mode: name
value: ={{ $('Settings').item.json.catSheet }}
documentId: __rl: yes
mode: url
value: ={{ $('Settings').item.json.gs_url }}
executeOnce: yes
name: Subcategory
type: n8n-nodes-base.set
parameters: assignments: assignments: name: Subcategory
type: string
value: ={{ $json.Subcategory }}
includeOtherFields: yes
name: GS - Get Zip Codes
type: n8n-nodes-base.googleSheets
parameters: sheetName: __rl: yes
mode: name
value: ={{ $('Settings').item.json.sheet }}
documentId: __rl: yes
mode: url
value: ={{ $('Settings').item.json.gs_url }}
executeOnce: yes
name: Place Array
type: n8n-nodes-base.code
parameters: jsCode: // Get the places array from the input data
const places = items[0]?.json?.body?.places || [];

// Create an output array to hold each place as a separate item
let output = [];

if (places.length > 0) {
  for (let i = 0; i < places.length; i++) {
    // For each place, push a new item into the output array
    output.push({
      json: {
        place: places[i], // The individual place object
        otherData: items[0].json.otherData || null  // Include other data or default to null
      }
    });
  }
} else {
  // Log an error or handle the case where places array is empty or undefined
  console.log('Places array is empty or undefined.');
}

// Return the output array, so each place becomes its own item
return output;

name: Loop Zips
type: n8n-nodes-base.splitInBatches
name: Loop Subcats
type: n8n-nodes-base.splitInBatches
name: GS - Get Status
type: n8n-nodes-base.googleSheets
onError: continueErrorOutput
parameters: sheetName: __rl: yes
mode: name
value: ={{ $('Settings').first().json.sheet }}
documentId: __rl: yes
mode: url
value: ={{ $('Settings').first().json.gs_url }}
executeOnce: yes
name: Check Max Retries2
type: n8n-nodes-base.if
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: number
operation: gt
leftValue: ={{ $('Exponential Backoff2').item.json["retryCount"] }}
rightValue: 10
name: Stop and Error2
type: n8n-nodes-base.stopAndError
parameters: errorMessage: Google Sheets API Limit has been triggered and the workflow has stopped
name: Exponential Backoff2
type: n8n-nodes-base.code
parameters: mode: runOnceForEachItem
jsCode: // Define the retry count (coming from a previous node or set manually)
const retryCount = $json["retryCount"] || 0;  // If not present, default to 0
const maxRetries = 5;  // Define the maximum number of retries
const initialDelay = 1;  // Initial delay in seconds (1 second)

// If the retry count is less than the max retries, calculate the delay
if (retryCount < maxRetries) {
    const currentDelayInSeconds = initialDelay * Math.pow(2, retryCount);  // Exponential backoff delay in seconds
    
    // Log the delay time for debugging
    console.log(`Waiting for ${currentDelayInSeconds} seconds before retry...`);
    
    return {
        json: {
            retryCount: retryCount + 1,  // Increment retry count
            waitTimeInSeconds: currentDelayInSeconds, // Pass the delay time in seconds
            status: 'retrying',
        }
    };
} else {
    // If max retries are exceeded, return a failure response
    return {
        json: {
            error: 'Max retries exceeded',
            retryCount: retryCount,
            status: 'failed'
        }
    };
}

name: Wait2
type: n8n-nodes-base.wait
parameters: amount: ={{ $json["waitTime"] }}
name: Limit
type: n8n-nodes-base.limit
parameters: maxItems: 3
name: Sticky Note20
type: n8n-nodes-base.stickyNote
parameters: color: 6
width: 250
height: 1390
content: # AlexK1919 
![Alex Kim](https://media.licdn.com/dms/image/v2/D5603AQFOYMkqCPl6Sw/profile-displayphoto-shrink_400_400/profile-displayphoto-shrink_400_400/0/1718309808352?e=1736985600&v=beta&t=pQKm7lQfUU1ytuC2Gq1PRxNY-XmROFWbo-BjzUPxWOs)

#### I’m Alex Kim, an AI-Native Workflow Automation Architect Building Solutions to Optimize your Personal and Professional Life.


### About Me
https://beacons.ai/alexk1919


### Products Used 
[Google Maps API via Google Cloud Account](https://docs.n8n.io/integrations/builtin/credentials/google/oauth-generic/?utm_source=n8n_app&utm_medium=credential_settings&utm_campaign=create_new_credentials_modal)

name: Sticky Note8
type: n8n-nodes-base.stickyNote
parameters: color: 3
width: 247
height: 1391
content: # Settings
name: Sticky Note15
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 219
height: 220
content: In the Google Sheets document, set the subcategories you'd like to search for

Set the URL of your Google Sheets document

Set the Zip Code sheet name
name: Sticky Note
type: n8n-nodes-base.stickyNote
parameters: color: 7
width: 247
height: 1391
content: # Triggers
name: Zips
type: n8n-nodes-base.set
parameters: assignments: assignments: name: zip
type: number
value: ={{ $json.zip }}
includeOtherFields: yes
name: Sticky Note1
type: n8n-nodes-base.stickyNote
parameters: color: 5
width: 3060
height: 880
content: # Google Maps API
name: Filter Zips
type: n8n-nodes-base.filter
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: string
operation: empty
singleValue: yes
leftValue: ={{ $json.status }}
name: Filter Subcategories
type: n8n-nodes-base.filter
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: string
operation: notEquals
leftValue: ={{ $json.STATUS }}
rightValue: Ignore
name: If Empty
type: n8n-nodes-base.if
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: object
operation: empty
singleValue: yes
leftValue: ={{ $json.body }}
name: Split Out
type: n8n-nodes-base.splitOut
parameters: include: allOtherFields
fieldToSplitOut: row_number
name: Sticky Note2
type: n8n-nodes-base.stickyNote
parameters: color: 4
width: 3060
height: 500
content: # Data Prep

Connections:
Wait: main: node: Check Max Retries1
type: main
Zips: main: node: Filter Zips
type: main
Limit: main: node: Loop Zips
type: main
Wait1: main: node: Check Max Retries
type: main
Wait2: main: node: Check Max Retries2
type: main
Set Zip: main: node: GMaps API
type: main
If Empty: main: node: Loop Subcats
type: main
node: Place Array
type: main
Settings: main: node: GS - Get Zip Codes
type: main
GMaps API: main: node: If Empty
type: main
Loop Zips: main: node: GS - Get Subcategory
type: main
Split Out: main: node: Limit
type: main
Filter Zips: main: node: Set Row Number
type: main
Place Array: main: node: Set Place ID
type: main
Subcategory: main: node: Loop Subcats
type: main
Loop Subcats: main: node: Loop Zips
type: main
node: Set Zip
type: main
Set Place ID: main: node: Remove Duplicates
type: main
Set Row Number: main: node: Split Out
type: main
GS - Get Status: main: node: Update Status to Success
type: main
node: Exponential Backoff2
type: main
Check Max Retries: main: node: Stop and Error
type: main
node: Update Status to Success
type: main
Remove Duplicates: main: node: Add rows in Google Sheets
type: main
Check Max Retries1: main: node: Stop and Error1
type: main
node: Add rows in Google Sheets
type: main
Check Max Retries2: main: node: Stop and Error2
type: main
GS - Get Zip Codes: main: node: Zips
type: main
Exponential Backoff: main: node: Wait
type: main
Exponential Backoff1: main: node: Wait1
type: main
Exponential Backoff2: main: node: Wait2
type: main
Filter Subcategories: main: node: Subcategory
type: main
GS - Get Subcategory: main: node: Filter Subcategories
type: main
Execute Workflow Trigger: main: node: Settings
type: main
Run workflow every hours: main: node: Settings
type: main
Update Status to Success: main: node: Loop Subcats
type: main
node: Exponential Backoff1
type: main
Add rows in Google Sheets: main: node: GS - Get Status
type: main
node: Exponential Backoff
type: main
When clicking "Execute Workflow": main: node: Settings
type: main