{
  "name": "Follow Up After a Missed Demo | Uptune",
  "nodes": [
    {
      "parameters": {
        "content": "## Follow Up After a Missed Demo\nCheck the Zoom report for a Calendly invitee and prepare a rescheduling draft when the attendance evidence supports it.\n\nRun **Run with sample data** first. **Setup** defaults to dry run and uses the sample fixture without contacting connected apps. Configure the accounts and destinations in README.md before switching modes.",
        "width": 450,
        "height": 235
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -390
      ],
      "id": "118cbd97-61f9-5793-9a32-64b254b0af6e",
      "name": "Start here"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -720,
        80
      ],
      "id": "96d7c119-4753-5b25-82cc-f6efce8fc6da",
      "name": "Run with sample data"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nreturn [{json: {sampleRun: true, body: {\"bookingId\": \"invitee-demo-01\", \"meetingUuid\": \"fictional-Zoom-uuid==\", \"endedAt\": \"2025-01-15T15:30:00Z\", \"inviteeEmail\": \"avery@example.com\", \"inviteeName\": \"Avery Morgan\", \"hostEmail\": \"sales@example.com\"}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -480,
        80
      ],
      "id": "642550b5-a7ed-56e5-ba0a-807f15551c4e",
      "name": "Sample input"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "uptune-missed-demo-follow-up",
        "authentication": "headerAuth",
        "responseMode": "onReceived",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -480,
        300
      ],
      "id": "89a5ffbc-d060-5172-8f7b-3ba903fe5d25",
      "name": "Receive event",
      "webhookId": "89a5ffbc-d060-5172-8f7b-3ba903fe5d25"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst config = {\n  \"dryRun\": true,\n  \"tableId\": \"\",\n  \"reportDelayMinutes\": 20,\n  \"verifiedZoomIdentityRequired\": false,\n  \"createGmailDraft\": false,\n  \"bookingUrl\": \"\"\n};\nreturn [{json: {...input, config}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        150
      ],
      "id": "fe609efb-2463-5577-bb42-4114f44215a0",
      "name": "Setup"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst cfg = input.config;\nconst b = input.body || {};\nconst preview = input.config.dryRun !== false || input.sampleRun === true;\nconst bookingId = String(b.bookingId || '').trim();\nconst uuid = String(b.meetingUuid || '').trim();\nconst email = String(b.inviteeEmail || '').trim().toLowerCase();\nconst host = String(b.hostEmail || '').trim().toLowerCase();\nconst ended = new Date(b.endedAt).getTime();\nif (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/.test(bookingId) || !/^[A-Za-z0-9+/=_-]{8,120}$/.test(uuid) || !/^[^\\s@<>,\";]+@[^\\s@<>,\";]+\\.[^\\s@<>,\";]+$/.test(email) ||\n    !/^[^\\s@<>,\";]+@[^\\s@<>,\";]+\\.[^\\s@<>,\";]+$/.test(host) || email === host || typeof b.endedAt !== 'string' || !/T.*(?:Z|[+-]\\d{2}:\\d{2})$/.test(b.endedAt) || !Number.isFinite(ended)) {\n  throw new Error('Provide a booking ID, Zoom occurrence UUID, ended timestamp and two distinct email addresses.');\n}\nif (!Number.isInteger(cfg.reportDelayMinutes) || cfg.reportDelayMinutes < 5 || cfg.reportDelayMinutes > 1440) throw new Error('Set a report delay between 5 and 1440 minutes.');\nif (!preview && (!cfg.tableId || ended > Date.now() - cfg.reportDelayMinutes*60000)) throw new Error('Select a state table and wait for the Zoom report before submitting this event.');\nif (cfg.bookingUrl && !/^https:\\/\\/calendly\\.com\\/[A-Za-z0-9/_-]+$/.test(cfg.bookingUrl)) throw new Error('Set a Calendly scheduling URL or leave it blank.');\nreturn [{json:{preview,bookingId,uuid,email,host,givenName:String(b.inviteeName || '').trim().split(/\\s+/)[0].slice(0,50)}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -75,
        150
      ],
      "id": "eee2ef1e-fc8d-5b7d-aa45-9efa7e19e88a",
      "name": "Validate completed booking"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "430a5370-915e-59c1-beb1-cf85e8785c5a",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        160,
        150
      ],
      "id": "0917c4ea-6e44-5c59-97d3-008a8cf52768",
      "name": "Check live Zoom report?"
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.tableId }}",
          "mode": "id"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "event_key",
              "condition": "eq",
              "keyValue": "={{ $(\"Validate completed booking\").first().json.bookingId }}"
            }
          ]
        },
        "returnAll": false,
        "limit": 2
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        395,
        150
      ],
      "id": "5bae763d-ee63-511a-a372-7e59b9194fb5",
      "name": "Find previous booking",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst rows = $input.all().map(item=>item.json).filter(row=>row.event_key); if (rows.length > 1) throw new Error('Resolve duplicate state rows before retrying.'); return [{json:{exists:rows.length === 1,status:rows[0]?.status || ''}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        630,
        150
      ],
      "id": "db0c1182-beae-535b-8e04-15ee2c4479f2",
      "name": "Check previous booking"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "d301afa7-cdf3-578a-bf68-2d3aa8b10d0c",
              "leftValue": "={{ !$json.exists }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        865,
        150
      ],
      "id": "a3d54247-a4a5-5079-a52e-3284c195b93f",
      "name": "New booking check?"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.zoom.us/v2/report/meetings/'+ ($(\"Validate completed booking\").first().json.uuid.startsWith('/') || $(\"Validate completed booking\").first().json.uuid.includes('//') ? encodeURIComponent(encodeURIComponent($(\"Validate completed booking\").first().json.uuid)) : encodeURIComponent($(\"Validate completed booking\").first().json.uuid)) + '/participants?page_size=300' }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "zoomOAuth2Api"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1100,
        150
      ],
      "id": "d5634f0f-1026-561a-99e0-cf38768d11a1",
      "name": "Read Zoom attendance"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst b = $(\"Validate completed booking\").first().json;\nconst cfg = $(\"Setup\").first().json.config;\nconst report = b.preview ? {\"participants\": [{\"user_email\": \"sales@example.com\", \"duration\": 1800}], \"next_page_token\": \"\"} : input;\nif (!Array.isArray(report.participants)) throw new Error('Zoom did not return a participant list.');\nconst emails = report.participants.map(row=>String(row.user_email || '').trim().toLowerCase());\nconst joined = emails.includes(b.email);\nconst reasons = [];\nif (report.next_page_token) reasons.push('The participant report has additional pages');\nif (!emails.includes(b.host)) reasons.push('The expected host is missing from the report');\nif (emails.some(value=>!/^[^\\s@<>,\";]+@[^\\s@<>,\";]+\\.[^\\s@<>,\";]+$/.test(value))) reasons.push('At least one participant has no usable email');\nif (cfg.verifiedZoomIdentityRequired !== true) reasons.push('Email-based attendance has not been enabled');\nconst status = joined ? 'attended' : reasons.length ? 'needs_review' : 'no_show';\nconst message = [\n  'Hi ' + (b.givenName || 'there') + ',', '', 'We missed you at our demo. If you’d still like to talk, I’d be happy to find another time.',\n  cfg.bookingUrl ? 'You can pick a time here: '+cfg.bookingUrl : 'Reply with a time that works for you.', '', 'Thanks!'\n].join('\\n');\nreturn [{json:{...b,status,reason:joined?'Invitee email was present in Zoom':reasons.join(' | ') || 'Invitee email absent from the completed report',\n  subject:'Would you like to reschedule your demo?',message}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1335,
        150
      ],
      "id": "9f7b7005-0d7a-5ae7-b86e-ac630be1ff78",
      "name": "Classify attendance"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "761381eb-3597-5e7e-b25b-327166f99807",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1570,
        150
      ],
      "id": "897b9218-f2a8-5c66-aff9-18c375e171e0",
      "name": "Store live attendance result?"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "f8935e27-14c7-5ec8-a05f-fe0b2c7063db",
              "leftValue": "={{ $json.status === \"no_show\" && $(\"Setup\").first().json.config.createGmailDraft === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1805,
        150
      ],
      "id": "9b8527bc-b540-5d60-9f40-320fbaa5bdb0",
      "name": "Create rescheduling draft?"
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.tableId }}",
          "mode": "id"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "event_key",
              "condition": "eq",
              "keyValue": "={{ $(\"Classify attendance\").first().json.bookingId }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "event_key": "={{ $(\"Classify attendance\").first().json.bookingId }}",
            "status": "={{ $(\"Classify attendance\").first().json.status }}",
            "detail": "={{ $(\"Classify attendance\").first().json.reason }}",
            "result_id": "={{ \"\" }}"
          },
          "schema": [
            {
              "id": "event_key",
              "displayName": "event_key",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "detail",
              "displayName": "detail",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "result_id",
              "displayName": "result_id",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            }
          ],
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2040,
        150
      ],
      "id": "637b3179-b386-55c4-b321-2a468b82190c",
      "name": "Record attendance result"
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.tableId }}",
          "mode": "id"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "event_key",
              "condition": "eq",
              "keyValue": "={{ $(\"Classify attendance\").first().json.bookingId }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "event_key": "={{ $(\"Classify attendance\").first().json.bookingId }}",
            "status": "={{ \"draft_pending\" }}",
            "detail": "={{ $(\"Classify attendance\").first().json.reason }}",
            "result_id": "={{ \"\" }}"
          },
          "schema": [
            {
              "id": "event_key",
              "displayName": "event_key",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "detail",
              "displayName": "detail",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "result_id",
              "displayName": "result_id",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            }
          ],
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2275,
        150
      ],
      "id": "8c1db24f-51cb-583a-97be-6dc4486da153",
      "name": "Reserve rescheduling draft"
    },
    {
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $(\"Classify attendance\").first().json.subject }}",
        "message": "={{ $(\"Classify attendance\").first().json.message }}",
        "options": {
          "sendTo": "={{ $(\"Classify attendance\").first().json.email }}"
        }
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2510,
        150
      ],
      "id": "b135c829-a6d5-51b8-b3d9-d4f437ad150f",
      "name": "Create Gmail rescheduling draft"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nif (!input.id) throw new Error(\"Check Gmail for an existing draft before retrying.\"); return [{json:{id:input.id}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2745,
        150
      ],
      "id": "1e5ddca2-46c6-5213-92d6-a3d56f3156d8",
      "name": "Check rescheduling draft ID"
    },
    {
      "parameters": {
        "resource": "row",
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.tableId }}",
          "mode": "id"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "event_key",
              "condition": "eq",
              "keyValue": "={{ $(\"Classify attendance\").first().json.bookingId }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "event_key": "={{ $(\"Classify attendance\").first().json.bookingId }}",
            "status": "={{ \"draft_ready\" }}",
            "detail": "={{ $(\"Classify attendance\").first().json.reason }}",
            "result_id": "={{ $json.id }}"
          },
          "schema": [
            {
              "id": "event_key",
              "displayName": "event_key",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "detail",
              "displayName": "detail",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "result_id",
              "displayName": "result_id",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            }
          ],
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2980,
        150
      ],
      "id": "b784b383-d25b-5fe8-a096-818181012572",
      "name": "Record rescheduling draft"
    }
  ],
  "connections": {
    "Run with sample data": {
      "main": [
        [
          {
            "node": "Sample input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample input": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive event": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup": {
      "main": [
        [
          {
            "node": "Validate completed booking",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate completed booking": {
      "main": [
        [
          {
            "node": "Check live Zoom report?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check live Zoom report?": {
      "main": [
        [
          {
            "node": "Find previous booking",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Classify attendance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find previous booking": {
      "main": [
        [
          {
            "node": "Check previous booking",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check previous booking": {
      "main": [
        [
          {
            "node": "New booking check?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New booking check?": {
      "main": [
        [
          {
            "node": "Read Zoom attendance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Zoom attendance": {
      "main": [
        [
          {
            "node": "Classify attendance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify attendance": {
      "main": [
        [
          {
            "node": "Store live attendance result?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store live attendance result?": {
      "main": [
        [
          {
            "node": "Create rescheduling draft?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create rescheduling draft?": {
      "main": [
        [
          {
            "node": "Reserve rescheduling draft",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Record attendance result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reserve rescheduling draft": {
      "main": [
        [
          {
            "node": "Create Gmail rescheduling draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Gmail rescheduling draft": {
      "main": [
        [
          {
            "node": "Check rescheduling draft ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check rescheduling draft ID": {
      "main": [
        [
          {
            "node": "Record rescheduling draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "all"
  },
  "pinData": {},
  "tags": []
}
