{
  "name": "Inbound Lead Responder",
  "nodes": [
    {
      "parameters": {
        "content": "## Inbound Lead Responder\n\n**What this does:**\nReceives inbound leads via webhook, classifies them with an LLM (OpenRouter / owl-alpha), and drafts personalized replies for high-value categories.\n\n**Flow:**\n1. Webhook receives POST /inbound-lead\n2. Normalize Lead extracts fields (with sample-data fallback)\n3. Classify Lead \u2192 category + urgency_score + intent_summary\n4. Parse Classification (try/catch JSON)\n5. Route by Category:\n   - spam / support \u2192 straight to Ack\n   - hot_lead / partnership \u2192 Draft Reply \u2192 Format \u2192 Gmail \u2192 Slack \u2192 Ack\n6. Acknowledge Webhook returns JSON status\n\n**Required env / creds:**\n- OpenRouter API (httpHeaderAuth)\n- Gmail OAuth2\n- SLACK_WEBHOOK_URL env var",
        "height": 520,
        "width": 460
      },
      "id": "sticky-doc-1",
      "name": "Workflow Docs",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -200,
        -40
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "inbound-lead",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        340,
        320
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "field-name",
              "name": "name",
              "value": "={{ $json.body?.name || \"Jordan Maxwell\" }}",
              "type": "string"
            },
            {
              "id": "field-email",
              "name": "email",
              "value": "={{ $json.body?.email || \"jordan@acmewidgets.com\" }}",
              "type": "string"
            },
            {
              "id": "field-message",
              "name": "message",
              "value": "={{ $json.body?.message || \"Hi \u2014 I run a 12-person agency and we're hitting a ceiling on growth. Our flywheel keeps stalling and we're losing roughly $40k MRR to churn each quarter. Curious if your team works with shops our size on retention systems. Open to a quick call next week.\" }}",
              "type": "string"
            },
            {
              "id": "field-source",
              "name": "source",
              "value": "={{ $json.body?.source || \"contact-form\" }}",
              "type": "string"
            },
            {
              "id": "field-company",
              "name": "company",
              "value": "={{ $json.body?.company || \"Acme Widgets\" }}",
              "type": "string"
            },
            {
              "id": "field-received-at",
              "name": "received_at",
              "value": "={{ $now.toISO() }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "normalize-1",
      "name": "Normalize Lead",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        560,
        320
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://openrouter.ai/api/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "HTTP-Referer",
              "value": "https://github.com/BiG-Zach/inbound-lead-responder-n8n"
            },
            {
              "name": "X-Title",
              "value": "Inbound Lead Responder"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"openrouter/owl-alpha\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are a lead-classification assistant. Read the inbound message and return STRICT JSON only, no prose, no markdown fences. Schema: {\\\"category\\\": one of [\\\"hot_lead\\\", \\\"cold_lead\\\", \\\"support\\\", \\\"spam\\\", \\\"partnership\\\"], \\\"urgency_score\\\": integer 1-10, \\\"intent_summary\\\": short string under 200 chars}. hot_lead = clear buying intent or budget mentioned. cold_lead = vague curiosity. support = existing customer asking for help. spam = promotional/irrelevant. partnership = vendor/affiliate/collab outreach.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {{ JSON.stringify(\"Name: \" + $json.name + \"\\nEmail: \" + $json.email + \"\\nCompany: \" + $json.company + \"\\nSource: \" + $json.source + \"\\nMessage:\\n\" + $json.message) }}\n    }\n  ],\n  \"temperature\": 0.2,\n  \"response_format\": {\"type\": \"json_object\"}\n}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "classify-1",
      "name": "Classify Lead",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        780,
        320
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Parse the OpenRouter classification response with a safe fallback.\nconst lead = $('Normalize Lead').item.json;\nconst raw = $input.item.json;\n\nlet category = 'cold_lead';\nlet urgency_score = 5;\nlet intent_summary = 'Unclassified \u2014 parser fallback.';\n\ntry {\n  const content = raw?.choices?.[0]?.message?.content;\n  if (!content) throw new Error('No content in LLM response');\n\n  // Strip possible markdown fencing just in case.\n  const cleaned = String(content).replace(/^```(?:json)?\\s*/i, '').replace(/```\\s*$/,'').trim();\n  const parsed = JSON.parse(cleaned);\n\n  if (parsed.category) category = String(parsed.category).toLowerCase().trim();\n  if (Number.isFinite(parsed.urgency_score)) {\n    urgency_score = Math.min(10, Math.max(1, Math.round(parsed.urgency_score)));\n  }\n  if (parsed.intent_summary) intent_summary = String(parsed.intent_summary).slice(0, 240);\n\n  const allowed = ['hot_lead','cold_lead','support','spam','partnership'];\n  if (!allowed.includes(category)) category = 'cold_lead';\n} catch (err) {\n  intent_summary = 'Classifier parse error: ' + (err?.message || 'unknown') + '. Defaulting to cold_lead.';\n}\n\nreturn [{\n  json: {\n    ...lead,\n    category,\n    urgency_score,\n    intent_summary\n  }\n}];"
      },
      "id": "parse-1",
      "name": "Parse Classification",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1000,
        320
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cond-spam",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "spam",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "spam"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cond-support",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "support",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "outputKey": "support"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cond-hot",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "hot_lead",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "cond-partnership",
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "partnership",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "or"
              },
              "outputKey": "high_priority"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "fallback"
        }
      },
      "id": "switch-1",
      "name": "Route by Category",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1220,
        320
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://openrouter.ai/api/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "HTTP-Referer",
              "value": "https://github.com/BiG-Zach/inbound-lead-responder-n8n"
            },
            {
              "name": "X-Title",
              "value": "Inbound Lead Responder"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"openrouter/owl-alpha\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You draft warm, personalized replies to inbound leads. Output ONLY the reply body \u2014 no subject line, no preamble, no quoted message. Constraints: exactly 3 sentences. Reference one specific detail from the lead's message. Friendly but not gushing. End with a clear next step. Sign off with the literal string [Your Name] on its own line.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {{ JSON.stringify(\"Lead name: \" + $json.name + \"\\nCompany: \" + $json.company + \"\\nCategory: \" + $json.category + \"\\nUrgency: \" + $json.urgency_score + \"/10\\nIntent: \" + $json.intent_summary + \"\\n\\nOriginal message:\\n\" + $json.message) }}\n    }\n  ],\n  \"temperature\": 0.7,\n  \"max_tokens\": 400\n}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "draft-1",
      "name": "Draft High-Priority Reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1480,
        220
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Merge the drafted reply with the lead context so downstream nodes have everything.\nconst lead = $('Parse Classification').item.json;\nconst draft = $input.item.json;\n\nlet body = '';\ntry {\n  body = draft?.choices?.[0]?.message?.content?.trim() || '';\n} catch (e) {\n  body = '';\n}\n\nif (!body) {\n  body = `Hi ${lead.name || 'there'},\\n\\nThanks for reaching out \u2014 we'd love to learn more about what you're working on. Mind if I send over a couple of times for a quick call this week?\\n\\n[Your Name]`;\n}\n\nconst source = lead.source || 'your inquiry';\nconst subject = `Re: your message about ${source}`;\n\nreturn [{\n  json: {\n    ...lead,\n    reply_subject: subject,\n    reply_body: body,\n    to_email: lead.email,\n    to_name: lead.name\n  }\n}];"
      },
      "id": "format-1",
      "name": "Format Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1700,
        220
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.to_email }}",
        "subject": "={{ $json.reply_subject }}",
        "emailType": "text",
        "message": "={{ $json.reply_body }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "gmail-1",
      "name": "Send Email Reply",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1920,
        220
      ],
      "continueOnFail": true,
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.SLACK_WEBHOOK_URL }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"text\": {{ JSON.stringify(\"\ud83d\udd25 New \" + $json.category + \" (urgency \" + $json.urgency_score + \"/10) from \" + $json.name + \" <\" + $json.email + \">\\n*Intent:* \" + $json.intent_summary + \"\\n*Draft sent:* \" + $json.reply_subject) }}\n}",
        "options": {
          "timeout": 15000
        }
      },
      "id": "slack-1",
      "name": "Notify Slack",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2140,
        220
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"status\": \"received\",\n  \"category\": \"{{ $json.category }}\",\n  \"urgency\": {{ $json.urgency_score }}\n}",
        "options": {}
      },
      "id": "respond-1",
      "name": "Acknowledge Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2360,
        320
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Normalize Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Lead": {
      "main": [
        [
          {
            "node": "Classify Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Lead": {
      "main": [
        [
          {
            "node": "Parse Classification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Classification": {
      "main": [
        [
          {
            "node": "Route by Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Category": {
      "main": [
        [
          {
            "node": "Acknowledge Webhook",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Acknowledge Webhook",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Draft High-Priority Reply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Acknowledge Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft High-Priority Reply": {
      "main": [
        [
          {
            "node": "Format Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Reply": {
      "main": [
        [
          {
            "node": "Send Email Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email Reply": {
      "main": [
        [
          {
            "node": "Notify Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Slack": {
      "main": [
        [
          {
            "node": "Acknowledge Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}