HubSpot Native Workflows: Geographic Routing & Lead Stage Automation

This playbook details the technical configuration, step-by-step UI setups, and custom-coded Node.js workflow actions required to orchestrate geographic custom object routing and lead generation natively inside HubSpot.


🗺️ 1. Geographic Zip-to-Franchise Association Workflow

Because The Dog Wizard maps incoming clients (Contacts) to franchise locations (Companies) using custom objects (Zip Codes and Territories), we utilize a HubSpot Custom Code Action (requires Operations Hub Pro) inside a Contact-based workflow to perform high-speed database traversals and auto-association.

⚙️ UI Setup Specifications

  1. Workflow Type: Contact-based (re-enrollment enabled on Postal Code change).
  2. Enrollment Trigger: Contact property Postal Code/Zip Code is known.
  3. Action 1: Add a “Custom Code” action.
    • Language: Node.js 18.x
    • Secrets: Add HUBSPOT_ACCESS_TOKEN as a workflow secret.
    • Property Inputs: Select Postal code and map it to input variable zipCode.
  4. Action 2: Once associated, send a Slack or internal notification to the Front Office team if the resolved company has Managed by Front Office = True.

💻 Custom Code Script: Zip Code Traversal

Copy and paste this script directly into the HubSpot Custom Code block:

const axios = require('axios');
 
exports.main = async (event, callback) => {
  const zipCode = event.inputFields['zipCode'];
  const contactId = event.object.objectId;
  
  if (!zipCode) {
    console.log("No zip code found on enrolling contact record.");
    return callback();
  }
 
  // Replace these with your actual HubSpot Custom Object Type IDs
  const ZIP_CODE_OBJECT_TYPE_ID = "2-10543932"; // Example: Replace with your actual schema ID
  const TERRITORY_OBJECT_TYPE_ID = "2-10543933"; // Example: Replace with your actual schema ID
 
  const token = process.env.HUBSPOT_ACCESS_TOKEN;
  const client = axios.create({
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
 
  try {
    console.log(`Step 1: Searching for Custom Zip Code Object record matching '${zipCode}'...`);
    const zipSearchRes = await client.post(`https://api.hubapi.com/crm/v3/objects/${ZIP_CODE_OBJECT_TYPE_ID}/search`, {
      filterGroups: [{
        filters: [{
          propertyName: 'zip_code',
          operator: 'EQ',
          value: zipCode
        }]
      }],
      limit: 1
    });
 
    if (!zipSearchRes.data.results || zipSearchRes.data.results.length === 0) {
      console.log(`❌ No Zip Code custom object found matching '${zipCode}'. Ending workflow.`);
      return callback();
    }
 
    const matchedZipRecordId = zipSearchRes.data.results[0].id;
    console.log(`✅ Found custom Zip Code record ID: ${matchedZipRecordId}`);
 
    console.log(`Step 2: Retrieving associated Territory record...`);
    const territoryAssocRes = await client.get(`https://api.hubapi.com/crm/v4/objects/${ZIP_CODE_OBJECT_TYPE_ID}/${matchedZipRecordId}/associations/${TERRITORY_OBJECT_TYPE_ID}`);
    
    if (!territoryAssocRes.data.results || territoryAssocRes.data.results.length === 0) {
      console.log("❌ Zip Code record has no associated Territory custom object. Ending workflow.");
      return callback();
    }
 
    const matchedTerritoryId = territoryAssocRes.data.results[0].toObjectId;
    console.log(`✅ Found custom Territory record ID: ${matchedTerritoryId}`);
 
    console.log(`Step 3: Retrieving associated Franchise Company record...`);
    const companyAssocRes = await client.get(`https://api.hubapi.com/crm/v4/objects/${TERRITORY_OBJECT_TYPE_ID}/${matchedTerritoryId}/associations/companies`);
 
    if (!companyAssocRes.data.results || companyAssocRes.data.results.length === 0) {
      console.log("❌ Territory record has no associated Company. Ending workflow.");
      return callback();
    }
 
    const resolvedCompanyId = companyAssocRes.data.results[0].toObjectId;
    console.log(`✅ Resolved Franchise Company record ID: ${resolvedCompanyId}`);
 
    console.log(`Step 4: Creating Contact-to-Company association...`);
    await client.put(`https://api.hubapi.com/crm/v4/objects/contacts/${contactId}/associations/companies/${resolvedCompanyId}`, [
      {
        associationCategory: "HUBSPOT_DEFINED",
        associationTypeId: 1 // Standard Contact to Company Link
      }
    ]);
 
    console.log(`🎉 Success! Contact ${contactId} has been successfully associated with Company ${resolvedCompanyId} geographically.`);
    
    callback({
      outputFields: {
        associatedCompanyId: resolvedCompanyId
      }
    });
  } catch (err) {
    console.error("❌ Error running custom zip-code routing action:", err.message);
    if (err.response) console.error("HubSpot Response Error:", err.response.data);
    callback(err);
  }
};

🧭 2. Lead Object Generation (Sales Hub Pro)

This Contact-based workflow automatically provisions standard Sales Hub Pro Lead Records as soon as a high-priority contact lands from PocketSuite.

⚙️ UI Setup Specifications

  1. Workflow Type: Contact-based.
  2. Enrollment Trigger: Contact property pocket_suite_client_id is known.
  3. Action 1: “Create Lead” (native HubSpot Sales Hub Pro action).
    • Pipeline: Sales Pipeline / Prospecting Workspace.
    • Initial Stage: New.
    • Name: Lead - [Contact First Name] [Contact Last Name].
    • Owner: Rotated among Front Office team members.
    • Associations: Check both “Associate with Contact” and “Associate with Company” (this links the Lead to the geographic franchise Company resolved in the step above).

🔄 3. Lead Stage Automation (Activity-Driven)

This workflow keeps the Leads pipeline up-to-date programmatically based on the actions logged onto the Contact timeline by our Quo (OpenPhone) SMS/call system.

⚙️ UI Setup Specifications

Create a single Contact-based workflow with branching logic:

graph TD
    Start[Contact Activity Change] --> CheckActivity{Which Activity Logged?}
    
    CheckActivity -->|SMS Logged OR ps_outreach_sent = true| Attempting[Set Lead Stage = "Attempting"]
    CheckActivity -->|Call Logged OR ps_connection_established = true| Connected[Set Lead Stage = "Connected"]
    CheckActivity -->|Evaluation Booking Registered| Qualified[Set Lead Stage = "Qualified"]
  1. Enrollment Trigger:
    • Activity: SMS message is created on Contact timeline OR ps_outreach_sent = True.
    • OR Activity: Call is logged on Contact timeline OR ps_connection_established = True.
    • OR Contact Property pocket_suite_lead_source is updated to a booking status.
  2. Branching Logic:
    • Branch A: Attempting Stage
      • Trigger: SMS activity is logged OR ps_outreach_sent is true.
      • Action: Use “Update Lead Stage” native action ➡️ Set to Attempting.
    • Branch B: Connected Stage
      • Trigger: Call activity is logged OR ps_connection_established is true.
      • Action: Use “Update Lead Stage” native action ➡️ Set to Connected.
    • Branch C: Qualified Stage
      • Trigger: Evaluation booked in PocketSuite (mapped to the Contact record).
      • Action: Use “Update Lead Stage” native action ➡️ Set to Qualified.

🏆 Architectural Gains

By using native HubSpot Workflows coupled with custom-coded Node.js traversal actions, we keep data pipelines fully automated, real-time, and zero-cost, preventing external middleware breakage.