# Intelligent Lead Routing & Response — HubSpot + n8n

Blueprint `REVOPS-L1-001` · Configurable template · Prepared 8 October 2026

This package implements your four phases with HubSpot as the CRM, ZeroBounce for validation, OpenAI for scoring and drafting, Slack for Hot lead alerts, and Resend for actual email sending. PostgreSQL provides durable ingestion, concurrency-safe routing, and duplicate-send controls. These are ordinary PostgreSQL tables and functions; no extensions or community n8n nodes are required.

All workflows import inactive. The worker starts with `dry_run: true`, `auto_send_enabled: false`, and Slack disabled. **Dry run still updates HubSpot and makes paid validation/AI API calls.** Configure a sandbox CRM and test inboxes first.

## What changed from the original blueprint

| Original direction | Implemented behavior and reason |
|---|---|
| Immediate webhook starts a long automation | Persist first, return 202, then start a worker asynchronously. A scheduled sweep handles queued jobs if dispatch fails. |
| Valid/invalid email fork | Allow only matching `valid` addresses; hold unknown, catch-all, disposable, role-based and other risky results. API failures go to review. Validation reduces risk; it cannot guarantee zero bounces. |
| AI chooses Hot/Warm/Cold | AI supplies bounded fit/intent/urgency scores and evidence. Code derives the priority and applies independent sending gates. |
| Round robin for every lead | Preserve existing HubSpot owners. Assign new unowned contacts using an atomic database rotation over the configured available reps. |
| Alert every Hot classification | Alert only eligible Hot leads. Suppressed contacts and excluded customers do not generate sales alerts. Operations incidents use a separate optional channel. |
| Delay every initial reply | Hot leads default to zero delay; Warm leads use a random integer from 10–15 minutes. Delays are configurable pacing, with no claims that a human read or typed the message. |
| Every lead receives AI outreach | Cold, uncertain, unsupported, opted-out and stale leads do not auto-send. Existing customers default to CRM-only handling. |
| CRM sends and logs the email | Resend sends. HubSpot's email activity endpoint records the exact sent subject/body and contact association afterward. |
| Email Sent means success | `Email Sent` means the provider accepted the request. Delivery, replies, bounces and complaints require separate event integrations. |

The extra database makes this more involved than a basic webhook-to-CRM demo. It is included because shared in-memory counters and CRM-only status checks do not provide transactional control across concurrent workflow runs.

## Files

| File | Purpose |
|---|---|
| `01-inbound-intake.n8n.json` | Authenticated webhook, normalization, input validation, durable enqueue, acknowledgment, asynchronous worker dispatch. |
| `02-hubspot-lead-worker.n8n.json` | Validation, CRM create/update, owner assignment, AI scoring/drafting, alerts, sending, logging. |
| `03-hubspot-property-setup.n8n.json` | Run once to create missing contact properties and check existing definitions. |
| `04-errors-and-stalled-jobs.n8n.json` | Error-workflow handler plus periodic stalled/review/logging incident detection. |
| `database-setup.sql` | Required PostgreSQL schema and transactional functions. |
| `database-smoke-test.sql` | Sandbox database checks, all rolled back. Not executed in this environment. |
| `hubspot-property-definitions.json` | Property schema reference; **not an n8n workflow**. |
| `sample-webhook-payload.json` | Example payload for your form backend; use a real inbox you control for live testing. |
| `validation-report.json` | 64 passing local structure, syntax and business-rule checks, with explicit unverified areas. |

## Setup order

1. Provision a dedicated PostgreSQL 14+ application database reachable from n8n. Run `database-setup.sql`. If the setup owner and n8n login differ, apply the commented grants at the bottom for your actual application role. Use an encrypted database connection. Do not modify n8n's internal database tables.
2. Run `database-smoke-test.sql` in your sandbox. Expect a `PASS` notice and rollback. This verifies database behavior sequentially; it is not a concurrent load test.
3. Import all four `*.n8n.json` files individually using n8n's **Import from File** command. Use a current n8n release that supports the embedded node versions. The files have no deployed workflow IDs and must be saved in your own instance.
4. Create the credentials below. Rebind every node showing a `REPLACE_*_CREDENTIAL` reference to your real credential. Secrets belong in n8n credentials, not JSON or the Configuration node.
5. Run workflow 03 manually. It creates only missing properties. An existing property with incompatible types/options stops the setup; resolve that mismatch before proceeding. Property definitions are in `hubspot-property-definitions.json`.
6. Edit **Configuration** in workflow 02. Replace company information, approved service catalog, verified sender, HubSpot portal ID, and real rep profiles. The configuration deliberately fails if required placeholders remain.
7. In workflow 01, **Start Lead Worker**, select your imported workflow 02. Keep **Wait for Sub-Workflow Completion** off. Make sure the worker permits calls from the intake workflow in its workflow settings.
8. In workflow 02 Settings, select workflow 04 as its **Error Workflow**. Also attach it to workflow 01 if you want intake/dispatch failures recorded. Workflow IDs are instance-specific, so these selections are made after import.
9. If using Slack, set `slack_enabled: true` and its channel in workflow 02. Add each rep's Slack member ID for direct mentions. Independently enable the operations channel in workflow 04's **Ops Alert Configuration**.
10. Save/publish/activate workflows 01, 02 and 04 as required by your n8n version. Workflow 03 stays manual. Activation enables the production webhook and scheduled recovery scans.
11. Run the sandbox cases below with dry run enabled. Inspect the queued job, HubSpot contact, owner, scores, draft and execution. When satisfied, set **both** `dry_run: false` and `auto_send_enabled: true` for live first responses.

There is no requirement to activate sending to review this template. Dry-run jobs finish as drafts; enabling sending later does **not** send those existing drafts automatically.

## Credentials

| Credential label | n8n type | Values |
|---|---|---|
| REVOPS Form Secret | Header Auth | Header name such as `X-Lead-Webhook-Secret`; long random secret value. Only your trusted form backend sends it. |
| REVOPS Postgres | Postgres | Dedicated application database, TLS, application login. Bind in workflows 01, 02 and 04. |
| REVOPS HubSpot | Header Auth | `Authorization` = `Bearer YOUR_PRIVATE_APP_TOKEN`. Bind all HubSpot HTTP nodes in workflows 02/03. |
| REVOPS ZeroBounce | Query Auth | Name `api_key`; value your ZeroBounce key. The HTTP node supplies email and timeout. |
| REVOPS OpenAI | Header Auth | `Authorization` = `Bearer YOUR_OPENAI_API_KEY`. Requires an API account; a ChatGPT subscription is not the credential. |
| REVOPS Resend | Header Auth | `Authorization` = `Bearer YOUR_RESEND_API_KEY`. Sender domain must be verified. |
| REVOPS Slack | Header Auth | `Authorization` = `Bearer YOUR_SLACK_BOT_TOKEN`, with `chat:write` and access to the configured channels. Only needed when alerts are enabled. |

HubSpot permissions: contact object read/write; contact schema read/write for workflow 03; permissions to create email activities. HubSpot's API reference for the email operation lists the accepted scopes for your app platform. Use the account's actual owner IDs, not user IDs. You can retrieve them with an authorized request to `GET /crm/v3/owners/` if needed; add owner-read permission for that lookup. Do not hardcode a presumed email-write scope without checking what your app supports.

The sender is a verified company address with the assigned rep's email as `reply_to`. It does not require spoofing a rep mailbox. To send directly from individual mailboxes instead, replace the sending adapter and preserve its idempotency/reconciliation behavior; do not point the existing JSON at a different email provider unchanged.

## Form contract

The webhook accepts a JSON object containing:

- Required: `submission_id` (stable UUID), `email`.
- Optional strings: `first_name`, `last_name`, `company`, `phone`, `job_title`, `message`, `service_interest`, `budget`, `timeline`, `form_id`, `page_url`.
- `response_requested`: actual JSON boolean `true` only when the trusted backend has recorded that the person requested a response to this inquiry. Missing, false, and the string `"true"` all prevent auto-send. This field does not enroll a contact into marketing subscriptions.

Use the same UUID and same data when retrying a delivery. A different inquiry gets a new UUID. A UUID reused with different normalized data returns 409. Malformed input returns 400. Accepted and identical duplicate submissions return 202. If the database cannot persist the lead, the workflow must fail rather than acknowledge it as queued; the backend should retry with the same UUID.

Example server-side request:

```bash
curl --request POST 'https://YOUR_N8N_HOST/webhook/revops-l1-001-inbound' \
  --header 'Content-Type: application/json' \
  --header 'X-Lead-Webhook-Secret: YOUR_SECRET' \
  --data-binary '@sample-webhook-payload.json'
```

Do not send this shared-secret request directly from browser JavaScript. Your form backend should enforce its normal bot/rate/size controls and forward approved submissions. Native HubSpot form/webhook event payloads need an adapter to this contract; this template does not assume they already have these fields.

## Scoring and knowledgebase

The approved `services` array is the initial knowledgebase. Populate it with actual offerings, ideal customers and exclusions. Keep prices and promises out unless explicitly approved. For a large catalog, replace this array with a trusted retrieval step, keeping prospect text separate from authoritative knowledge.

| Dimension or gate | Default |
|---|---|
| Fit | 0–40 |
| Buying intent | 0–40 |
| Urgency with evidence | 0–20 |
| Hot | Total >=75, fit >=25, intent >=25, approved service match, confidence >=0.80, no review flag |
| Warm | Total >=40 and not qualified as Hot |
| Cold | Below Warm threshold |
| Minimum fit/intent for any automatic reply | Fit >=20 and intent >=15 |
| Confidence below threshold / flagged ambiguity / unavailable owner profile | Human review, regardless of score |
| Automatic reply priorities | Hot and Warm only, with all remaining gates satisfied |

Priority and send permission are separate. A Warm classification alone does not authorize a reply. Low-confidence leads get the HubSpot `Lead: Review` value; their underlying assessment is retained in PostgreSQL.

The model produces structured JSON, not tool calls. Code validates scores, confidence, service IDs and draft format, fixes the recipient from the sanitized submission, and adds the configured signature. The model cannot choose recipients, change credentials or execute external actions. Prompts and schemas reduce errors; they do not guarantee factual or scoring accuracy. Calibrate the rubric and threshold against labeled leads before scaling automatic sending.

Default model: pinned `gpt-4.1-mini-2025-04-14`. Change it to a model available in your account that supports Chat Completions structured outputs and the request parameters, then retest. Claude/Gemini and alternate validators need their request/response adapter changed; they are not drop-in credential replacements.

## HubSpot behavior

The template uses custom dropdown properties rather than a generic tag field:

- `revops_email_validation`: Email: Validated / Failed / Risky / Review.
- `revops_lead_priority`: Lead: Hot / Warm / Cold / Review.
- `revops_response_status`: Pending / Draft Only / Human Review / Email Blocked / Suppressed / Email Sent / CRM Log Pending.
- Numeric score, score explanation, latest submission ID, draft subject/body, provider email ID and sent timestamp.
- `revops_do_not_contact`: explicit manual suppression control.

Contact lookup uses the direct email endpoint. Existing names, company, phone and job title are preserved; only blank fields are filled. Existing lifecycle stage and owner are preserved. New contacts start as lifecycle `lead`. A create conflict gets one re-read-and-patch attempt. Malformed email input is rejected before it reaches HubSpot; syntactically valid but failed validation is recorded for review/blocking.

Rep availability is controlled by `reps[].available`; it is not a live calendar/presence integration. Existing owners outside the roster remain assigned, with automatic sending held for review. Contact processing is serialized within this integration. HubSpot itself does not provide a transactional owner compare-and-set here, so an external owner edit can still race the initial assignment; the fresh pre-send owner check stops sending if the owner differs at that check.

Immediately before sending, the worker rechecks the contact's email, owner, lifecycle, `hs_email_optout`, `hs_email_bad_address`, hard-bounce reason, `revops_do_not_contact`, validation and response status. To cancel a waiting reply, set `revops_do_not_contact=true` or change `revops_response_status` from Pending. A rep's normal mailbox activity is **not** automatically monitored; set this field from a separate reply/manual-engagement workflow if you need that cancellation behavior.

Checks cover the listed CRM flags, not every HubSpot subscription type or every external suppression source. Connect the appropriate subscription and provider suppression data before adapting this first-response flow for broader marketing campaigns.

## Execution and sending reliability

New accepted entries start the worker immediately. A five-minute schedule claims one queued job per tick as backup. Each execution processes one job. PostgreSQL locks the queue row, guards the email throughout processing, and advances the owner rotation atomically. A second submission for the same email waits until the first finishes. If many jobs accumulate after an outage, temporarily shorten the queue sweep or start additional worker executions; the database controls competing claims.

The five-minute worker and operations schedules each run 288 times per day. Account for idle scheduled executions in your n8n plan; for a low-volume deployment you can lengthen both schedules to 15 minutes after setting your desired recovery/alert latency. No promise of zero execution overhead is made.

The exact provider request and idempotency key are saved before sending. A database send reservation prevents concurrent or repeated sends, and a 24-hour per-email cooldown suppresses subsequent first-response emails. Resend retries use the same saved request/key and are bounded to a short interval. Resend retains idempotency keys for 24 hours, so that feature is not indefinite duplicate protection.

If a send result is uncertain, the send reservation remains unresolved. Later sends to that email are blocked until an operator reconciles it. If provider acceptance is saved but HubSpot logging fails, the error workflow records `log_pending` in PostgreSQL. This does not automatically update HubSpot to CRM Log Pending while its API is failing. No automatic full-workflow replay or exactly-once CRM activity guarantee is claimed.

Configuration is snapshotted per execution. Changing the global sending switches or deactivating a workflow does not reliably cancel already-running/waiting executions. Use the CRM suppression control and cancel pending n8n executions when an immediate stop is required. A small race remains between the final external CRM read and the send request.

Workflow 04 records failures and detects queued backlogs, stale work, review holds, failed Hot alerts and logging holds. Its scheduled incident notifications are once per job/state. Enable operations Slack for active notifications; otherwise use n8n executions and the incident table. Error Trigger is for automatic failures; manually run tests may require inspection in n8n and the next stalled-job scan. Database outages can also prevent recording/alerting, so production infrastructure monitoring remains outside this template.

## Recovery without duplicate emails

Start by inspecting the failed n8n execution and the queue row:

```sql
SELECT job_id,status,execution_id,email,owner_id,contact_id,send_attempt_at,
       provider_email_id,crm_email_id,reason,last_error,alert_error,draft
FROM revops.jobs
WHERE job_id = 'YOUR_SUBMISSION_UUID'::uuid;
```

1. **Failure before any send attempt:** fix the cause and ensure the old execution cannot resume. In a transaction, lock the job, verify `send_attempt_at` and `provider_email_id` are null and no send guard references it, delete its contact guard if present, then set its status to `queued`, clear `execution_id`, and update `updated_at`. Start a worker or wait for the sweep. Do not do this for a paused execution that is still running.
2. **Send attempted, provider ID absent:** look up the exact request in Resend using provider logs and the stored idempotency key/job tag. Keep the unresolved send guard until the outcome is established. Within the provider's 24-hour window, only the identical saved request and key can be retried safely under that provider guarantee. After the window, do not infer that the provider never sent it.
3. **Provider accepted, CRM log missing:** reconcile the HubSpot activity timeline first because its create request may have succeeded despite a lost response. Create only a confirmed missing activity using the exact stored subject/body and original acceptance time from provider records; persist its ID, update the contact status, then finish the job. Do not resend the email or rerun the entire worker.

This package intentionally leaves ambiguous recovery to an operator. It includes no automated resend/reconciliation endpoint.

Keep execution-data and database retention aligned with your handling of prospect data. Queue/guard history is part of replay protection; deleting it changes those guarantees. A normal production deployment should also ingest delivery/bounce/complaint events and synchronize suppression flags. Those event workflows are outside this package.

## Sandbox acceptance checks

| Case | Expected result |
|---|---|
| Valid Hot prospect, available rep, dry run | Assigned owner, high score, saved draft; no email. Optional Hot alert if enabled. |
| Same UUID and identical payload twice | One queue entry and one worker dispatch; second intake is an accepted duplicate. |
| Same UUID, different data | 409, no new lead processing. |
| Invalid / unknown / catch-all / disposable email | CRM hygiene hold; no send. |
| Model timeout / refusal / malformed output | Review; no send. |
| Unknown or unavailable existing owner | Preserve owner; review; no send. |
| Cold lead | CRM score/owner, no sales alert or automatic email. |
| Warm eligible lead, live sends enabled | 10–15 minute delay, then fresh suppression/ownership check. |
| Set Do Not Contact during delay | Suppressed before send. |
| Change owner or response status during delay | Suppressed before send. |
| Repeat inquiry with a new UUID inside 24h | CRM records inquiry; sending cooldown suppresses another first response. |
| Simultaneous new unowned contacts | Atomic rotation; no duplicate queue claims. Verify with concurrent requests in your sandbox. |
| Provider acceptance followed by HubSpot failure | Provider ID retained; `log_pending` in PostgreSQL; reconcile logging without resending. |

## Validation performed here

64 local checks passed: JSON/graph integrity, all embedded Code node JavaScript and expression syntax, input sanitization, hygiene cases, CRM request logic, scoring bounds, deterministic sending gates, late suppression/owner/status changes, exact email activity content and stored provider request/header references. See `validation-report.json`.

Not verified here: live n8n import/execution, PostgreSQL function execution, concurrent database behavior under load, account permissions/credentials, live API responses, actual delivery or model accuracy on your service catalog. No external account was changed and no email was sent.

## API references

- [n8n HTTP Request](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/) and [Postgres query parameters](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.postgres/).
- [n8n asynchronous sub-workflow execution](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflow/).
- [HubSpot contacts](https://developers.hubspot.com/docs/api-reference/legacy/crm/objects/contacts/guide) and [email activity logging](https://developers.hubspot.com/docs/api-reference/legacy/crm/activities/emails/guide).
- [ZeroBounce validation statuses and endpoint](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails).
- [OpenAI structured outputs](https://developers.openai.com/api/docs/guides/structured-outputs) and [configured model](https://developers.openai.com/api/docs/models/gpt-4.1-mini).
- [Resend send API](https://resend.com/docs/api-reference/emails/send-email) and [24-hour idempotency keys](https://resend.com/docs/dashboard/emails/idempotency-keys).

