{
  "name": "JAH · Customer Support + Reception · AI Receptionist",
  "nodes": [
    {
      "parameters": {
        "content": "## AI Receptionist\nA call gets missed. The caller gets a helpful text in 30 seconds.\nBooking asks get your booking link. Emergencies alert you fast.\n\n**5 minute setup**\n1. Open the Config node. It says EDIT ME.\n2. Type your business name and booking link.\n3. Set your open hour and close hour.\n4. Keep testMode true for your first run.\n5. Run the test in the yellow note. Then add real keys.",
        "height": 380,
        "width": 500
      },
      "id": "sticky-header",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [160, 40]
    },
    {
      "parameters": {
        "content": "## Placeholders to fill\nFill these 6 before you go live.\n- YOUR_BUSINESS_NAME: shows in every text.\n- YOUR_BOOKING_LINK: your Calendly or booking page.\n- YOUR_TWILIO_PHONE_NUMBER: the number that sends texts.\n- YOUR_OWNER_MOBILE: gets emergency alerts.\n- YOUR_GOOGLE_SHEET_ID: your call log sheet.\n- OPENAI_API_KEY: add it as an n8n environment variable.\n\nThen swap each gray NoOp node for the real Twilio, email, and Google Sheets nodes.",
        "height": 380,
        "width": 520
      },
      "id": "sticky-creds",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [700, 40]
    },
    {
      "parameters": {
        "content": "## Test it right now, zero keys\n1. Keep testMode true in the Config node.\n2. Activate the workflow.\n3. Send a POST to the webhook path jah-test-jah-ai-receptionist.\n4. Use this body:\n{\"name\": \"Sam\", \"phone\": \"+15550100200\", \"channel\": \"missed_call\", \"message\": \"I want to book an appointment for Friday\"}\n5. You get a JSON answer back in about 2 seconds.\n\nThe mock node stands in for the AI. No key is called.\nTry the word emergency in the message. Watch the route change.",
        "height": 380,
        "width": 620
      },
      "id": "sticky-test",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [1260, 40]
    },
    {
      "parameters": {
        "content": "## Step 2: AI reads the message\nThe AI tags each message with an intent and an urgency.\nIntents: book, question, emergency.\nTest mode skips the AI call. The mock node tags it instead.\nLive mode calls OpenAI with your OPENAI_API_KEY.",
        "height": 260,
        "width": 560
      },
      "id": "sticky-ai",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [860, 940]
    },
    {
      "parameters": {
        "content": "## Step 3: Route and reply\nBooking asks get a text with your booking link.\nQuestions get a reply that promises an answer in 1 business hour.\nEmergencies send you an alert right away.\nEvery contact lands as 1 row in your log sheet.",
        "height": 280,
        "width": 560
      },
      "id": "sticky-route",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [2160, 40]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "jah-test-jah-ai-receptionist",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-in",
      "name": "Missed Call Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [240, 520]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "cfg1", "name": "testMode", "value": true, "type": "boolean" },
            { "id": "cfg2", "name": "businessName", "value": "YOUR_BUSINESS_NAME", "type": "string" },
            { "id": "cfg3", "name": "bookingLink", "value": "YOUR_BOOKING_LINK", "type": "string" },
            { "id": "cfg4", "name": "businessHoursStart", "value": 8, "type": "number" },
            { "id": "cfg5", "name": "businessHoursEnd", "value": 18, "type": "number" },
            { "id": "cfg6", "name": "twilioFrom", "value": "YOUR_TWILIO_PHONE_NUMBER", "type": "string" },
            { "id": "cfg7", "name": "ownerMobile", "value": "YOUR_OWNER_MOBILE", "type": "string" },
            { "id": "cfg8", "name": "sheetId", "value": "YOUR_GOOGLE_SHEET_ID", "type": "string" }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "config",
      "name": "Config (EDIT ME)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [460, 520]
    },
    {
      "parameters": {
        "jsCode": "// Normalize the inbound payload so every downstream node has stable fields.\nconst cfg = $input.first().json;\nconst data = cfg.body || {};\nconst message = (data.message || data.transcript || data.notes || '').toString();\nconst allowedChannels = ['missed_call', 'voicemail', 'form'];\nconst channel = allowedChannels.includes(data.channel) ? data.channel : 'missed_call';\nlet receivedAt = data.receivedAt ? new Date(data.receivedAt) : new Date();\nif (isNaN(receivedAt.getTime())) { receivedAt = new Date(); }\nconst hour = receivedAt.getHours();\nconst start = isNaN(Number(cfg.businessHoursStart)) ? 8 : Number(cfg.businessHoursStart);\nconst end = isNaN(Number(cfg.businessHoursEnd)) ? 18 : Number(cfg.businessHoursEnd);\nconst afterHours = hour < start || hour >= end;\nreturn [{ json: {\n  callerName: (data.name || 'there').toString(),\n  callerPhone: (data.phone || '').toString(),\n  callerEmail: (data.email || '').toString(),\n  channel,\n  message,\n  hour,\n  afterHours,\n  testMode: cfg.testMode === true || cfg.testMode === 'true'\n} }];"
      },
      "id": "normalize",
      "name": "Normalize Inquiry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [680, 520]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": ""
          },
          "conditions": [
            {
              "id": "tm1",
              "leftValue": "={{ $json.testMode }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-testmode",
      "name": "Test Mode?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [900, 520]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "mock1",
              "name": "intent",
              "value": "={{ $json.message.toLowerCase().match(/emergency|urgent|flood|burst|leak|fire|no heat|asap/) ? 'emergency' : ($json.message.toLowerCase().match(/book|appointment|schedule|quote|estimate|come out|reserve/) ? 'book' : 'question') }}",
              "type": "string"
            },
            {
              "id": "mock2",
              "name": "urgency",
              "value": "={{ $json.message.toLowerCase().match(/emergency|urgent|flood|burst|leak|fire|no heat|asap/) ? 'high' : 'normal' }}",
              "type": "string"
            },
            {
              "id": "mock3",
              "name": "aiSummary",
              "value": "={{ 'Test mode summary. Caller said: ' + $json.message.slice(0, 90) }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "mock-ai",
      "name": "Mock AI Intent (test mode)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1120, 400]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.OPENAI_API_KEY || '' }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', max_tokens: 200, messages: [{ role: 'user', content: `You are the front desk AI for ${$('Config (EDIT ME)').first().json.businessName}. Read this missed call, voicemail, or form message. Respond with ONLY raw JSON, no markdown fences, shaped exactly: {\"intent\": \"book\" or \"question\" or \"emergency\", \"urgency\": \"low\" or \"normal\" or \"high\", \"summary\": \"one short sentence\"}.\\n\\nChannel: ${$json.channel}\\nCaller: ${$json.callerName}\\nMessage: ${$json.message}` }] }) }}",
        "options": {}
      },
      "id": "ai-intent",
      "name": "AI Intent and Urgency (OpenAI)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1120, 660]
    },
    {
      "parameters": {
        "jsCode": "// Parse the AI answer and clamp it to values the router understands.\nconst ai = $input.first().json;\nconst raw = ai.choices?.[0]?.message?.content || '{}';\nlet parsed = {};\ntry { parsed = JSON.parse(raw.replace(/```json|```/g, '').trim()); } catch (e) { parsed = {}; }\nconst intents = ['book', 'question', 'emergency'];\nconst urgencies = ['low', 'normal', 'high'];\nconst intent = intents.includes(parsed.intent) ? parsed.intent : 'question';\nconst urgency = urgencies.includes(parsed.urgency) ? parsed.urgency : 'normal';\nconst src = $('Normalize Inquiry').first().json;\nreturn [{ json: { ...src, intent, urgency, aiSummary: (parsed.summary || '').toString() } }];"
      },
      "id": "parse-ai",
      "name": "Parse AI Intent",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1340, 660]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": ""
          },
          "conditions": [
            {
              "id": "ah1",
              "leftValue": "={{ $json.afterHours }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-afterhours",
      "name": "After Hours?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1560, 520]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ahn1",
              "name": "greetingLine",
              "value": "Sorry I missed you. The shop is closed right now.",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "set-afterhours",
      "name": "After Hours Greeting",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1780, 400]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ohn1",
              "name": "greetingLine",
              "value": "Sorry I missed your call. I am with another customer.",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "set-openhours",
      "name": "Open Hours Greeting",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1780, 660]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": ""
                },
                "conditions": [
                  {
                    "id": "sw1",
                    "leftValue": "={{ $json.intent }}",
                    "rightValue": "book",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "book"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": ""
                },
                "conditions": [
                  {
                    "id": "sw2",
                    "leftValue": "={{ $json.intent }}",
                    "rightValue": "question",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "question"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": ""
                },
                "conditions": [
                  {
                    "id": "sw3",
                    "leftValue": "={{ $json.intent }}",
                    "rightValue": "emergency",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "emergency"
            }
          ]
        },
        "options": {}
      },
      "id": "switch-intent",
      "name": "Route Intent",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [2000, 520]
    },
    {
      "parameters": {
        "jsCode": "// Build the booking text message.\nconst cfg = $('Config (EDIT ME)').first().json;\nconst j = $input.first().json;\nconst replyBody = `Hi ${j.callerName}. ${j.greetingLine} You can grab a time here in 30 seconds: ${cfg.bookingLink} Reply to this text with any question. ${cfg.businessName}`;\nreturn [{ json: { ...j, replyChannel: 'sms', replyTo: j.callerPhone || 'unknown', replyBody } }];"
      },
      "id": "compose-book",
      "name": "Compose Booking Text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2220, 320]
    },
    {
      "parameters": {
        "jsCode": "// Build the question reply. Email if we have one, text if not.\nconst cfg = $('Config (EDIT ME)').first().json;\nconst j = $input.first().json;\nconst useEmail = j.callerEmail.length > 0;\nconst replyBody = `Hi ${j.callerName}. ${j.greetingLine} I got your question and I am on it. You get an answer within 1 business hour. Want to skip the wait? Book a time here: ${cfg.bookingLink} ${cfg.businessName}`;\nreturn [{ json: { ...j, replyChannel: useEmail ? 'email' : 'sms', replyTo: useEmail ? j.callerEmail : (j.callerPhone || 'unknown'), replyBody } }];"
      },
      "id": "compose-question",
      "name": "Compose Question Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2220, 520]
    },
    {
      "parameters": {
        "jsCode": "// Build the urgent alert for the owner plus a fast reply to the caller.\nconst cfg = $('Config (EDIT ME)').first().json;\nconst j = $input.first().json;\nconst replyBody = `Hi ${j.callerName}. I see this is urgent. A real person calls you back within 15 minutes. ${cfg.businessName}`;\nconst ownerAlert = `URGENT lead. ${j.callerName} at ${j.callerPhone || j.callerEmail || 'no contact given'}. They said: ${j.message.slice(0, 160)}`;\nreturn [{ json: { ...j, replyChannel: 'sms_plus_owner_alert', replyTo: j.callerPhone || 'unknown', replyBody, ownerAlert, ownerMobile: cfg.ownerMobile } }];"
      },
      "id": "compose-urgent",
      "name": "Compose Urgent Alert",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2220, 720]
    },
    {
      "parameters": {},
      "id": "send-sms",
      "name": "Send SMS (add your Twilio node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [2440, 320]
    },
    {
      "parameters": {},
      "id": "send-reply",
      "name": "Send Reply (add your email or Twilio node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [2440, 520]
    },
    {
      "parameters": {},
      "id": "alert-owner",
      "name": "Alert Owner Now (add your Twilio node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [2440, 720]
    },
    {
      "parameters": {
        "jsCode": "// Shape one clean row for the call log sheet.\nconst j = $input.first().json;\nconst cfg = $('Config (EDIT ME)').first().json;\nreturn [{ json: {\n  logged_at: new Date().toISOString(),\n  business: cfg.businessName,\n  caller: j.callerName,\n  phone: j.callerPhone,\n  email: j.callerEmail,\n  channel: j.channel,\n  intent: j.intent,\n  urgency: j.urgency,\n  after_hours: j.afterHours === true,\n  reply_channel: j.replyChannel,\n  reply_body: j.replyBody || '',\n  summary: j.aiSummary || '',\n  test_mode: j.testMode === true,\n  sheet_id: cfg.sheetId\n} }];"
      },
      "id": "log-row",
      "name": "Build Log Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2660, 520]
    },
    {
      "parameters": {},
      "id": "log-sheets",
      "name": "Log to Google Sheets (add your Sheets node)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [2880, 520]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'ok', intent: $json.intent, urgency: $json.urgency, after_hours: $json.after_hours, reply_channel: $json.reply_channel, reply_preview: ($json.reply_body || '').slice(0, 140), test_mode: $json.test_mode }) }}",
        "options": {}
      },
      "id": "respond",
      "name": "Respond With Summary",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [3100, 520]
    }
  ],
  "connections": {
    "Missed Call Webhook": {
      "main": [
        [
          { "node": "Config (EDIT ME)", "type": "main", "index": 0 }
        ]
      ]
    },
    "Config (EDIT ME)": {
      "main": [
        [
          { "node": "Normalize Inquiry", "type": "main", "index": 0 }
        ]
      ]
    },
    "Normalize Inquiry": {
      "main": [
        [
          { "node": "Test Mode?", "type": "main", "index": 0 }
        ]
      ]
    },
    "Test Mode?": {
      "main": [
        [
          { "node": "Mock AI Intent (test mode)", "type": "main", "index": 0 }
        ],
        [
          { "node": "AI Intent and Urgency (OpenAI)", "type": "main", "index": 0 }
        ]
      ]
    },
    "Mock AI Intent (test mode)": {
      "main": [
        [
          { "node": "After Hours?", "type": "main", "index": 0 }
        ]
      ]
    },
    "AI Intent and Urgency (OpenAI)": {
      "main": [
        [
          { "node": "Parse AI Intent", "type": "main", "index": 0 }
        ]
      ]
    },
    "Parse AI Intent": {
      "main": [
        [
          { "node": "After Hours?", "type": "main", "index": 0 }
        ]
      ]
    },
    "After Hours?": {
      "main": [
        [
          { "node": "After Hours Greeting", "type": "main", "index": 0 }
        ],
        [
          { "node": "Open Hours Greeting", "type": "main", "index": 0 }
        ]
      ]
    },
    "After Hours Greeting": {
      "main": [
        [
          { "node": "Route Intent", "type": "main", "index": 0 }
        ]
      ]
    },
    "Open Hours Greeting": {
      "main": [
        [
          { "node": "Route Intent", "type": "main", "index": 0 }
        ]
      ]
    },
    "Route Intent": {
      "main": [
        [
          { "node": "Compose Booking Text", "type": "main", "index": 0 }
        ],
        [
          { "node": "Compose Question Reply", "type": "main", "index": 0 }
        ],
        [
          { "node": "Compose Urgent Alert", "type": "main", "index": 0 }
        ]
      ]
    },
    "Compose Booking Text": {
      "main": [
        [
          { "node": "Send SMS (add your Twilio node)", "type": "main", "index": 0 }
        ]
      ]
    },
    "Compose Question Reply": {
      "main": [
        [
          { "node": "Send Reply (add your email or Twilio node)", "type": "main", "index": 0 }
        ]
      ]
    },
    "Compose Urgent Alert": {
      "main": [
        [
          { "node": "Alert Owner Now (add your Twilio node)", "type": "main", "index": 0 }
        ]
      ]
    },
    "Send SMS (add your Twilio node)": {
      "main": [
        [
          { "node": "Build Log Row", "type": "main", "index": 0 }
        ]
      ]
    },
    "Send Reply (add your email or Twilio node)": {
      "main": [
        [
          { "node": "Build Log Row", "type": "main", "index": 0 }
        ]
      ]
    },
    "Alert Owner Now (add your Twilio node)": {
      "main": [
        [
          { "node": "Build Log Row", "type": "main", "index": 0 }
        ]
      ]
    },
    "Build Log Row": {
      "main": [
        [
          { "node": "Log to Google Sheets (add your Sheets node)", "type": "main", "index": 0 }
        ]
      ]
    },
    "Log to Google Sheets (add your Sheets node)": {
      "main": [
        [
          { "node": "Respond With Summary", "type": "main", "index": 0 }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
