{
  "id": "qC8SFIEjlyWY0hvC",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "No Show / Missed Demo Follow Up",
  "tags": [],
  "nodes": [
    {
      "id": "8531ef8f-43d0-452c-98ea-4645a92e7e42",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        224,
        1120
      ],
      "parameters": {
        "color": 3,
        "width": 396,
        "height": 356,
        "content": "## \ud83d\udccb WORKFLOW OVERVIEW\n\nThis automation tracks Calendly demo bookings and checks Zoom attendance. When someone no-shows:\n\n\u2705 Updates database\n\u2705 Generates AI follow-up messages\n\u2705 Notifies your team\n\u2705 (Optional) Sends recovery email\n\u2705 (Optional) Updates CRM\n\n**Two Main Paths:**\n1. **Booking Path** (top): Logs when demos are scheduled\n2. **Attendance Path** (middle): Checks who showed up after meeting ends"
      },
      "typeVersion": 1
    },
    {
      "id": "dbd246ba-1a3b-4a1b-b75e-80b8932dff66",
      "name": "Setup Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        944
      ],
      "parameters": {
        "color": 5,
        "width": 384,
        "height": 296,
        "content": "## \u2699\ufe0f ONE-TIME SETUP (Run Once)\n\n**Before first use:**\n1. Add your Calendly API token below\n2. Click \"Execute Workflow\" \n3. This creates the webhook subscription\n4. You only need to run this ONCE\n\n**What it does:**\nTells Calendly to notify n8n when new bookings happen"
      },
      "typeVersion": 1
    },
    {
      "id": "b5f882b3-e2b2-4487-a802-f4d1b2836283",
      "name": "Zoom Validation Guide",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1056,
        496
      ],
      "parameters": {
        "color": 4,
        "width": 420,
        "height": 360,
        "content": "## \ud83d\udd34 CRITICAL: ZOOM VALIDATION\n\n**Required for Zoom webhooks to work:**\n\n1. This webhook handles Zoom's validation challenge\n2. Uses HMAC SHA-256 encryption\n3. Must respond within 3 seconds\n\n**Setup:**\n- Add your Zoom webhook secret in the Code node\n- Keep this workflow ACTIVE\n- Use production URL when validating in Zoom dashboard\n\n**After validation succeeds, you can disable the duplicate webhook below**"
      },
      "typeVersion": 1
    },
    {
      "id": "c718c4c9-7f69-4e73-9497-191bfd067fa5",
      "name": "Path 1 Explanation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        1248
      ],
      "parameters": {
        "color": 3,
        "width": 380,
        "height": 280,
        "content": "## \ud83d\udce5 PATH 1: BOOKING TRACKING\n\n**Flow:**\n1. Calendly webhook fires when booking created\n2. Extract Zoom meeting ID + attendee info\n3. Filter for demo events only\n4. Save to database for later lookup\n\n**Why:** We need to store the booking so we can match it when the Zoom meeting ends"
      },
      "typeVersion": 1
    },
    {
      "id": "c38cc125-7571-4a3d-9cd3-542dea207de2",
      "name": "Path 2 Explanation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        1536
      ],
      "parameters": {
        "color": 3,
        "width": 384,
        "height": 332,
        "content": "## \ud83d\udd0d PATH 2: ATTENDANCE CHECK\n\n**Flow:**\n1. Zoom webhook fires when meeting ends\n2. Get access token + fetch meeting from database\n3. Call Zoom API for participant list\n4. Check if expected attendee joined\n5. Update database with attendance status\n\n**Then splits:**\n- \u2705 Attended \u2192 Database updated, done\n- \u274c No-show \u2192 Continue to follow-up actions"
      },
      "typeVersion": 1
    },
    {
      "id": "f9bd45e4-c8ad-4fd8-95a1-705a614aa6a0",
      "name": "AI Configuration",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2592,
        1184
      ],
      "parameters": {
        "color": 5,
        "width": 400,
        "height": 336,
        "content": "## \ud83e\udd16 AI PERSONALIZATION\n\n**What happens:**\n1. AI generates custom email + LinkedIn messages\n2. Based on available context (email, meeting ID, participants)\n\n**To customize:**\n- Edit the prompt to match your brand voice\n- Add more context if you enrich data earlier\n- Adjust message length/tone\n\n**\u26a0\ufe0f Missing:** You need to add a Parse node after this to extract the JSON fields before using them!"
      },
      "typeVersion": 1
    },
    {
      "id": "35d77e58-2101-47cc-96f2-9f4c3c605388",
      "name": "Follow-up Actions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3536,
        1440
      ],
      "parameters": {
        "color": 6,
        "width": 420,
        "height": 340,
        "content": "## \ud83d\udce4 FOLLOW-UP ACTIONS\n\n**Three parallel paths:**\n\n1. **Slack notification** \u2192 Alert your team immediately\n2. **Recovery email** \u2192 (Disabled) Automated outreach\n3. **CRM update** \u2192 (Disabled) Log activity in HubSpot\n\n**Enable when ready:**\n- Configure credentials\n- Customize message templates\n- Test with a real no-show\n\n**All run simultaneously** so your team is notified even if email fails"
      },
      "typeVersion": 1
    },
    {
      "id": "73ed0acb-4dba-4826-8400-3d3fd59432d7",
      "name": "Database Configuration",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2176,
        1184
      ],
      "parameters": {
        "color": 5,
        "width": 400,
        "height": 340,
        "content": "## \ud83d\udcbe DATABASE SETUP\n\n**Create a Data Table with these columns:**\n- `meeting_id` (string or number)\n- `email` (string)\n- `status` (string: 'pending', 'attended', 'no_show')\n\n**Why two nodes:**\n1. **Insert** = Save booking when scheduled\n2. **Get** = Look up booking when meeting ends\n3. **Update** = Mark attendance status\n\n**Critical:** meeting_id must match between Calendly & Zoom!"
      },
      "typeVersion": 1
    },
    {
      "id": "a3248c16-6d27-4fd8-854f-a86171b20d90",
      "name": "Zoom Auth Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1728,
        1808
      ],
      "parameters": {
        "color": 4,
        "width": 360,
        "height": 300,
        "content": "## \ud83d\udd10 ZOOM OAUTH SETUP\n\n**You need Server-to-Server OAuth credentials:**\n\n1. Go to Zoom Marketplace\n2. Create a Server-to-Server OAuth app\n3. Get: Account ID, Client ID, Client Secret\n4. Add scopes: `meeting:read:past_meeting:admin`\n\n**Security:** These credentials give access to meeting data. Keep them secret!"
      },
      "typeVersion": 1
    },
    {
      "id": "894619ce-a0d6-4847-a836-21288a8df5d7",
      "name": "Setup Checklist",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        224,
        1488
      ],
      "parameters": {
        "color": 4,
        "width": 400,
        "height": 380,
        "content": "## \u26a0\ufe0f SETUP CHECKLIST\n\n**Before activating:**\n- [ ] Create n8n Data Table\n- [ ] Get Calendly API token\n- [ ] Get Zoom OAuth credentials  \n- [ ] Get Zoom webhook secret\n- [ ] Run one-time setup (top left)\n- [ ] Validate Zoom webhook\n- [ ] Add OpenAI API key\n- [ ] Configure Slack (optional)\n- [ ] Configure email sender (optional)\n- [ ] Test with fake booking\n\n**Then activate workflow!**"
      },
      "typeVersion": 1
    },
    {
      "id": "e108da24-f19b-4417-83fd-9e83088c8b4d",
      "name": "Calendly Booking Webhook",
      "type": "n8n-nodes-base.webhook",
      "notes": "PATH 1: Receives booking data when a Calendly event is scheduled",
      "position": [
        1104,
        1312
      ],
      "parameters": {
        "path": "cal-uri-get",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "bffa0474-3d78-41e7-b1c5-93967644c0dc",
      "name": "Extract Booking Data",
      "type": "n8n-nodes-base.code",
      "notes": "Parses Calendly webhook payload and extracts Zoom meeting details",
      "position": [
        1328,
        1312
      ],
      "parameters": {
        "jsCode": "// Extract all booking data from Calendly webhook\nconst payload = $json.body.payload;\nconst scheduledEvent = payload.scheduled_event;\nconst location = scheduledEvent.location;\n\n// Extract Zoom meeting ID\nconst zoomMeetingId = location?.data?.id || null;\nconst zoomJoinUrl = location?.join_url || null;\n\nif (!zoomMeetingId) {\n  throw new Error('No Zoom meeting ID found - is this a Zoom meeting?');\n}\n\nreturn [{\n  json: {\n    // IDs\n    booking_id: payload.uri,\n    meeting_id: parseInt(zoomMeetingId),\n    event_type_uri: scheduledEvent.event_type,\n    \n    // Meeting details\n    zoom_join_url: zoomJoinUrl,\n    zoom_password: location?.data?.password || null,\n    event_name: scheduledEvent.name,\n    start_time: scheduledEvent.start_time,\n    end_time: scheduledEvent.end_time,\n    \n    // Attendee info\n    email: payload.email,\n    name: payload.name,\n    \n    // Tracking\n    status: 'pending',\n    created_at: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f2ad5409-75a3-4dc2-a439-416b01d3d62c",
      "name": "Filter: Demo Events Only",
      "type": "n8n-nodes-base.filter",
      "notes": "\u26a0\ufe0f CONFIGURE: Replace 'YOUR_CALENDLY_EVENT_TYPE_URI' with your specific demo event type URI from Calendly",
      "position": [
        1552,
        1312
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f41e5129-f6e0-414a-9e38-1735f3da3b1d",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.event_type_uri }}",
              "rightValue": "YOUR_CALENDLY_EVENT_TYPE_URI"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "3cec4aa0-a335-4dc3-85e9-dd62a336dfcc",
      "name": "Zoom Meeting Ended Webhook",
      "type": "n8n-nodes-base.webhook",
      "notes": "PATH 2: Receives webhook when Zoom meeting ends to check attendance",
      "position": [
        1104,
        1568
      ],
      "parameters": {
        "path": "zoom-meeting-ended",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "9934e2fc-53b5-4811-a1c7-97eaf985e6c6",
      "name": "Filter: Meeting Ended Events",
      "type": "n8n-nodes-base.filter",
      "notes": "Ensures we only process meeting.ended events from Zoom",
      "position": [
        1488,
        1568
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.body.event }}",
              "value2": "meeting.ended",
              "operation": "equals"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "16831094-cc44-490d-8747-bb5d322486fb",
      "name": "Extract Meeting ID from Zoom",
      "type": "n8n-nodes-base.code",
      "notes": "Parses Zoom webhook to get meeting ID for lookup",
      "position": [
        1664,
        1568
      ],
      "parameters": {
        "jsCode": "// Extract Zoom meeting ID from webhook and convert to number\nconst meetingId = parseInt($json.body?.payload?.object?.id);\n\nif (!meetingId) {\n  throw new Error('No meeting ID in Zoom webhook');\n}\n\nreturn [{\n  json: {\n    meeting_id: meetingId,\n    meeting_uuid: $json.body?.payload?.object?.uuid,\n    ended_at: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1b53510d-2ec2-4528-bf68-00be8314c828",
      "name": "Get Zoom Participants",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Fetches list of participants from Zoom API to check who attended",
      "position": [
        2208,
        1568
      ],
      "parameters": {
        "url": "=https://api.zoom.us/v2/past_meetings/{{ $json.meeting_id }}/participants",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('Get Zoom Access Token').item.json.access_token }}"
            }
          ]
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "984a024a-9421-4350-b05b-3f829dc7ebc1",
      "name": "Check if Attendee Showed Up",
      "type": "n8n-nodes-base.code",
      "notes": "Compares expected attendee email with actual Zoom participants",
      "position": [
        2384,
        1568
      ],
      "parameters": {
        "jsCode": "// Check if expected attendee actually joined\nconst bookingData = $('Get Booking from Database').first().json;\nconst expectedEmail = bookingData.email.toLowerCase();\nconst participants = $json.participants || [];\n\nconst attended = participants.some(p => \n  p.user_email && p.user_email.toLowerCase() === expectedEmail\n);\n\nreturn [{\n  json: {\n    ...bookingData,\n    attended: attended,\n    participant_count: participants.length,\n    participants_list: participants.map(p => p.name || p.user_email).join(', ')\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "151f697a-1f1e-49c2-a691-01b45d1b9ddf",
      "name": "AI Generate Follow-Up Messages",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "notes": "\u26a0\ufe0f CONFIGURE: Connect your OpenAI API credentials. Customize this prompt to match your brand voice and follow-up style.",
      "position": [
        2832,
        1568
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=Generate follow-up messages for a demo no-show.\n\nContext:\n- Email: {{ $json.email }}\n- Meeting ID: {{ $json.meeting_id }}\n- Participant count: {{ $json.participant_count }}\n- Who was there: {{ $json.participants_list }}\n- Attendee status: Did not attend\n\nCreate professional, empathetic follow-up messages:\n\n1. EMAIL SUBJECT (short, friendly, non-pushy)\n2. EMAIL BODY (3-4 sentences maximum - acknowledge the miss, express understanding that things happen, offer to reschedule OR send a quick Loom recording)\n3. LINKEDIN MESSAGE (2 sentences, casual and brief)\n\nReturn ONLY valid JSON in this exact format:\n{\n  \"email_subject\": \"...\",\n  \"email_body\": \"...\",\n  \"linkedin_message\": \"...\"\n}"
            }
          ]
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "4667b279-40bd-4246-80d3-7b5d74cf639f",
      "name": "Send Recovery Email",
      "type": "n8n-nodes-base.emailSend",
      "notes": "\u26a0\ufe0f CONFIGURE: Replace 'YOUR_SALES_EMAIL@company.com' with your sender email. Configure your email provider credentials. Enable this node when ready to send automated emails.",
      "disabled": true,
      "position": [
        3344,
        1392
      ],
      "parameters": {
        "options": {},
        "subject": "={{ $json.email_subject }}",
        "toEmail": "={{ $json.email }}",
        "fromEmail": "user@example.com"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ec561207-12f7-4327-8e1b-fa6e191f4cc4",
      "name": "Notify Team in Slack",
      "type": "n8n-nodes-base.slack",
      "notes": "\u26a0\ufe0f CONFIGURE: 1) Connect Slack OAuth credentials 2) Select your channel 3) Customize the message template to include info your team needs (attendee name, company, demo type, next steps, etc.)",
      "position": [
        3344,
        1760
      ],
      "parameters": {
        "text": "=\ud83c\udfaf *NEW DEMO NO-SHOW*\n\n\ud83d\udce7 Email: {{ $json.email }}\n\ud83c\udd94 Meeting ID: {{ $json.meeting_id }}\n\ud83d\udc65 Participants: {{ $json.participants_list }}\n\ud83d\udcca Total in meeting: {{ $json.participant_count }}\n\n\u2705 *Actions Taken:*\n- Recovery email queued\n- Database updated\n\n\ud83d\udcac *Suggested Follow-ups:*\n{{ $json.linkedin_message }}\n\n---\nCustomize this message template to fit your team's needs!",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "your-channel-name"
        },
        "otherOptions": {
          "mrkdwn": true
        },
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "454864d6-90ec-4741-9f57-2cf86f0c8b33",
      "name": "Save Booking to Database",
      "type": "n8n-nodes-base.dataTable",
      "notes": "\u26a0\ufe0f CONFIGURE: Create a Data Table in n8n with columns: meeting_id, email, status. Link it here.",
      "position": [
        1744,
        1312
      ],
      "parameters": {
        "columns": {
          "value": {
            "email": "={{ $json.email }}",
            "meeting_id": "={{ $json.meeting_id }}"
          },
          "schema": [
            {
              "id": "meeting_id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "meeting_id",
              "defaultMatch": false
            },
            {
              "id": "email",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "email",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_DATA_TABLE_ID",
          "cachedResultUrl": "/projects/YOUR_PROJECT_ID/datatables/YOUR_DATA_TABLE_ID",
          "cachedResultName": "Calendly Events"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "32d50735-236a-4f69-b5a9-39d772d46965",
      "name": "Get Booking from Database",
      "type": "n8n-nodes-base.dataTable",
      "notes": "Retrieves the original booking details using Zoom meeting ID",
      "position": [
        1856,
        1648
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "meeting_id",
              "keyValue": "={{ $json.meeting_id }}"
            }
          ]
        },
        "matchType": "allConditions",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_DATA_TABLE_ID",
          "cachedResultUrl": "/projects/YOUR_PROJECT_ID/datatables/YOUR_DATA_TABLE_ID",
          "cachedResultName": "Calendly Events"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9a42b893-b4c0-4eaf-ad40-b3adf4799661",
      "name": "Get Zoom Access Token",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "\u26a0\ufe0f CONFIGURE: Replace with your Zoom Server-to-Server OAuth credentials (Account ID, Client ID, Client Secret). Get these from your Zoom App in the Zoom Marketplace.",
      "position": [
        1856,
        1488
      ],
      "parameters": {
        "url": "https://zoom.us/oauth/token",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "grant_type",
              "value": "account_credentials"
            },
            {
              "name": "account_id",
              "value": "YOUR_ZOOM_ACCOUNT_ID"
            },
            {
              "name": "client_id",
              "value": "YOUR_ZOOM_CLIENT_ID"
            },
            {
              "name": "client_secret",
              "value": "YOUR_ZOOM_CLIENT_SECRET"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "40feb37d-85bb-4c18-9435-30d108b359e6",
      "name": "Merge Access Token with Booking Data",
      "type": "n8n-nodes-base.merge",
      "notes": "Combines Zoom access token with meeting data for API call",
      "position": [
        2048,
        1568
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "4d9ecb8a-a6c7-4f54-ae84-f3dd981ae9ff",
      "name": "Update Attendance Status",
      "type": "n8n-nodes-base.dataTable",
      "notes": "Updates database with attendance status (true/false)",
      "position": [
        2640,
        1728
      ],
      "parameters": {
        "columns": {
          "value": {
            "email": "={{ $json.email}}",
            "status": "={{ $json.attended }}",
            "meeting_id": "={{ $json.meeting_id }}"
          },
          "schema": [
            {
              "id": "meeting_id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "meeting_id",
              "defaultMatch": false
            },
            {
              "id": "email",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "email",
              "defaultMatch": false
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "filters": {
          "conditions": [
            {
              "keyName": "meeting_id",
              "keyValue": "={{ $json.meeting_id }}"
            }
          ]
        },
        "options": {},
        "operation": "update",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_DATA_TABLE_ID",
          "cachedResultUrl": "/projects/YOUR_PROJECT_ID/datatables/YOUR_DATA_TABLE_ID",
          "cachedResultName": "Calendly Events"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "49e2fe76-6b3b-4d73-895e-fadcbb515c45",
      "name": "Update CRM Deal (Optional)",
      "type": "n8n-nodes-base.hubspot",
      "notes": "\u26a0\ufe0f OPTIONAL: Enable and configure this to update HubSpot (or your CRM) deals when someone no-shows. Map the deal ID and fields you want to update.",
      "disabled": true,
      "position": [
        3344,
        1568
      ],
      "parameters": {
        "dealId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "resource": "deal",
        "operation": "update",
        "updateFields": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "a993d193-02c7-4c15-bf85-49ac4ce3132a",
      "name": "Manual Setup Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "notes": "SETUP PATH: Run this once to create Calendly webhook subscription",
      "position": [
        1104,
        1088
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "33dbaafd-198b-4533-82f0-54920bac9f08",
      "name": "Get Calendly Organization",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "\u26a0\ufe0f CONFIGURE: Replace 'YOUR_CALENDLY_API_TOKEN' with your Calendly Personal Access Token from https://calendly.com/integrations/api_webhooks",
      "position": [
        1328,
        1088
      ],
      "parameters": {
        "url": "https://api.calendly.com/users/me",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "1cfed318-50e7-4bed-b894-3ef5a7760360",
      "name": "Extract Organization URI",
      "type": "n8n-nodes-base.code",
      "notes": "Parses Calendly response to get your organization URI",
      "position": [
        1552,
        1088
      ],
      "parameters": {
        "jsCode": "// Extract organization URI\nconst response = $input.first().json;\nconst orgUri = response.resource.current_organization;\n\nif (!orgUri) {\n  throw new Error('No organization found. You might be on a personal account.');\n}\n\nconsole.log('\u2705 Your Organization URI:', orgUri);\n\nreturn [{\n  json: {\n    organization_uri: orgUri,\n    user_uri: response.resource.uri,\n    user_name: response.resource.name,\n    user_email: response.resource.email\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7c50f2e3-7f00-4cac-a890-61df07e935ef",
      "name": "Create Calendly Webhook",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "\u26a0\ufe0f CONFIGURE: Replace 'YOUR_N8N_WEBHOOK_URL' with your n8n instance URL and 'YOUR_CALENDLY_API_TOKEN' with your Calendly token",
      "position": [
        1776,
        1088
      ],
      "parameters": {
        "url": "https://api.calendly.com/webhook_subscriptions",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"url\": \"YOUR_N8N_WEBHOOK_URL/webhook/cal-uri-get\",\n  \"events\": [\"invitee.created\"],\n  \"organization\": \"{{ $json.organization_uri }}\",\n  \"scope\": \"organization\",\n  \"signing_key\": \"your_optional_secret_key\"\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "7b694aed-9d5c-4321-90be-84d05cb33c25",
      "name": "Show Setup Success",
      "type": "n8n-nodes-base.code",
      "position": [
        1984,
        1088
      ],
      "parameters": {
        "jsCode": "// Parse webhook creation response\nconst response = $input.first().json;\n\nif (response.resource) {\n  console.log('\u2705 WEBHOOK CREATED SUCCESSFULLY!');\n  console.log('Webhook URI:', response.resource.uri);\n  console.log('Webhook URL:', response.resource.url);\n  console.log('Events:', response.resource.events);\n  console.log('State:', response.resource.state);\n  \n  return [{\n    json: {\n      success: true,\n      webhook_uri: response.resource.uri,\n      webhook_url: response.resource.url,\n      events: response.resource.events,\n      state: response.resource.state,\n      message: '\u2705 Webhook subscription created! Calendly will now POST to your n8n webhook when bookings happen.'\n    }\n  }];\n} else {\n  throw new Error('Webhook creation failed: ' + JSON.stringify(response));\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "171e6449-1352-467b-8392-dbccf290c867",
      "name": "Zoom Webhook Validator",
      "type": "n8n-nodes-base.code",
      "notes": "\u26a0\ufe0f CONFIGURE: Replace 'YOUR_ZOOM_WEBHOOK_SECRET' with the secret token from your Zoom app's Feature page",
      "position": [
        1328,
        896
      ],
      "parameters": {
        "jsCode": "const crypto = require('crypto');\n\n// Your Zoom webhook secret token\nconst ZOOM_WEBHOOK_SECRET = 'YOUR_ZOOM_WEBHOOK_SECRET';\n\nconst body = $json.body;\n\n// Check if this is a validation request\nif (body?.event === 'endpoint.url_validation') {\n  const plainToken = body.payload.plainToken;\n  \n  // Hash the plainToken using HMAC SHA-256\n  const encryptedToken = crypto\n    .createHmac('sha256', ZOOM_WEBHOOK_SECRET)\n    .update(plainToken)\n    .digest('hex');\n  \n  return [{\n    plainToken: plainToken,\n    encryptedToken: encryptedToken\n  }];\n}\n\n// For normal webhook events, pass through\nreturn [$json];"
      },
      "typeVersion": 2
    },
    {
      "id": "fbb4c54e-4fb6-41cc-98b8-8f5944cb508c",
      "name": "Send Validation Response to Zoom",
      "type": "n8n-nodes-base.respondToWebhook",
      "notes": "Responds to Zoom's validation challenge with encrypted token",
      "position": [
        1520,
        896
      ],
      "parameters": {
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "typeVersion": 1.4
    },
    {
      "id": "33cf8321-30bb-49d6-bdd1-3463cb8ae23c",
      "name": "Filter: No-Shows Only",
      "type": "n8n-nodes-base.filter",
      "notes": "Only processes meetings where attendee did NOT show up (attended = false)",
      "position": [
        2624,
        1568
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "356a49d4-56b0-43f3-b640-f6e1c2f2b165",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.attended }}",
              "rightValue": "0"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a367fc8c-3e58-4c9e-b5b0-0e12786e9987",
      "name": "Zoom Validation Webhook (Duplicate)",
      "type": "n8n-nodes-base.webhook",
      "notes": "DISABLED: This is a duplicate for validation testing. Delete after Zoom webhook is validated.",
      "disabled": true,
      "position": [
        1104,
        896
      ],
      "parameters": {
        "path": "zoom-meeting-ended",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "0bff6195-14a8-43cf-9d02-871cf8944ab3",
  "connections": {
    "Extract Booking Data": {
      "main": [
        [
          {
            "node": "Filter: Demo Events Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Setup Trigger": {
      "main": [
        [
          {
            "node": "Get Calendly Organization",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter: No-Shows Only": {
      "main": [
        [
          {
            "node": "AI Generate Follow-Up Messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Zoom Access Token": {
      "main": [
        [
          {
            "node": "Merge Access Token with Booking Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Zoom Participants": {
      "main": [
        [
          {
            "node": "Check if Attendee Showed Up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zoom Webhook Validator": {
      "main": [
        [
          {
            "node": "Send Validation Response to Zoom",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Calendly Webhook": {
      "main": [
        [
          {
            "node": "Show Setup Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calendly Booking Webhook": {
      "main": [
        [
          {
            "node": "Extract Booking Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Organization URI": {
      "main": [
        [
          {
            "node": "Create Calendly Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter: Demo Events Only": {
      "main": [
        [
          {
            "node": "Save Booking to Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Booking from Database": {
      "main": [
        [
          {
            "node": "Merge Access Token with Booking Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get Calendly Organization": {
      "main": [
        [
          {
            "node": "Extract Organization URI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zoom Meeting Ended Webhook": {
      "main": [
        [
          {
            "node": "Filter: Meeting Ended Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Attendee Showed Up": {
      "main": [
        [
          {
            "node": "Update Attendance Status",
            "type": "main",
            "index": 0
          },
          {
            "node": "Filter: No-Shows Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Meeting ID from Zoom": {
      "main": [
        [
          {
            "node": "Get Zoom Access Token",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Booking from Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter: Meeting Ended Events": {
      "main": [
        [
          {
            "node": "Extract Meeting ID from Zoom",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Generate Follow-Up Messages": {
      "main": [
        [
          {
            "node": "Notify Team in Slack",
            "type": "main",
            "index": 0
          },
          {
            "node": "Update CRM Deal (Optional)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Recovery Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zoom Validation Webhook (Duplicate)": {
      "main": [
        [
          {
            "node": "Zoom Webhook Validator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Access Token with Booking Data": {
      "main": [
        [
          {
            "node": "Get Zoom Participants",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}