Stale Deal Nudger
Module 15 · business · intermediate
starter.json
In n8n, copy this file and paste it onto the canvas, or use Import from File.
{
"name": "15 - Stale Deal Nudger (starter)",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1-5"
}
]
}
},
"id": "m15-schedule",
"name": "Weekdays 8 AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
0
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "deals/stale-check",
"responseMode": "responseNode",
"options": {}
},
"id": "m15-webhook",
"name": "Stale Check API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"webhookId": "course-15-stale-check"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "s1",
"name": "stale_days",
"value": "={{ { appointmentscheduled: 3, qualifiedtobuy: 7, presentationscheduled: 5, decisionmakerboughtin: 4, contractsent: 3, default: 7 } }}",
"type": "object"
},
{
"id": "s2",
"name": "closed_stages",
"value": "={{ ['closedwon', 'closedlost'] }}",
"type": "array"
},
{
"id": "s3",
"name": "owners",
"value": "={{ { '12345678': { name: 'Alex Rivera' }, '23456789': { name: 'Priya Shah' } } }}",
"type": "object"
},
{
"id": "s4",
"name": "crm_deal_url",
"value": "https://app.hubspot.com/contacts/YOUR_PORTAL_ID/deal/{id}",
"type": "string"
},
{
"id": "s5",
"name": "use_ai",
"value": false,
"type": "boolean"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "m15-settings",
"name": "Settings (edit me)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
220,
150
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a1",
"leftValue": "={{ $('Stale Check API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m15-source",
"name": "Deals from API?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
440,
150
]
},
{
"parameters": {
"authentication": "appToken",
"resource": "deal",
"operation": "getAll",
"returnAll": true,
"filters": {
"properties": [
"dealname",
"dealstage",
"amount",
"hubspot_owner_id",
"notes_last_updated",
"hs_lastmodifieddate"
]
}
},
"id": "m15-hubspot",
"name": "Get HubSpot Deals",
"type": "n8n-nodes-base.hubspot",
"typeVersion": 2.1,
"position": [
660,
40
]
},
{
"parameters": {
"jsCode": "// HubSpot deals → the common deal shape used by the rest of the workflow.\n// The HubSpot node may return properties as { value: … } (legacy API) or plain values; handle both.\nconst s = $('Settings (edit me)').first().json;\nconst val = (p, k) => (p?.[k] && typeof p[k] === 'object' ? p[k].value : p?.[k]) ?? null;\n\nreturn $input.all()\n .filter((i) => i.json.dealId || i.json.id)\n .map(({ json: d }) => {\n const p = d.properties ?? {};\n const lastActivity = val(p, 'notes_last_updated') ?? val(p, 'hs_lastmodifieddate');\n const ownerId = String(val(p, 'hubspot_owner_id') ?? '');\n return {\n json: {\n id: String(d.dealId ?? d.id),\n name: val(p, 'dealname') ?? '(unnamed deal)',\n stage: String(val(p, 'dealstage') ?? 'unknown').toLowerCase(),\n amount: Number(val(p, 'amount') ?? 0),\n owner: s.owners[ownerId]?.name ?? (ownerId ? `Owner ${ownerId}` : 'Unassigned'),\n contact: '',\n last_activity: lastActivity ? new Date(Number(lastActivity) || lastActivity).toISOString() : null,\n url: s.crm_deal_url ? s.crm_deal_url.replace('{id}', String(d.dealId ?? d.id)) : '',\n },\n };\n });\n"
},
"id": "m15-map",
"name": "Map HubSpot Deals",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
40
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "v1",
"leftValue": "={{ Array.isArray($json.body?.deals) }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m15-valid",
"name": "Has deals[]?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
660,
260
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { error: 'POST {\"deals\": [{\"id\",\"name\",\"stage\",\"amount\",\"owner\",\"last_activity\"}], \"now\"?: ISO date, \"ai\"?: true}' } }}",
"options": {
"responseCode": 400
}
},
"id": "m15-400",
"name": "Respond Bad Input",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
880,
400
]
},
{
"parameters": {
"jsCode": "// API path: deals posted in the body (any CRM, or a test). Same common shape as the HubSpot mapper.\nconst body = $input.first().json.body ?? {};\nreturn (body.deals ?? []).map((d, i) => ({\n json: {\n id: String(d.id ?? `deal-${i + 1}`),\n name: String(d.name ?? '(unnamed deal)'),\n stage: String(d.stage ?? 'unknown').toLowerCase(),\n amount: Number(d.amount ?? 0),\n owner: String(d.owner ?? 'Unassigned'),\n contact: String(d.contact ?? ''),\n last_activity: d.last_activity ?? null,\n url: String(d.url ?? ''),\n },\n}));\n"
},
"id": "m15-from-req",
"name": "Deals from Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
220
],
"alwaysOutputData": false
},
{
"parameters": {
"content": "## Your task\nBoth sources already produce the same deal shape (id, name, stage, amount, owner, contact, last_activity, url).\n1. **Find Stale Deals** (Code): skip closed stages; idle days vs the per-stage limit in Settings; urgency = overdue days × size factor; sort.\n2. If none: respond `{stale_count: 0}` (API) or stop (schedule).\n3. Draft a follow-up per deal: a template (Code) or AI (Basic LLM Chain + structured parser) when `ai: true`.\n4. **Group by Owner**: one digest message per salesperson.\n5. API → respond with all owners; schedule → send each owner their nudge.\n\nTest: `./test/test.sh` (uses assets/sample-deals.json)",
"height": 260,
"width": 420
},
"id": "starter-task-note",
"name": "Your Task",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
0,
-340
]
}
],
"connections": {
"Weekdays 8 AM": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Stale Check API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Settings (edit me)": {
"main": [
[
{
"node": "Deals from API?",
"type": "main",
"index": 0
}
]
]
},
"Deals from API?": {
"main": [
[
{
"node": "Has deals[]?",
"type": "main",
"index": 0
}
],
[
{
"node": "Get HubSpot Deals",
"type": "main",
"index": 0
}
]
]
},
"Get HubSpot Deals": {
"main": [
[
{
"node": "Map HubSpot Deals",
"type": "main",
"index": 0
}
]
]
},
"Map HubSpot Deals": {
"main": [
[]
]
},
"Has deals[]?": {
"main": [
[
{
"node": "Deals from Request",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Bad Input",
"type": "main",
"index": 0
}
]
]
},
"Deals from Request": {
"main": [
[]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
}
}