{
  "$schema": "../workflow-recipe.schema.json",
  "format": "revenanas.workflow-recipe/v1",
  "slug": "hubspot-clay-lead-routing",
  "title": "How to route HubSpot leads to the right rep in Clay",
  "job": "Assign a new, unowned HubSpot lead to an eligible sales rep using fit, territory, segment, availability, and weighted capacity.",
  "tools": ["HubSpot", "Clay"],
  "service": {
    "name": "Automated Inbound & PLG Qualification",
    "url": "https://www.revenanas.com/services/automated-inbound-plg-qualification"
  },
  "importability": {
    "clay": "official-cli-bundle",
    "notes": "This workflow was built and tested through Clay's official Terracotta CLI. Clay does not document a public template link for Workflows, so the download uses a transparent Revenanas bundle plus an installer that recreates the draft with official CLI commands, validates the graph, and can run the tests again to confirm it still works.",
    "template_url": null
  },
  "direct_answer": "To route HubSpot leads in Clay without overwriting owners, first stop any record that already has an owner. For unowned leads, derive a territory and commercial segment, filter the rep roster to active and available matches, select the rep with the lowest capacity-adjusted load, and only then prepare the HubSpot owner update. If nobody qualifies, hold the lead for RevOps review instead of assigning it arbitrarily.",
  "inputs": [
    {"name": "hubspot_record_id", "type": "string", "required": true, "description": "The HubSpot contact or lead record to update."},
    {"name": "email", "type": "email", "required": true, "description": "Used for contact and company enrichment."},
    {"name": "company_domain", "type": "domain", "required": false, "description": "Preferred company matching key when HubSpot already has it."},
    {"name": "existing_owner_id", "type": "string", "required": false, "description": "A non-empty value prevents automatic reassignment."},
    {"name": "country", "type": "string", "required": true, "description": "ISO country code used to derive a routing region."},
    {"name": "employee_count", "type": "integer", "required": true, "description": "Used to derive the commercial segment."},
    {"name": "target_account", "type": "boolean", "required": false, "description": "Can promote a lead into a named-account routing branch."},
    {"name": "reps", "type": "array", "required": true, "description": "Rep roster containing owner IDs, territories, segments, availability, current load and capacity weight."}
  ],
  "outputs": [
    {"name": "assigned_owner_id", "type": "string", "required": true, "description": "HubSpot owner ID selected from the eligible reps view."},
    {"name": "assigned_owner_email", "type": "email", "required": true, "description": "Human-readable assignment for review."},
    {"name": "routing_reason", "type": "string", "required": true, "description": "The territory, segment and capacity rule that selected the rep."},
    {"name": "routing_status", "type": "enum", "required": true, "description": "assigned, held_existing_owner, or held_no_eligible_rep."}
  ],
  "steps": [
    {
      "id": "01-source",
      "action": "Receive the lead and current rep roster",
      "purpose": "Start one workflow run with the fields required to make a routing decision.",
      "configuration": {"tested_trigger": "manual", "production_options": ["webhook", "audience segment", "workflow routine"]}
    },
    {
      "id": "02-owner-guard",
      "action": "Normalize the lead and derive routing keys",
      "purpose": "Normalize identifiers, map country to territory and map headcount to segment.",
      "configuration": {"regions": ["North America", "UKI", "DACH", "Rest of Europe", "Unknown"], "segments": ["SMB", "Mid-market", "Enterprise", "Named account", "Unknown"]}
    },
    {
      "id": "03-enrich",
      "action": "Check whether HubSpot already has an owner",
      "purpose": "Stop before rep selection when a human or another system already owns the record.",
      "configuration": {"run_if": "existing_owner_id is not empty", "terminal_status": "held_existing_owner"}
    },
    {
      "id": "04-segment",
      "action": "Preserve an existing owner",
      "purpose": "Return an auditable hold result without changing ownership.",
      "configuration": {"output": "held_existing_owner"}
    },
    {
      "id": "05-eligible-reps",
      "action": "Select an eligible rep by capacity",
      "purpose": "Exclude inactive or out-of-office reps, require territory and segment matches, then choose the lowest current load divided by capacity weight.",
      "configuration": {"required": ["active = true", "out_of_office = false", "region match", "segment match", "hubspot_owner_id present"], "score": "current_open_leads / capacity_weight"}
    },
    {
      "id": "06-distribute",
      "action": "Check whether an eligible rep was found",
      "purpose": "Split successful assignments from records that need RevOps review.",
      "configuration": {"assigned_if": "routing_status equals assigned", "default": "held_no_eligible_rep"}
    },
    {
      "id": "07-writeback",
      "action": "Prepare the HubSpot owner writeback",
      "purpose": "Emit the exact record ID, owner ID, owner email and routing reason for a downstream HubSpot action.",
      "configuration": {"write_only_if": "existing_owner_id is blank AND assigned_owner_id is present", "fields": ["hubspot_record_id", "hubspot_owner_id", "routing_reason"]}
    },
    {
      "id": "08-monitor",
      "action": "Hold unmatched leads for RevOps review",
      "purpose": "Make empty eligible-rep pools visible instead of silently dropping or randomly assigning leads.",
      "configuration": {"status": "held_no_eligible_rep", "review_fields": ["hubspot_record_id", "routing_status", "routing_reason", "eligible_rep_count"]}
    }
  ],
  "guardrails": [
    "Never overwrite a non-empty HubSpot owner automatically.",
    "Re-run the tests after changing territory, segment or capacity rules.",
    "Hold the row when no eligible rep exists; do not fall back to an arbitrary owner.",
    "Keep API credentials in Clay connections, not in formula columns or downloaded files.",
    "Write the assignment reason so RevOps can audit the rule later.",
    "Add the live HubSpot write action only after mapping it against a test portal."
  ],
  "test_cases": [
    {"name": "Existing owner", "input": {"existing_owner_id": "owner_existing"}, "expected_status": "held_existing_owner", "observed": "passed"},
    {"name": "Enterprise UK lead", "input": {"country": "GB", "employee_count": 1200, "existing_owner_id": ""}, "expected_owner": "owner_selected", "observed": "passed"},
    {"name": "No active territory owner", "input": {"country": "DE", "employee_count": 80, "existing_owner_id": ""}, "expected_status": "held_no_eligible_rep", "observed": "passed"}
  ],
  "architecture": {
    "summary": "A manual test trigger feeds one normalization step and two explicit rule branches. The first branch protects existing owners. The second filters and scores eligible reps, then either emits a HubSpot-ready payload or holds the record for review.",
    "diagram": "Manual input -> Normalize lead -> Existing owner? -> Hold existing owner OR Select eligible rep -> Rep found? -> Prepare HubSpot writeback OR Hold for RevOps review"
  },
  "verification": {
    "last_tested": "2026-09-11",
    "builder": "Clay official Terracotta CLI",
    "graph_valid": true,
    "passing_tests": 3,
    "data_credits_used": 0,
    "action_credits_used": 0,
    "observed_failures": [
      "Clay code-node output schemas rejected array fields, so the rep roster is serialized as JSON between deterministic code steps.",
      "The standalone code tester allowed Python datetime while the workflow runtime did not, so timestamps were moved out of the routing core.",
      "Clay treated an empty optional mapped field as missing until the downstream input schema explicitly excluded it from the required list."
    ]
  },
  "limitations": [
    "The downloadable workflow stops at a HubSpot-ready payload; it does not include a live HubSpot write action or credentials.",
    "The rep roster must be supplied by the trigger or replaced with a governed CRM, warehouse or Clay function lookup.",
    "Capacity-adjusted selection is deterministic for the supplied roster. True round-robin state across independent workflow runs needs an external state store.",
    "Clay currently documents public template links for tables and workbooks, not Workflows. The provided installer recreates the workflow through official CLI commands instead."
  ],
  "faqs": [
    {"question": "Can Clay route HubSpot leads without overwriting an existing owner?", "answer": "Yes. Put an existing-owner conditional before rep selection and terminate that branch with a held_existing_owner status. The tested workflow preserved the supplied owner and never reached the assignment branch."},
    {"question": "How does this Clay lead-routing workflow choose a rep?", "answer": "It requires an active, available rep whose territory and segment match the lead, then selects the lowest current_open_leads divided by capacity_weight. Ties are resolved deterministically."},
    {"question": "What happens when no HubSpot owner is eligible?", "answer": "The workflow returns held_no_eligible_rep with an explanation and eligible_rep_count of zero. It does not fall back to a random owner."},
    {"question": "Does the routing logic consume Clay credits?", "answer": "The tested normalization, conditionals and Python code steps used zero data credits and zero action credits. A production HubSpot action or enrichment may have its own cost."},
    {"question": "Is the download a native Clay workflow template?", "answer": "No. Clay does not currently document public template links for Workflows. The download is a transparent bundle installed through Clay's official CLI; the installer recreates the draft, validates it and can run the tests again to confirm it still works."}
  ],
  "sources": [
    {"name": "Clay Workflows documentation", "url": "https://university.clay.com/docs/workflows"},
    {"name": "Clay Workflow FAQs", "url": "https://university.clay.com/docs/workflow-faqs"},
    {"name": "Clay workbook and table template sharing", "url": "https://university.clay.com/docs/workbook-table-sharing-guide"}
  ]
}
