TL;DR
Lead scoring is a number or label (cold / warm / hot) that tells sales who to call first — based on form data, on-site behavior, and campaign source. A contact form in Next.js is not just mailto: — it is a Server Action saving the lead to PostgreSQL on Branchly Cloud, a CRM webhook, and a generate_lead event in GA4. The sales pipeline maps statuses (new → qualified → proposal → won) to automation: owner assignment, CRM task, Slack when score > 70. Below: data model, scoring rules, form integration, and anti-spam.
Who is this for
- B2B companies getting dozens of form inquiries monthly and losing hot leads in a shared "info@" inbox
- Sales teams without clear "who is priority" criteria — every lead looks the same
- Developers implementing forms in Next.js 15 (Server Actions) with database and CRM persistence
- Marketing wanting to connect UTM / GA4 with lead quality, not just submit count
- Site owners after a DevStudio launch — extending the form with scoring without rewriting the whole site
Keyword
lead scoring contact form, sales pipeline crm, nextjs contact form, server actions lead, crm webhook integration, b2b lead points
From form to deal — process map
Typical flow in a corporate site project:
Site (UTM in session) → Form submit → Server-side validation
→ Save Lead in Branchly (PostgreSQL)
→ Compute score + segment
→ CRM webhook (HubSpot / Pipedrive / custom)
→ Notification (email / Slack) if hot
→ Pipeline: statuses and sales tasksThe form is the system entry point, not the end. Without database and CRM persistence, every submit is an email in a shared inbox — unmeasurable in reports and easy to lose.
Data model — Lead and Score
Prisma on Branchly (illustrative fragment):
model Lead {
id String @id @default(cuid())
email String
company String?
budget String?
message String?
utmSource String?
utmCampaign String?
score Int @default(0)
segment LeadSegment @default(COLD)
status LeadStatus @default(NEW)
createdAt DateTime @default(now())
}
enum LeadSegment {
COLD
WARM
HOT
}
enum LeadStatus {
NEW
CONTACTED
QUALIFIED
PROPOSAL
WON
LOST
}Score can be a weighted sum of fields and events. Set segment thresholds: 0–39 cold, 40–69 warm, 70+ hot — calibrate quarterly from conversion to won.
Lead scoring rules — what to score
| Signal | Example weight | Rationale |
|---|---|---|
| Budget "> 50k" | +30 | Investment readiness |
| Company filled (not gmail) | +20 | B2B vs consumer |
utm_medium=cpc |
+15 | Paid traffic intent |
Session pages /pricing, /case-studies |
+10 each | Research phase |
| Message > 200 chars | +10 | Specific need |
| Free email (gmail, etc.) | −15 | Lower B2B value |
| Missing phone when required | −10 | Harder to reach |
Keep rules in code (TypeScript) or a ScoringRule table — the latter lets marketing change weights without deploy but needs an admin UI.
Example scoring function:
function computeLeadScore(input: LeadInput, sessionPages: string[]): number {
let score = 0;
if (input.company && !isFreeEmail(input.email)) score += 20;
if (input.budget === '50000+') score += 30;
if (input.utmMedium === 'cpc') score += 15;
if (sessionPages.includes('/pricing')) score += 10;
if ((input.message?.length ?? 0) > 200) score += 10;
if (isFreeEmail(input.email)) score -= 15;
return Math.max(0, Math.min(100, score));
}Next.js form — Server Action end-to-end
Server Action validates (Zod), scores, saves, sends webhook:
'use server';
import { z } from 'zod';
import { prisma } from '@/lib/prisma';
import { computeLeadScore } from '@/lib/scoring';
const schema = z.object({
email: z.string().email(),
company: z.string().optional(),
budget: z.enum(['unknown', '10000', '50000+']).optional(),
message: z.string().max(5000),
utmSource: z.string().optional(),
utmMedium: z.string().optional(),
});
export async function submitContactForm(formData: FormData) {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { ok: false, errors: parsed.error.flatten() };
const score = computeLeadScore(parsed.data, []);
const segment = score >= 70 ? 'HOT' : score >= 40 ? 'WARM' : 'COLD';
const lead = await prisma.lead.create({
data: { ...parsed.data, score, segment: segment as any, status: 'NEW' },
});
await fetch(process.env.CRM_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ leadId: lead.id, score, segment }),
});
return { ok: true, leadId: lead.id };
}The client form passes UTMs from sessionStorage or hidden fields set after page_view. Hosting on DevStudioIT Cloud provides stable Server Actions runtime and env with CRM_WEBHOOK_URL / DATABASE_URL to Branchly.
Sales pipeline in CRM
The pipeline is Kanban columns matching LeadStatus. Automations:
| Status | Automatic action |
|---|---|
| NEW + HOT | Assign senior AE, Slack within 15 min |
| NEW + COLD | Nurturing email, no day-zero call |
| CONTACTED | Task "follow up in 3 days" |
| QUALIFIED | Create deal, estimate value |
| PROPOSAL | Reminder after 7 days silence |
| WON / LOST | Close, optional GA4 offline conversion sync |
CRM receives webhook with score and segment — map to custom fields and "Hot leads this week" lists. One segment definition company-wide — otherwise marketing and sales speak different languages.
Anti-spam, GDPR, and data quality
- Honeypot + rate limit (IP / fingerprint) — less noise in scoring
- Double opt-in for newsletter vs single submit for quote request — different consents in privacy policy
- Consent for processing and sales contact — checkbox before submit
- Lead storage logs in EU (Branchly) — audit who accessed data
Bot-driven scores lower quality — filter leads with same IP > 5/h.
GA4 and marketing feedback loop
Event generate_lead with lead_score, lead_segment (custom dimensions) compares campaigns by average score, not just submit count. A cheaper CPC campaign with lower avg score may be worse than an expensive one delivering hot leads.
FAQ
How many scoring rules at start?
Five to seven signals is enough. Over-engineered day-one models — nobody validates weights. After 90 days compare hot lead scores with won conversion and adjust thresholds.
Does lead scoring replace qualification calls?
No. Score prioritizes the queue; BANT/MEDDIC qualification stays with sales. Auto-rejecting cold leads without company policy wastes long-tail opportunity.
HubSpot vs own database on Branchly?
Small companies: form → Branchly + webhook to HubSpot free. Larger: lead in PostgreSQL as source of truth, bidirectional CRM sync. Next.js Server Action fits both — consistent lead ID matters.
How to test pipeline without spamming CRM?
Staging on Branchly + CRM sandbox (see staging with Branchly). Test submit with [TEST] prefix in company.
Want a form that delivers leads to sales?
- Contact us — we design form, scoring, CRM, and hosting in one rollout
- Next.js Server Actions — contact forms — technical submit layer
- Business websites — corporate sites with form and analytics from day one
About the author
We build fast websites, web/mobile apps, AI chatbots and hosting setups — with a focus on SEO and conversion.
Recommended links
From theory to production — Branchly, our hosting stack and shipped work.
