AutomationFlowsAI & RAG › Recover Missed Demos with Calendly, Zoom & Ai-generated Follow-ups

Recover Missed Demos with Calendly, Zoom & Ai-generated Follow-ups

ByConnor Provines @connorprovines on n8n.io

Automatically detects missed Zoom demos booked via Calendly and triggers AI-powered follow-up sequences.

Webhook trigger★★★★★ complexityAI-powered36 nodesHTTP RequestOpenAIEmail SendSlackData TableHubSpot
AI & RAG Trigger: Webhook Nodes: 36 Complexity: ★★★★★ AI nodes: yes Added:
Recover Missed Demos with Calendly, Zoom & Ai-generated Follow-ups — n8n workflow card showing HTTP Request, OpenAI, Email Send integration

This workflow corresponds to n8n.io template #9959 — we link there as the canonical source.

This workflow follows the Datatable → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Automatically detects missed Zoom demos booked via Calendly and triggers AI-powered follow-up sequences.

Source: https://n8n.io/workflows/9959/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This template generates a sales follow-up presentation in Google Slides after a sales call recorded in Claap. The workflow is simplified to showcase the main use case. You can customize and enrich thi

HTTP Request, Slack, HubSpot +1
AI & RAG

Consulting firms in strategy, management, or IT who want to automate client onboarding and internal task assignment.

OpenAI, Google Sheets, Slack +3
AI & RAG

Who is this for? Event organizers, conference planners, and marketing teams fighting registration drop-off who want 4-field forms with LinkedIn-level attendee intelligence. What problem is this workfl

Data Table, HubSpot, Email Send +4
AI & RAG

Watch on Youtube▶️

HTTP Request, Email Send, Google Sheets +3
AI & RAG

Venafi Presentation - Watch Video

Venafi Tls Protect Cloud, HTTP Request, OpenAI +1