← All projects
LIVE DEMO

Lead scoring and CRM sync

One n8n workflow that scores a web form lead, upserts it in HubSpot and only announces the hot ones

STACKn8n 2.31.6 / PostgreSQL 16.14 / HubSpot CRM API / Slack Web API / OpenRouter / Node.js 22 / Docker
CONTEXTSpec project (Upwork brief)
DATE2026-07
Open recruiter demo lead-scoring-hubspot-sync-demo.aaron.cv
EVIDENCE, NOT CLAIMS

Eleven cases you can trigger yourself, plus your own lead

The demo posts to the same webhook as the source repository, with the same workflow export, the same criteria file and the same provider. Every scenario states what it expects before it runs, then shows the actual result and the PostgreSQL rows behind it. The console re-derives the class from the stored per-criterion answers and checks it against what the workflow wrote, so you can see that the class came from the rule table. Scoring is probabilistic and is labelled as such; the policy checks are not. HubSpot and Slack are local stand-ins that speak the real request and response contracts, and that is stated on the page.

10 of 10 fixtures classified as expected Real provider calls against the placeholder criteria, rule version example-1.
10 of 10 schema valid on the first answer 0 repaired, 0 leads on the fallback class.
413 ms average scoring latency Slowest of the ten was 631 ms. Full webhook round trip on the Hot lead was 3585 ms, including both API calls.
1013 / 163 input and output tokens per lead Measured over ten leads: 10128 in, 1628 out.
64 of 64 checks in the live acceptance run Six scenarios against api.hubapi.com and slack.com, every message id confirmed with chat.getPermalink.
76 contract and error path tests passed 0 failed, plus 25 schema checks and 14 workflow export checks.
168 of 168 policy checks passed on the deployed demo Eleven scenarios plus a custom lead, run against the deployed stack on 2026-07-27. 19 of 19 model answer checks matched on the same sweep. Nothing failed.
$0.00 measured cost per lead The measured route is a free development route, so the provider reported no cost. The token counts are real, so a paid endpoint costs $0.30 per thousand leads at the open weight ceiling this build budgets for.
Not measured production accuracy, availability, provider region and retention This build has never seen production traffic. The fixtures carry the class they were written to get, which says the rules hold together, not that they match how a sales team qualifies.
01

The model judges, code decides the class

The prompt never asks for Hot, Warm or Cold. Four criteria with weights and thresholds live in a config file; the class is arithmetic over the answers the model gave.

VERIFY: Run any scenario and open the scoring breakdown, where the console re-derives the class from the stored answers
02

A bad answer never becomes a guess

A provider that stays down after four attempts, or an answer that does not fit the criteria file, falls back to Warm with needs_review. The lead still reaches the CRM, flagged for a human.

VERIFY: Run Provider failure and Answer the schema rejects
03

Duplicates cost nothing

The submission key is claimed under a unique constraint before the provider is called. A redelivery returns the stored result with one lead row, one scoring event and one contact.

VERIFY: Run Duplicate delivery
04

Retries are bounded and recorded

Four attempts with 2, 4 and 8 second waits on the scoring call and on a CRM 5xx. A CRM 4xx is never repeated, because a rejected payload does not get better on the second try.

VERIFY: Run Provider failure and CRM outage
05

Nothing is dropped silently

A malformed submission answers HTTP 200 so the form provider stops redelivering, and lands in dead_letter with the full payload. The ops alert names the stage and the reason and is throttled to one per key per fifteen minutes.

VERIFY: Run Malformed submission, then run it again inside the throttle window
06

Only the class that earns it gets a notification

The chat message goes out for Hot leads only, and only while the operator killswitch in the database is on. A skipped notification is logged with its reason.

VERIFY: Run Routine lead and Notifications switched off
07

Free text is data, never instructions

Everything the lead typed sits between two markers in the prompt. An injection attempt is flagged on the lead, named in the CRM reason field and kept verbatim in the raw payload table.

VERIFY: Run Prompt injection in the message
08

Every decision is auditable

Raw payload, normalized lead, one scoring event per attempt with tokens and latency, one sync row per outbound system, dead letters and alerts are read back from PostgreSQL after each run, next to the n8n execution id.

VERIFY: Open the PostgreSQL evidence panel under any result
TRY THESE CASES
Routine leadWarm, contact created with all five scored properties, no notification
Urgent lead with approved budgetHot, contact created, one message in the Hot channel
Ambiguous submissionLower reported confidence, class still from the rule table, no notification
After hours Hot leadAnnounced at 02:40 anyway, and the daily digest called on its own webhook
Duplicate deliverySecond call reported as duplicate, one lead row, one scoring event, one contact
Provider failureFour attempts with backoff, fallback to Warm with needs_review, lead still synced
Answer the schema rejectsNo retry, fallback class, rejection reason stored
Malformed submissionHTTP 200, dead letter with the full payload, throttled ops alert, nothing in the CRM
CRM outageFour upsert attempts, dead letter with the request, ops alert, lead kept
Prompt injection in the messageClass unchanged, attempt flagged on the lead and named in the CRM reason
Notifications switched offHot lead synced, notification logged as skipped with the killswitch as the reason
Your own leadSame criteria and same guardrails, on a synthetic address you write yourself
RUNTIME PATH
  1. The public console validates a synthetic submission and posts it to the internal n8n webhook
  2. The raw payload is written to PostgreSQL before anything is judged
  3. The payload is normalized into one lead shape; a rejection carries a reason code and goes straight to dead_letter
  4. The submission key is claimed under a unique constraint, so a redelivery stops before the provider call
  5. Enrichment adds the company domain, whether the mailbox is free, and what this address and domain sent before
  6. One provider call scores four criteria under a strict JSON schema, with four attempts and 2, 4 and 8 second waits
  7. The answer is validated against the schema and the criteria options, then code turns it into points and a class
  8. The contact is upserted in HubSpot on email: search, then create or update, with five scored properties
  9. A Hot lead is announced in the chat channel; any other class is logged as a skipped notification with its reason
  10. Scoring event and sync rows are written, and a separate error workflow catches what the main path did not
DATA BOUNDARY

Synthetic leads only, and no real CRM or workspace behind the demo.

  • Synthetic leads only. The custom form accepts addresses on the reserved example.com domain and nothing else.
  • Only the normalized lead fields and the criteria text reach the model provider. Nothing else leaves the host.
  • The provider key lives in a gateway container. The browser, n8n and both stand-ins never receive it.
  • There is no HubSpot account and no chat workspace behind the demo. Both stand-ins speak the real contracts and hold what the workflow sends, so no contact is created and no message reaches a person.
  • n8n, PostgreSQL and both stand-ins stay on the internal Docker network; the console is the single public surface.
  • Provider processing region and retention are not configured or measured for this reference implementation.
  • The demo database and both stand-ins reset to their seeded state every night at 04:00 CET.

Two things go wrong with lead scoring built on a language model. The model invents a class, and a bad answer quietly becomes a decision. This build takes both off the table. The prompt never asks for Hot, Warm or Cold. The model answers four separate questions about budget, company size, timeline and decision authority, each one under a strict JSON schema with a fixed set of allowed values and a reason. The points and the class come from a weight and threshold table in the repository, so the same answers always produce the same class and a rule change is a config edit rather than a prompt rewrite.

The submission is written to PostgreSQL before anything is judged, and the provider submission id is claimed under a unique constraint before the model is called. A form provider that redelivers after a timeout hits that constraint instead of paying for a second scoring call, and gets the stored result back. Everything the lead typed goes into the prompt as data between two markers, with the markers stripped from the text, so an instruction in the free text is recorded as an injection attempt rather than obeyed.

The failure paths got as much attention as the happy path. The scoring call is tried four times with 2, 4 and 8 second waits; after that the lead falls back to Warm with needs_review set and still reaches the CRM, flagged for a human, because a lead you cannot score is worse lost than unscored. An answer that does not fit the criteria file takes the same fallback without a retry, since the provider did answer. A CRM 5xx is repeated with the same backoff, a 4xx is not, because repeating a rejected payload cannot fix it, and a permanent failure writes the full request to a dead letter table and raises an ops alert that is throttled to one per alert key per fifteen minutes.

This is a reference project built on a public brief, with synthetic submissions and my own test accounts. There is no client. The live acceptance run went against the real HubSpot and Slack APIs on a developer test portal and a test workspace, and the numbers below come from that run and from a scoring measurement over ten fixtures, not from an estimate.

What it does

The proof console for the lead scoring workflow
The public proof console. Every card states its expected behaviour before you run it.
Eleven scenario cards covering happy paths and failures
Eleven scenarios: the happy paths, the guardrails, the injected failures and the operator controls.
One lead scored, with the per-criterion verdict and database rows
One run: expected versus actual, the per-criterion verdict, the class re-derived from the rule table, and the PostgreSQL rows behind it.
The published lead scoring workflow in n8n
The published workflow: intake, idempotent claim, scoring with retries, CRM upsert, notification and dead letter.

Want something like this for your business?

Next one
yours?

Send a short description of what eats your time. You will usually see a working prototype before you have paid anything.

© 2026 aaron.cv · Torhout, Belgium (CET) aaronsinnaeve5@gmail.com Upwork