Lead scoring lives at the intersection of CRM and Automation. The score is a numeric attribute on the lead record. Automations read it via check_engagement_score, the CRM service exposes endpoints to compute and refresh it, and decay rules make sure stale leads don't sit at the top of the queue forever.
The score model
A lead score is an integer (typically 0–100) stored on lead.data.score. The score combines:
- Demographic fit — fixed points for matching ICP fields (industry, size, geography).
- Behavioral activity — points awarded by recent events (email opens, page visits, link clicks, content downloads, form submits).
- Negative signals — point deductions for unsubscribes, bounces, opt-outs.
- Decay — a time-based decay that subtracts a percentage of the score per period of inactivity.
The weights are configurable per org. The CRM LeadScoringService exposes the calculator; rules are stored as scoring rule records so they can be updated without code.
Recalculate a score
/crm/leads/score/:idJWTRecomputes a single lead's score from current rules and recorded activity. Returns {leadId, score}.
curl -X POST https://appengine.appmint.io/crm/leads/score/$LEAD_ID \
-H "Authorization: Bearer $JWT" -H "orgid: $ORG"
/crm/leads/score/batchJWTBatch recalc — pass an array of lead IDs. Use this nightly via a scheduled_time automation to apply decay.
Use the score in a flow
The check_engagement_score condition reads the score from context.variables.engagementScore, .leadScore, or .contact.leadScore (it tries all three) and compares against a threshold:
{
"id": "score-gate",
"type": "condition",
"config": {
"conditionType": "check_engagement_score",
"scoreThreshold": 75,
"operator": "gte",
"timeframe": "30d",
"trueFlowId": "create-task-for-rep",
"falseFlowId": "stay-in-nurture"
}
}
If no score is found on the context, the condition computes a basic one from available activity (counts of opens, clicks, page visits). For production accuracy, hydrate the score before the condition runs by calling POST /crm/leads/score/:id from an action step or by triggering on data_updated of the lead record.
Decay rules
A decay rule reduces a stale lead's score over time. Configure them as scoring rules of type decay:
{
"type": "decay",
"trigger": "no_activity",
"windowDays": 30,
"reductionPercent": 25,
"minScore": 0
}
Apply decay by scheduling a daily automation:
{
"name": "Daily lead-score decay",
"triggers": [
{ "id": "t1", "type": "scheduled_time", "config": { "schedule": { "type": "cron", "cronExpression": "0 2 * * *" } } }
],
"steps": [
{
"id": "s1",
"type": "action",
"config": {
"actionType": "send_webhook",
"url": "{{baseUrl}}/crm/leads/score/batch",
"method": "POST",
"body": { "all": true, "applyDecay": true }
}
}
]
}
Or call the CRM service directly from a custom action.
Qualification thresholds
Three score bands drive the lead pipeline:
| Band | Score range | Pipeline action |
|---|---|---|
| Cold | 0–39 | Stay in nurture campaigns |
| Warm | 40–74 | Create follow-up task; add to drip |
| Hot | 75+ | Auto-qualify; assign rep; create opportunity |
The qualify_lead action moves a lead into the qualified state:
/crm/leads/qualify/:idJWT{
"id": "auto-qualify",
"type": "action",
"config": {
"actionType": "qualify_lead",
"leadId": "{{lead.id}}",
"notes": "Auto-qualified at score {{lead.score}}"
}
}
The complementary actions are disqualify_lead (POST /crm/leads/disqualify/:id) and convert_lead (POST /crm/leads/convert/:id) — typically wired into a flow after a hot lead closes a deal.
Wiring it together
A complete lead-scoring loop:
- Triggers update the score. A
database_changeautomation ondata_updatedforactivityrecords callsPOST /crm/leads/score/:idto refresh the score whenever a contact does something noteworthy. - A score-gate condition decides what's next.
check_engagement_scorebranches between nurture, follow-up, and qualification. - A daily cron automation applies decay. Schedule a
scheduled_timetrigger at 2am that batch-recalculates scores so inactive leads cool off. - Pipeline moves trigger more flows. A
database_changeautomation onleadupdates can fire whenqualified === trueto create an opportunity, assign a rep, and notify Slack.
The score is just a number. The interesting logic is in the rules that compute it and the conditions that read it. Keep both versioned — when you change weights, automations branch differently overnight.