Skip to content

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"
  }
}