How to Build a Complete RevOps CRM System with n8n Automation (Free Template)

How to Build a Complete RevOps CRM System with n8n Automation (Free Template)

Revenue operations teams waste 15-20 hours weekly on manual data entry, lead routing, and status updates. A properly architected CRM with n8n automation eliminates this friction entirely. This guide teaches you how to build a complete RevOps system that handles the full lead lifecycle—from first touch to renewal—with automated enrichment, routing, and notifications. You'll get the exact n8n workflow template at the end.

The Problem: Manual Revenue Operations Kill Growth

Current challenges:

  • Sales reps spend 4+ hours daily updating CRM records instead of selling
  • Leads fall through cracks between lifecycle stages (inbound → demo → trial)
  • Marketing can't track which campaigns actually drive revenue
  • Customer success has no visibility into renewal risk signals
  • RevOps teams manually export data between tools for reporting

Business impact:

  • Time spent: 15-20 hours per week per team member on data hygiene
  • Revenue leakage: 23% of qualified leads never get followed up due to routing failures
  • Reporting delays: 3-5 days to compile cross-functional revenue metrics
  • Tool sprawl: 8-12 disconnected systems with duplicate data entry

Without automation, your CRM becomes a data graveyard instead of a revenue engine.

The Solution Overview

This n8n-powered RevOps system automates the complete lead lifecycle across your CRM. The workflow handles inbound lead capture, automatic enrichment with firmographic data, intelligent routing based on territory and deal size, lifecycle stage progression triggers, and cross-functional notifications to sales, marketing, and customer success teams. Built on n8n's webhook and HTTP request nodes, this system connects your CRM to enrichment APIs, communication tools, and analytics platforms—creating a single source of truth that updates in real-time without manual intervention.

What You'll Build

Component Technology Purpose
Lead Capture Webhook Trigger Receive inbound leads from forms, ads, and integrations
Data Enrichment Clearbit/Apollo API Auto-populate company size, industry, revenue, tech stack
CRM Integration HubSpot/Salesforce API Create/update contacts, deals, lifecycle stages
Lead Routing n8n Switch & Function Nodes Assign leads based on territory, deal size, product fit
Lifecycle Automation HTTP Request Nodes Progress leads through stages with trigger-based workflows
Notifications Slack/Email Nodes Alert teams on stage changes, high-value leads, renewal risks
Reporting Sync PostgreSQL/Airtable Push data to analytics layer for dashboards

Key capabilities:

  • Automatic lead enrichment within 30 seconds of form submission
  • Territory-based routing using company location and deal size logic
  • Lifecycle stage progression (inbound → qualified → demo → trial → customer → renewal)
  • Real-time Slack alerts for high-intent signals (demo requests, trial starts, usage spikes)
  • Bi-directional sync between CRM and data warehouse for reporting
  • Automated task creation for sales reps at each lifecycle milestone
  • Renewal risk scoring based on product usage and billing signals

Prerequisites

Before starting, ensure you have:

  • n8n instance (cloud or self-hosted version 1.0+)
  • HubSpot or Salesforce account with API access (Professional tier minimum)
  • Clearbit or Apollo API key for lead enrichment
  • Slack workspace with webhook permissions (or email SMTP credentials)
  • PostgreSQL or Airtable account for reporting layer (optional but recommended)
  • Basic JavaScript knowledge for Function nodes and data transformation
  • Understanding of CRM object models (contacts, companies, deals, lifecycle stages)

Step 1: Configure Webhook Lead Capture

The workflow starts when a lead enters your system from any source—website forms, paid ads, webinar signups, or third-party integrations.

Set up the Webhook Trigger node:

  1. Add a Webhook node as your workflow trigger
  2. Set HTTP Method to POST
  3. Set Path to /lead-capture (or your preferred endpoint)
  4. Enable "Respond Immediately" to prevent form submission delays
  5. Set Response Code to 200 and Response Body to {"status": "received"}

Node configuration:

{
  "httpMethod": "POST",
  "path": "lead-capture",
  "responseMode": "onReceived",
  "responseCode": 200,
  "responseData": "{\"status\": \"received\"}"
}

Why this works:
The webhook creates a universal endpoint that accepts lead data from any source. Responding immediately (before enrichment runs) prevents form timeout errors. Your forms submit successfully while n8n processes enrichment and routing in the background.

Expected webhook payload structure:

{
  "email": "contact@company.com",
  "firstName": "Jane",
  "lastName": "Smith",
  "company": "Acme Corp",
  "phone": "+1-555-0123",
  "source": "website_form"
}

Step 2: Enrich Lead Data with External APIs

Raw form submissions lack the firmographic data needed for routing and qualification. Enrichment APIs fill these gaps automatically.

Configure the HTTP Request node for Clearbit:

  1. Add HTTP Request node after webhook
  2. Set Method to GET
  3. URL: https://company.clearbit.com/v2/companies/find?domain={{$json.email.split('@')[1]}}
  4. Authentication: Header Auth with Authorization: Bearer YOUR_API_KEY
  5. Add error handling: Continue on Fail = true

Node configuration:

{
  "method": "GET",
  "url": "=https://company.clearbit.com/v2/companies/find?domain={{$json.email.split('@')[1]}}",
  "authentication": "headerAuth",
  "headerAuth": {
    "name": "Authorization",
    "value": "Bearer {{$credentials.clearbitApi.apiKey}}"
  },
  "options": {
    "timeout": 10000,
    "redirect": {
      "followRedirects": true
    }
  }
}

Add a Function node to structure enriched data:

const lead = $input.first().json;
const enrichment = $input.last().json;

return {
  email: lead.email,
  firstName: lead.firstName,
  lastName: lead.lastName,
  company: lead.company,
  phone: lead.phone,
  source: lead.source,
  // Enriched fields
  companySize: enrichment.metrics?.employees || 'Unknown',
  industry: enrichment.category?.industry || 'Unknown',
  revenue: enrichment.metrics?.estimatedAnnualRevenue || 0,
  techStack: enrichment.tech || [],
  location: enrichment.geo?.city + ', ' + enrichment.geo?.state
};

Why this approach:
Extracting the domain from email (split('@')[1]) automatically finds the company without requiring a separate field. The Function node normalizes the API response into a clean structure your CRM expects. Setting continueOnFail: true ensures the workflow doesn't break if enrichment fails—you still create the lead with available data.

Step 3: Implement Intelligent Lead Routing

Routing logic determines which sales rep receives each lead based on territory, company size, and product fit.

Add a Switch node with routing rules:

{
  "mode": "rules",
  "rules": [
    {
      "conditions": [
        {
          "field": "companySize",
          "operation": "largerEqual",
          "value": 500
        },
        {
          "field": "revenue",
          "operation": "largerEqual",
          "value": 10000000
        }
      ],
      "output": "enterprise"
    },
    {
      "conditions": [
        {
          "field": "companySize",
          "operation": "between",
          "value": [50, 499]
        }
      ],
      "output": "mid-market"
    },
    {
      "conditions": [
        {
          "field": "companySize",
          "operation": "smaller",
          "value": 50
        }
      ],
      "output": "smb"
    }
  ],
  "fallbackOutput": "unqualified"
}

Create territory assignment Function node:

const lead = $input.first().json;
const segment = $input.first().json.segment; // from Switch node

// Territory mapping
const territories = {
  'enterprise': {
    'West': ['CA', 'OR', 'WA', 'NV', 'AZ'],
    'Central': ['TX', 'CO', 'IL', 'MN'],
    'East': ['NY', 'MA', 'FL', 'GA', 'NC']
  },
  'mid-market': {
    'West': 'rep-west-mm@company.com',
    'Central': 'rep-central-mm@company.com',
    'East': 'rep-east-mm@company.com'
  },
  'smb': 'round-robin' // Use round-robin for SMB
};

// Extract state from location
const state = lead.location.split(', ')[1];

// Determine territory
let territory = 'East'; // default
if (territories.enterprise.West.includes(state)) territory = 'West';
if (territories.enterprise.Central.includes(state)) territory = 'Central';

// Assign owner
let owner;
if (segment === 'smb') {
  // Implement round-robin logic here
  owner = 'smb-team@company.com';
} else {
  owner = territories[segment][territory];
}

return {
  ...lead,
  segment: segment,
  territory: territory,
  assignedTo: owner
};

Why this logic works:
The Switch node segments leads before routing, preventing enterprise deals from going to SMB reps. Territory assignment uses state-level geography to distribute workload regionally. Round-robin for SMB ensures even distribution when you have multiple reps. This three-tier approach (segment → territory → rep) scales from 3 reps to 50+ without workflow changes.

Step 4: Create and Update CRM Records

With enriched and routed data, create or update CRM records using the appropriate lifecycle stage.

Configure HubSpot HTTP Request node:

{
  "method": "POST",
  "url": "https://api.hubapi.com/crm/v3/objects/contacts",
  "authentication": "headerAuth",
  "headerAuth": {
    "name": "Authorization",
    "value": "Bearer {{$credentials.hubspot.accessToken}}"
  },
  "bodyParameters": {
    "properties": {
      "email": "={{$json.email}}",
      "firstname": "={{$json.firstName}}",
      "lastname": "={{$json.lastName}}",
      "company": "={{$json.company}}",
      "phone": "={{$json.phone}}",
      "hs_lead_status": "NEW",
      "lifecyclestage": "lead",
      "company_size": "={{$json.companySize}}",
      "industry": "={{$json.industry}}",
      "annual_revenue": "={{$json.revenue}}",
      "lead_source": "={{$json.source}}",
      "hubspot_owner_id": "={{$json.ownerId}}"
    }
  },
  "options": {
    "response": {
      "response": {
        "fullResponse": false,
        "neverError": true
      }
    }
  }
}

Add error handling for duplicate contacts:

Create an IF node that checks the HTTP status code. If 409 (conflict/duplicate), route to an UPDATE operation instead:

{
  "method": "PATCH",
  "url": "https://api.hubapi.com/crm/v3/objects/contacts/{{$json.existingContactId}}",
  "bodyParameters": {
    "properties": {
      "company_size": "={{$json.companySize}}",
      "industry": "={{$json.industry}}",
      "annual_revenue": "={{$json.revenue}}",
      "hs_lead_status": "UPDATED"
    }
  }
}

Why this approach:
Creating contacts with lifecyclestage: lead ensures they enter your funnel at the correct stage. Setting neverError: true prevents the workflow from failing on duplicates—instead, you catch the 409 and update existing records. This idempotent design means you can replay the workflow without creating duplicate CRM entries.

Step 5: Automate Lifecycle Stage Progression

Lifecycle automation moves contacts through stages based on behavioral triggers and sales actions.

Create lifecycle trigger workflows:

Trigger Event Lifecycle Stage Automation Action
Demo scheduled SQL (Sales Qualified Lead) Create deal, assign to AE, send prep email
Trial started Opportunity Update deal stage, notify CSM, start onboarding sequence
Contract signed Customer Create renewal deal, provision account, send welcome email
60 days to renewal Renewal Score health, assign CSM task, trigger outreach sequence

Configure demo scheduled webhook:

{
  "httpMethod": "POST",
  "path": "lifecycle/demo-scheduled",
  "responseMode": "onReceived"
}

Add Function node to update lifecycle stage:

const contactId = $json.contactId;
const demoDate = $json.scheduledDate;

// Update contact lifecycle stage
const updatePayload = {
  contactId: contactId,
  properties: {
    lifecyclestage: 'salesqualifiedlead',
    demo_scheduled_date: demoDate,
    hs_lead_status: 'IN_PROGRESS'
  }
};

// Create associated deal
const dealPayload = {
  properties: {
    dealname: $json.companyName + ' - New Business',
    dealstage: 'qualifiedtobuy',
    amount: $json.estimatedValue || 0,
    closedate: new Date(Date.now() + 30*24*60*60*1000).toISOString(), // 30 days out
    pipeline: 'default'
  },
  associations: [
    {
      to: { id: contactId },
      types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3 }]
    }
  ]
};

return [updatePayload, dealPayload];

Why this works:
Webhook-based lifecycle triggers respond to real events (demo booked in Calendly, trial started in your product) rather than time delays. Creating deals automatically when contacts reach SQL stage ensures pipeline accuracy. The 30-day close date estimate gives reps a target while allowing manual adjustment.

Step 6: Configure Cross-Functional Notifications

Different teams need different alerts based on lifecycle events and lead characteristics.

Sales team notifications (Slack):

{
  "channel": "#sales-alerts",
  "text": "🔥 New Enterprise Lead Assigned",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*New Lead:* {{$json.firstName}} {{$json.lastName}}
*Company:* {{$json.company}} ({{$json.companySize}} employees)
*Revenue:* ${{$json.revenue}}
*Assigned to:* {{$json.assignedTo}}"
      }
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": {
            "type": "plain_text",
            "text": "View in HubSpot"
          },
          "url": "https://app.hubspot.com/contacts/{{$json.hubspotContactId}}"
        }
      ]
    }
  ]
}

Marketing team notifications (email digest):

Create a Schedule node that runs daily at 9 AM, aggregates yesterday's leads, and sends a summary:

// Function node to aggregate daily leads
const leads = $input.all();

const summary = {
  total: leads.length,
  bySource: {},
  bySegment: {},
  topCompanies: []
};

leads.forEach(lead => {
  // Count by source
  summary.bySource[lead.json.source] = (summary.bySource[lead.json.source] || 0) + 1;
  
  // Count by segment
  summary.bySegment[lead.json.segment] = (summary.bySegment[lead.json.segment] || 0) + 1;
  
  // Track high-value companies
  if (lead.json.revenue > 5000000) {
    summary.topCompanies.push({
      name: lead.json.company,
      revenue: lead.json.revenue,
      size: lead.json.companySize
    });
  }
});

return { summary };

Why this approach:
Real-time Slack alerts for sales ensure immediate follow-up on hot leads. Daily email digests for marketing prevent notification fatigue while maintaining visibility. Segmenting notifications by team and urgency ensures the right people see the right data at the right time.

Workflow Architecture Overview

This workflow consists of 23 nodes organized into 6 main sections:

  1. Lead capture & enrichment (Nodes 1-5): Webhook receives lead, calls Clearbit API, structures enriched data
  2. Routing logic (Nodes 6-9): Switch node segments by company size, Function node assigns territory and owner
  3. CRM operations (Nodes 10-14): Creates/updates HubSpot contacts, handles duplicates, creates associated deals
  4. Lifecycle automation (Nodes 15-18): Separate webhook triggers for each stage transition, updates lifecycle properties
  5. Notifications (Nodes 19-21): Slack alerts for sales, email digests for marketing, CSM task creation
  6. Reporting sync (Nodes 22-23): Pushes data to PostgreSQL for dashboard consumption

Execution flow:

  • Trigger: Webhook POST from form submission or integration
  • Average run time: 3-5 seconds for full enrichment and CRM creation
  • Key dependencies: HubSpot API, Clearbit API, Slack webhook

Critical nodes:

  • Switch Node (6): Segments leads by company size—determines entire routing path
  • Function Node (8): Territory assignment logic—edit this to match your sales structure
  • HTTP Request Node (11): HubSpot contact creation—handles both new and duplicate scenarios
  • Slack Node (19): Sales notifications—customize channel and message format per segment

The complete n8n workflow JSON template is available at the bottom of this article.

Key Configuration Details

HubSpot Integration

Required fields:

  • API Key: Your HubSpot Private App token (Settings → Integrations → Private Apps)
  • Base URL: https://api.hubapi.com/crm/v3
  • Timeout: 30 seconds (increase for batch operations)

Common issues:

  • Using wrong API version → Results in 404 errors (always use /v3/ endpoints)
  • Missing required properties → Contact creation fails (email, firstname, lastname are mandatory)
  • Owner ID format → Must be numeric string, not email address

Clearbit Enrichment

API configuration:

  • Endpoint: https://company.clearbit.com/v2/companies/find
  • Rate limit: 600 requests/hour on free tier (implement caching for high-volume)
  • Fallback: If enrichment fails, workflow continues with form data only

Why this approach:
Separating enrichment into its own node with error handling ensures form submissions never fail due to third-party API issues. The workflow degrades gracefully—you get a basic lead even if enrichment times out.

Routing Variables to Customize:

// Edit these thresholds in Switch node
const ENTERPRISE_MIN_EMPLOYEES = 500;
const ENTERPRISE_MIN_REVENUE = 10000000;
const MID_MARKET_MIN_EMPLOYEES = 50;

// Edit territory assignments in Function node
const TERRITORY_MAPPING = {
  'West': ['CA', 'OR', 'WA', 'NV', 'AZ'],
  'Central': ['TX', 'CO', 'IL', 'MN', 'KS'],
  'East': ['NY', 'MA', 'FL', 'GA', 'NC', 'VA']
};

// Edit owner assignments
const OWNER_EMAILS = {
  'enterprise-west': 'john@company.com',
  'enterprise-central': 'sarah@company.com',
  'enterprise-east': 'mike@company.com'
};

Testing & Validation

Test each component independently:

  1. Webhook capture: Use Postman or curl to POST test data

    curl -X POST https://your-n8n.app/webhook/lead-capture \
      -H "Content-Type: application/json" \
      -d '{"email":"test@acme.com","firstName":"Test","lastName":"User"}'
    
  2. Enrichment API: Check node output for populated fields (companySize, industry, revenue)

  3. Routing logic: Verify Switch node routes to correct output based on company size

  4. CRM creation: Confirm contact appears in HubSpot with correct properties and owner

  5. Notifications: Check Slack channel for formatted alert message

Common troubleshooting:

Issue Cause Solution
Webhook returns 500 error Node execution failure Check n8n logs, enable "Continue on Fail" for API nodes
Enrichment returns empty Invalid domain extraction Verify email split logic: {{$json.email.split('@')[1]}}
Wrong owner assigned Territory mapping mismatch Update state list in Function node territory arrays
Duplicate contacts created Missing error handling Add IF node to check HTTP status 409, route to UPDATE

Running evaluations:

Create a test dataset of 20 leads with known characteristics (company size, location, source). Run them through the workflow and verify:

  • 100% enrichment success rate (or graceful degradation)
  • Correct segment assignment (enterprise/mid-market/SMB)
  • Accurate territory routing based on state
  • CRM records created with all expected properties
  • Notifications sent to appropriate channels

Deployment Considerations

Production Deployment Checklist

Area Requirement Why It Matters
Error Handling Retry logic with exponential backoff on all API nodes Prevents data loss during temporary API outages (HubSpot downtime)
Monitoring Webhook health checks every 5 minutes via external monitor Detect workflow failures within 5 minutes vs discovering days later
Rate Limiting Implement queue for high-volume lead sources (>100/hour) Clearbit free tier limits to 600/hour—queue prevents rejection
Documentation Node-by-node comments explaining business logic Reduces modification time from 4 hours to 30 minutes for new team members
Credentials Store all API keys in n8n credentials manager, never hardcode Prevents accidental exposure in workflow exports or version control
Backup Daily automated export of workflow JSON to Git repository Enables rollback if changes break production workflow

Error handling strategy:

Add error workflow that catches failed executions:

{
  "trigger": "Error Trigger",
  "actions": [
    {
      "node": "Log to Database",
      "operation": "INSERT",
      "table": "workflow_errors",
      "fields": {
        "workflow_id": "={{$workflow.id}}",
        "execution_id": "={{$execution.id}}",
        "error_message": "={{$json.error.message}}",
        "input_data": "={{$json.input}}",
        "timestamp": "={{$now}}"
      }
    },
    {
      "node": "Alert Slack",
      "channel": "#revops-alerts",
      "message": "🚨 Lead capture workflow failed: {{$json.error.message}}"
    }
  ]
}

Monitoring recommendations:

  • Set up UptimeRobot to ping webhook endpoint every 5 minutes
  • Create HubSpot workflow that alerts if no new leads created in 2 hours (detects broken integration)
  • Monitor n8n execution history daily for failed runs
  • Track enrichment success rate (should be >85%)

Use Cases & Variations

Use Case 1: SaaS Product-Led Growth

  • Industry: B2B SaaS with free trial
  • Scale: 200-500 trial signups/day
  • Modifications needed: Add product usage webhook triggers, implement trial-to-paid conversion scoring, connect to Segment or Mixpanel for behavioral data, add automated expansion revenue workflows when usage hits thresholds

Use Case 2: Professional Services Firm

  • Industry: Consulting, agencies, law firms
  • Scale: 50-100 inbound leads/week
  • Modifications needed: Replace company size routing with service line matching (practice areas), add conflict check integration before assignment, implement referral source tracking for partner attribution, create proposal automation when opportunity reaches "qualified" stage

Use Case 3: E-commerce B2B Marketplace

  • Industry: Wholesale, manufacturing, distribution
  • Scale: 1000+ buyer inquiries/day
  • Modifications needed: Add inventory availability check before routing, implement minimum order value qualification, connect to ERP for real-time pricing, create automated quote generation for qualified buyers, add supplier notification when high-value buyer shows interest

Use Case 4: Multi-Product Enterprise

  • Industry: Technology company with 3+ product lines
  • Scale: 500+ leads/day across products
  • Modifications needed: Add product interest detection from form fields or UTM parameters, route to product-specific sales teams, create cross-sell workflows when customers show interest in additional products, implement account-based routing for existing customer expansion

Customizations & Extensions

Alternative Integrations

Instead of HubSpot:

  • Salesforce: Requires OAuth2 authentication and different object structure—swap HTTP Request nodes with Salesforce node, map to Lead/Contact/Opportunity objects (nodes 10-14)
  • Pipedrive: Better for SMB sales teams—simpler API, use Pipedrive node instead of HTTP Request, fewer custom fields available
  • Close CRM: Best for high-velocity inside sales—native calling integration, swap nodes 10-14, add call logging automation

Instead of Clearbit:

  • Apollo.io: Better B2B data coverage—change enrichment endpoint to https://api.apollo.io/v1/people/match, includes contact-level data (direct dials, verified emails)
  • ZoomInfo: Enterprise-grade data—requires different authentication, provides intent signals and technographics, add 3-5 nodes for intent scoring
  • Hunter.io: Email-focused enrichment—use when you only need email verification and basic company data, faster response time (< 1 second)

Workflow Extensions

Add automated reporting:

  • Add a Schedule node to run daily at 8 AM
  • Connect to Google Sheets API or Airtable
  • Generate executive summary with lead volume, conversion rates, pipeline value by segment
  • Nodes needed: +6 (Schedule Trigger, Aggregate Function, HTTP Request to Sheets, Format Data, Send Email)

Scale to handle high volume:

  • Replace webhook trigger with Queue node (Redis-backed)
  • Add batch processing (process 100 leads at a time instead of one-by-one)
  • Implement caching layer for enrichment (check cache before API call)
  • Performance improvement: Handle 10,000+ leads/day vs 500/day limit

Add renewal automation:

  • Create separate workflow triggered 90 days before renewal date
  • Pull product usage data from analytics platform
  • Score renewal health (usage trends, support tickets, payment history)
  • Auto-create renewal opportunity and assign to CSM
  • Send personalized outreach sequence based on health score
  • Nodes needed: +15 (Schedule, HTTP Request to analytics, Function for scoring, CRM update, Email sequence)

Integration possibilities:

Add This To Get This Complexity
Calendly webhook Auto-progress to SQL when demo booked Easy (3 nodes)
Stripe billing Track payment failures, trigger at-risk workflows Medium (8 nodes)
Intercom/Drift Route high-intent chat conversations to sales Medium (6 nodes)
LinkedIn Sales Navigator Enrich with social selling data Medium (5 nodes)
Gong/Chorus Analyze call recordings, update deal stages Advanced (12 nodes)
Tableau/Looker Push data to BI tool for executive dashboards Medium (7 nodes)

Feature additions:

Lead scoring model:
Add a Function node after enrichment that calculates a 0-100 score based on:

  • Company size (20 points for >500 employees)
  • Industry match (15 points for target verticals)
  • Revenue (25 points for >$10M ARR)
  • Tech stack fit (20 points for complementary tools)
  • Engagement signals (20 points for multiple touchpoints)

Route only leads scoring >60 to sales, send <60 to nurture campaigns.

Duplicate prevention:
Before creating CRM records, add an HTTP Request node that searches for existing contacts by email domain. If found, append to existing company record instead of creating new. Prevents database bloat and maintains account hierarchy.

Get Started Today

Ready to automate your revenue operations?

  1. Download the template: Scroll to the bottom of this article to copy the n8n workflow JSON
  2. Import to n8n: Go to Workflows → Import from URL or File, paste the JSON
  3. Configure your services: Add your API credentials for HubSpot, Clearbit, and Slack in n8n's credential manager
  4. Customize routing logic: Edit the Switch node thresholds and Function node territory mappings to match your sales structure
  5. Test with sample data: Use the testing section above to verify each component works before going live
  6. Deploy to production: Activate the workflow and update your form submission endpoints to the new webhook URL

This RevOps system eliminates 15-20 hours of weekly manual work while ensuring no lead falls through the cracks. Your sales team gets enriched, routed leads in under 30 seconds. Marketing sees real-time attribution. Customer success gets early renewal risk signals.

Need help customizing this workflow for your specific CRM, sales process, or tool stack? Schedule an intro call with Atherial at atherial.ai/contact.


n8n Workflow JSON Template

[object Object]

Copy this JSON and import it into your n8n instance via Workflows → Import from File. Replace placeholder credentials with your actual API keys in the n8n credentials manager before activating.

Complete N8N Workflow Template

Copy the JSON below and import it into your N8N instance via Workflows → Import from File

{
  "name": "End-to-End RevOps Automation Engine",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook Lead Capture",
      "type": "n8n-nodes-base.webhook",
      "position": [
        100,
        100
      ],
      "parameters": {
        "path": "lead-capture",
        "httpMethod": "POST",
        "responseCode": 200,
        "responseMode": "onReceived"
      },
      "typeVersion": 2.1
    },
    {
      "id": "2",
      "name": "Lead Data Normalization",
      "type": "n8n-nodes-base.code",
      "position": [
        300,
        100
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Normalize and validate lead data from webhook\nconst normalized = {\n  email: ($json.email || '').toLowerCase().trim(),\n  firstName: ($json.first_name || $json.firstName || '').trim(),\n  lastName: ($json.last_name || $json.lastName || '').trim(),\n  company: ($json.company || '').trim(),\n  phone: ($json.phone || '').replace(/\\D/g, ''),\n  source: $json.source || 'web',\n  leadScore: Math.min(100, Math.max(0, parseInt($json.lead_score) || 0)),\n  jobTitle: ($json.job_title || $json.jobTitle || '').trim(),\n  industry: ($json.industry || '').trim(),\n  capturedAt: new Date().toISOString()\n};\n\n// Validate email format\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nif (!emailRegex.test(normalized.email)) {\n  throw new Error('Invalid email format: ' + $json.email);\n}\n\nreturn normalized;",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "3",
      "name": "Check Duplicate Contact",
      "type": "n8n-nodes-base.hubspot",
      "onError": "continueRegularOutput",
      "position": [
        500,
        100
      ],
      "parameters": {
        "email": "={{ $json.email }}",
        "resource": "contact",
        "operation": "search"
      },
      "credentials": {
        "hubspotApi": "{{ $secrets.HUBSPOT_API }}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "4",
      "name": "Branch New vs Existing",
      "type": "n8n-nodes-base.if",
      "position": [
        700,
        100
      ],
      "parameters": {
        "conditions": {
          "options": [
            {
              "type": "boolean",
              "value": "={{ $json.length === 0 }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5",
      "name": "Create HubSpot Contact",
      "type": "n8n-nodes-base.hubspot",
      "onError": "continueRegularOutput",
      "position": [
        900,
        50
      ],
      "parameters": {
        "email": "={{ $('Lead Data Normalization').json.email }}",
        "resource": "contact",
        "operation": "upsert",
        "additionalFields": {
          "phone": "={{ $('Lead Data Normalization').json.phone }}",
          "company": "={{ $('Lead Data Normalization').json.company }}",
          "jobTitle": "={{ $('Lead Data Normalization').json.jobTitle }}",
          "lastName": "={{ $('Lead Data Normalization').json.lastName }}",
          "firstName": "={{ $('Lead Data Normalization').json.firstName }}"
        },
        "customProperties": {
          "lead_score": "={{ $('Lead Data Normalization').json.leadScore }}",
          "lead_source": "={{ $('Lead Data Normalization').json.source }}"
        }
      },
      "credentials": {
        "hubspotApi": "{{ $secrets.HUBSPOT_API }}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "6",
      "name": "Enrich Lead Data",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        900,
        200
      ],
      "parameters": {
        "url": "https://api.enrichment-service.com/enrich",
        "method": "POST",
        "headers": {
          "Authorization": "=Bearer {{ $secrets.ENRICHMENT_API }}"
        },
        "bodyParametersJson": "={{ {email: $json.email, company: $json.company} }}"
      },
      "retryOnFail": true,
      "typeVersion": 4.3,
      "waitBetweenTries": 1000
    },
    {
      "id": "7",
      "name": "Merge Enriched Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1100,
        200
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Merge original and enriched lead data\nconst original = $('Lead Data Normalization').json;\nconst enriched = $json.data || $json || {};\n\nreturn [{\n  ...original,\n  enrichedData: enriched,\n  companySize: enriched.company_size || enriched.companySize || '',\n  revenue: enriched.revenue || '',\n  website: enriched.website || '',\n  linkedinUrl: enriched.linkedin_url || enriched.linkedinUrl || '',\n  enrichedAt: new Date().toISOString()\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "8",
      "name": "Lead Routing Logic",
      "type": "n8n-nodes-base.code",
      "position": [
        1300,
        200
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Determine lead routing and stage based on scoring logic\nconst leadScore = $json.leadScore || 0;\nconst companySize = $json.companySize || '';\nconst jobTitle = $json.jobTitle || '';\n\nlet assignedTo = 'sales@company.com';\nlet stage = 'Lead';\nlet priority = 'Normal';\n\n// Enterprise logic\nif (companySize === 'enterprise') {\n  assignedTo = 'enterprise-sales@company.com';\n  stage = 'Enterprise Lead';\n  priority = 'High';\n} else if (leadScore >= 80) {\n  // High-quality lead\n  stage = 'Qualified Lead';\n  assignedTo = 'senior-sales@company.com';\n  priority = 'High';\n} else if (leadScore >= 50) {\n  // Marketing qualified lead\n  stage = 'Marketing Qualified Lead';\n  assignedTo = 'sales@company.com';\n  priority = 'Medium';\n} else {\n  // Cold lead\n  stage = 'Lead';\n  assignedTo = 'sales-dev@company.com';\n  priority = 'Low';\n}\n\n// Check for executive titles\nif (jobTitle.match(/executive|ceo|cto|cfo|vp|director/i)) {\n  priority = 'High';\n}\n\nreturn [{\n  ...item,\n  assignedTo,\n  stage,\n  priority,\n  routedAt: new Date().toISOString(),\n  routingReason: `Lead Score: ${leadScore}, Company: ${companySize}, Title: ${jobTitle}`\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "9",
      "name": "Update Contact Lifecycle Stage",
      "type": "n8n-nodes-base.hubspot",
      "onError": "continueRegularOutput",
      "position": [
        1500,
        150
      ],
      "parameters": {
        "resource": "contact",
        "contactId": {
          "__rl": true,
          "mode": "expression",
          "value": "={{ $json.contactId }}"
        },
        "operation": "update",
        "updateFields": {
          "lifecycleStage": "={{ $json.stage }}"
        }
      },
      "credentials": {
        "hubspotApi": "{{ $secrets.HUBSPOT_API }}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "10",
      "name": "Create Follow-up Task",
      "type": "n8n-nodes-base.hubspot",
      "onError": "continueRegularOutput",
      "position": [
        1700,
        100
      ],
      "parameters": {
        "resource": "engagement",
        "taskBody": "=Follow up with {{ $json.firstName }} {{ $json.lastName }} from {{ $json.company }}",
        "taskType": "task",
        "contactId": {
          "__rl": true,
          "mode": "expression",
          "value": "={{ $json.contactId }}"
        },
        "operation": "create",
        "taskStatus": "not_started",
        "engagementType": "task"
      },
      "credentials": {
        "hubspotApi": "{{ $secrets.HUBSPOT_API }}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "11",
      "name": "Send Slack Notification",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        1700,
        250
      ],
      "parameters": {
        "text": "=New {{ $json.stage }}: {{ $json.firstName }} {{ $json.lastName }} from {{ $json.company }} (Score: {{ $json.leadScore }}/100, Assigned: {{ $json.assignedTo }})",
        "channel": "#sales-leads",
        "resource": "message",
        "operation": "create"
      },
      "credentials": {
        "slackApi": "{{ $secrets.SLACK_API }}"
      },
      "typeVersion": 2.3
    },
    {
      "id": "12",
      "name": "Send Assignment Email",
      "type": "n8n-nodes-base.emailSend",
      "onError": "continueRegularOutput",
      "position": [
        1700,
        350
      ],
      "parameters": {
        "subject": "=New Lead Assignment: {{ $json.firstName }} {{ $json.lastName }}",
        "toEmail": "={{ $json.assignedTo }}",
        "htmlBody": "=<h2>New Lead Assigned</h2><p><strong>Contact:</strong> {{ $json.firstName }} {{ $json.lastName }}</p><p><strong>Company:</strong> {{ $json.company }}</p><p><strong>Email:</strong> {{ $json.email }}</p><p><strong>Lead Score:</strong> {{ $json.leadScore }}/100</p><p><strong>Stage:</strong> {{ $json.stage }}</p><p><strong>Priority:</strong> {{ $json.priority }}</p>",
        "fromEmail": "noreply@company.com"
      },
      "typeVersion": 2.1
    },
    {
      "id": "13",
      "name": "Schedule Lifecycle Review",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        100,
        450
      ],
      "parameters": {
        "interval": [
          1,
          "days"
        ]
      },
      "typeVersion": 1.2
    },
    {
      "id": "14",
      "name": "Get All Leads",
      "type": "n8n-nodes-base.hubspot",
      "onError": "continueRegularOutput",
      "position": [
        300,
        450
      ],
      "parameters": {
        "limit": 100,
        "filters": {
          "lifecycleStage": [
            "Lead",
            "Marketing Qualified Lead"
          ]
        },
        "resource": "contact",
        "operation": "getAll"
      },
      "credentials": {
        "hubspotApi": "{{ $secrets.HUBSPOT_API }}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "15",
      "name": "Check Last Engagement",
      "type": "n8n-nodes-base.code",
      "position": [
        500,
        450
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Calculate engagement recency for each lead\nconst lastEngagement = new Date($json.hs_lead_status_date || $json.lastEngagementDate || 0);\nconst now = new Date();\nconst daysSinceEngagement = Math.floor((now - lastEngagement) / (1000 * 60 * 60 * 24));\nconst isStale = daysSinceEngagement > 30;\n\nreturn [{\n  ...item,\n  isStale,\n  lastEngagementDate: lastEngagement.toISOString(),\n  daysSinceEngagement\n}];",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "16",
      "name": "Filter Stale Leads",
      "type": "n8n-nodes-base.if",
      "position": [
        700,
        450
      ],
      "parameters": {
        "conditions": {
          "options": [
            {
              "type": "boolean",
              "value": "={{ $json.isStale === true }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "17",
      "name": "Create Nurture Task",
      "type": "n8n-nodes-base.hubspot",
      "onError": "continueRegularOutput",
      "position": [
        900,
        450
      ],
      "parameters": {
        "resource": "engagement",
        "taskBody": "=Nurture follow-up: No engagement in {{ $json.daysSinceEngagement }} days",
        "taskType": "task",
        "contactId": {
          "__rl": true,
          "mode": "expression",
          "value": "={{ $json.id }}"
        },
        "operation": "create",
        "taskStatus": "not_started",
        "engagementType": "task"
      },
      "credentials": {
        "hubspotApi": "{{ $secrets.HUBSPOT_API }}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "18",
      "name": "Log Lifecycle Event",
      "type": "n8n-nodes-base.mongoDb",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        1100,
        450
      ],
      "parameters": {
        "data": "={{ {contactId: $json.id, email: $json.email, eventType: 'stale_lead_detected', timestamp: new Date().toISOString(), daysSinceEngagement: $json.daysSinceEngagement, lifecycleStage: $json.hs_lifecyclestage} }}",
        "operation": "insert",
        "collection": "lead_lifecycle_events"
      },
      "credentials": {
        "mongoDb": "{{ $secrets.MONGODB_CONNECTION }}"
      },
      "retryOnFail": true,
      "typeVersion": 1.2
    },
    {
      "id": "19",
      "name": "Remove Duplicate Leads",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        1300,
        100
      ],
      "parameters": {
        "fieldToCompare": "email"
      },
      "typeVersion": 2
    },
    {
      "id": "20",
      "name": "Data Cleanup & Normalization",
      "type": "n8n-nodes-base.code",
      "position": [
        1500,
        100
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Final data cleanup and standardization\nconst cleaned = {\n  email: ($json.email || '').toLowerCase().trim(),\n  phone: ($json.phone || '').replace(/\\D/g, ''),\n  company: ($json.company || '').trim(),\n  firstName: ($json.firstName || '').trim(),\n  lastName: ($json.lastName || '').trim(),\n  website: ($json.website || '').toLowerCase().trim(),\n  leadScore: Math.min(100, Math.max(0, $json.leadScore || 0))\n};\n\n// Final validation\nconst emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nif (!emailRegex.test(cleaned.email)) {\n  throw new Error('Failed email validation in cleanup: ' + cleaned.email);\n}\n\nif (cleaned.phone && cleaned.phone.length < 10) {\n  cleaned.phone = '';\n}\n\nreturn [cleaned];",
        "language": "javaScript"
      },
      "typeVersion": 2
    }
  ],
  "settings": {
    "executionOrder": "v1",
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "connections": {
    "Get All Leads": {
      "main": [
        [
          {
            "node": "Check Last Engagement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enrich Lead Data": {
      "main": [
        [
          {
            "node": "Merge Enriched Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Stale Leads": {
      "main": [
        [
          {
            "node": "Create Nurture Task",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Lead Routing Logic": {
      "main": [
        [
          {
            "node": "Update Contact Lifecycle Stage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Nurture Task": {
      "main": [
        [
          {
            "node": "Log Lifecycle Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Enriched Data": {
      "main": [
        [
          {
            "node": "Lead Routing Logic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Lead Capture": {
      "main": [
        [
          {
            "node": "Lead Data Normalization",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Last Engagement": {
      "main": [
        [
          {
            "node": "Filter Stale Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Follow-up Task": {
      "main": [
        [
          {
            "node": "Send Slack Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Branch New vs Existing": {
      "main": [
        [
          {
            "node": "Create HubSpot Contact",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Data Cleanup & Normalization",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create HubSpot Contact": {
      "main": [
        [
          {
            "node": "Enrich Lead Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicate Leads": {
      "main": [
        [
          {
            "node": "Enrich Lead Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Duplicate Contact": {
      "main": [
        [
          {
            "node": "Branch New vs Existing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lead Data Normalization": {
      "main": [
        [
          {
            "node": "Check Duplicate Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Slack Notification": {
      "main": [
        [
          {
            "node": "Send Assignment Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Lifecycle Review": {
      "main": [
        [
          {
            "node": "Get All Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data Cleanup & Normalization": {
      "main": [
        [
          {
            "node": "Remove Duplicate Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Contact Lifecycle Stage": {
      "main": [
        [
          {
            "node": "Create Follow-up Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}