{
  "name": "Keep Calendly Bookings in Sync with Airtable and Slack | Uptune",
  "nodes": [
    {
      "parameters": {
        "content": "## Keep Calendly Bookings in Sync with Airtable and Slack\nRecord each booked invitee in Airtable, match the Google Calendar event, and notify sales in Slack.\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": "6aab0652-98f5-5e1b-9806-03a617e43528",
      "name": "Start here"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -720,
        80
      ],
      "id": "767021e0-f319-5a3e-9b9f-09a3f80e602e",
      "name": "Run with sample data"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nreturn [{json: {sampleRun: true, body: {\"event\": \"invitee.created\", \"payload\": {\"uri\": \"https://api.calendly.com/scheduled_events/sample-event/invitees/sample-invitee\", \"email\": \"avery@example.com\", \"name\": \"Avery Morgan\", \"scheduled_event\": {\"uri\": \"https://api.calendly.com/scheduled_events/sample-event\", \"name\": \"Discovery call\", \"start_time\": \"2027-01-12T16:00:00Z\", \"end_time\": \"2027-01-12T16:30:00Z\"}}}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -480,
        80
      ],
      "id": "0e9a6b14-9269-54c4-9af8-2b996d8b87da",
      "name": "Sample input"
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "events": [
          "invitee.created"
        ],
        "scope": "user"
      },
      "type": "n8n-nodes-base.calendlyTrigger",
      "typeVersion": 1,
      "position": [
        -480,
        300
      ],
      "id": "58042de6-c9f5-586c-b381-4ee3bc78314a",
      "name": "Receive event",
      "webhookId": "58042de6-c9f5-586c-b381-4ee3bc78314a"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst config = {\n  \"dryRun\": true,\n  \"calendarId\": \"\",\n  \"airtableBaseId\": \"\",\n  \"contactsTableId\": \"\",\n  \"meetingsTableId\": \"\",\n  \"slackChannelId\": \"\",\n  \"calendarWaitSeconds\": 30,\n  \"addInternalAttendeeEmail\": \"\"\n};\nreturn [{json: {...input, config}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        150
      ],
      "id": "941d1044-0f96-5012-9866-80b4fc91ef7a",
      "name": "Setup"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst config = input.config;\nconst event = input.body || input;\nconst booking = event.payload || {};\nconst scheduled = booking.scheduled_event || {};\nconst email = String(booking.email || '').trim().toLowerCase();\nconst bookingId = String(booking.uri || '').trim();\nconst match = bookingId.match(/^https:\\/\\/api\\.calendly\\.com\\/scheduled_events\\/([A-Za-z0-9-]+)\\/invitees\\/([A-Za-z0-9-]+)$/);\nconst start = new Date(scheduled.start_time);\nconst end = new Date(scheduled.end_time);\nconst preview = config.dryRun !== false || input.sampleRun === true;\nif (event.event !== 'invitee.created' || !match || !/^[^\\s@<>,\";]+@[^\\s@<>,\";]+\\.[^\\s@<>,\";]+$/.test(email) || isNaN(start) || isNaN(end) || end <= start) {\n  throw new Error('An invitee.created event with an invitee URI, email, and valid meeting times is required.');\n}\nif (!preview && (!config.calendarId || !config.airtableBaseId || !config.contactsTableId || !config.meetingsTableId ||\n    !Number.isInteger(config.calendarWaitSeconds) || config.calendarWaitSeconds < 1 || config.calendarWaitSeconds > 600 ||\n    (config.addInternalAttendeeEmail && !/^[^\\s@<>,\";]+@[^\\s@<>,\";]+\\.[^\\s@<>,\";]+$/.test(config.addInternalAttendeeEmail)))) {\n  throw new Error('Complete the account IDs, calendar wait time and optional internal attendee in Setup.');\n}\nreturn [{json:{bookingId,email,preview,start:start.toISOString(),end:end.toISOString(),\n  eventName:String(scheduled.name || 'Calendly booking').slice(0,180),name:String(booking.name || '').slice(0,180)}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -75,
        150
      ],
      "id": "e9a35c41-8181-5b67-ad1e-d7c2a58c2447",
      "name": "Normalize Calendly booking"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "99ab6450-21b4-51ad-b73d-fc502939e694",
              "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": "04ca9951-684e-54db-b93a-b37319f6764d",
      "name": "Read live calendar?"
    },
    {
      "parameters": {
        "amount": "={{ $(\"Setup\").first().json.config.calendarWaitSeconds }}",
        "unit": "seconds"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        395,
        150
      ],
      "id": "3542122c-0cf4-5644-b8d7-d97d0ab08e35",
      "name": "Wait for calendar sync",
      "webhookId": "3542122c-0cf4-5644-b8d7-d97d0ab08e35"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/' + encodeURIComponent($(\"Setup\").first().json.config.calendarId) + '/events' + '?singleEvents=true&maxResults=250&timeMin=' + encodeURIComponent($(\"Normalize Calendly booking\").first().json.start) + '&timeMax=' + encodeURIComponent($(\"Normalize Calendly booking\").first().json.end) }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "googleCalendarOAuth2Api"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        630,
        150
      ],
      "id": "48464608-38c1-58ad-b752-e623029ffa23",
      "name": "Find calendar events"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst booking = $(\"Normalize Calendly booking\").first().json;\nif (!booking.preview && !Array.isArray(input.items)) throw new Error('Google Calendar did not return an event list.');\nconst events = booking.preview ? [{\n  id:'sample-calendar-event',etag:'\"sample\"',start:{dateTime:booking.start},end:{dateTime:booking.end},attendees:[{email:booking.email}]\n}] : input.items;\nconst matches = events.filter(event => event.status !== 'cancelled' &&\n  Date.parse(event.start?.dateTime) === Date.parse(booking.start) && Date.parse(event.end?.dateTime) === Date.parse(booking.end) &&\n  (event.attendees || []).some(person => String(person.email || '').toLowerCase() === booking.email));\nconst status = input.nextPageToken || matches.length > 1 ? 'ambiguous' : matches.length === 1 && matches[0].id ? 'matched' : 'missing';\nconst event = status === 'matched' ? matches[0] : null;\nconst internalEmail = ($(\"Setup\").first().json.config.addInternalAttendeeEmail || '').trim().toLowerCase();\nconst canAdd = !booking.preview && event?.etag && !event.attendeesOmitted && internalEmail &&\n  !event.attendees.some(person => String(person.email || '').toLowerCase() === internalEmail);\nreturn [{json:{...booking,calendarStatus:status,eventId:event?.id || '',etag:event?.etag || '',\n  shouldAddAttendee:Boolean(canAdd),attendees:canAdd ? [...event.attendees.map(person=>({email:person.email,\n    ...(person.optional ? {optional:true} : {}), ...(person.resource ? {resource:true} : {}), ...(person.responseStatus ? {responseStatus:person.responseStatus} : {})})),{email:internalEmail}] : []}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        865,
        150
      ],
      "id": "3886e639-2fe5-588c-b46e-e3708486a474",
      "name": "Match calendar event"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "4bf96835-ad58-5f91-b023-63c10283662f",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        150
      ],
      "id": "6c22892f-36cf-5900-b877-5543dd81299a",
      "name": "Write live booking?"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "0c02d9fa-f737-5663-869f-dcbc4e65ac2b",
              "leftValue": "={{ $json.shouldAddAttendee }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1335,
        150
      ],
      "id": "6d761cdf-6fa7-552c-b4f2-bd04b8e105a6",
      "name": "Add internal attendee?"
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/' + encodeURIComponent($(\"Setup\").first().json.config.calendarId) + '/events' + '/' + encodeURIComponent($(\"Match calendar event\").first().json.eventId) + '?sendUpdates=none' }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "timeout": 30000
        },
        "nodeCredentialType": "googleCalendarOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({attendees:$json.attendees}) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "If-Match",
              "value": "={{ $(\"Match calendar event\").first().json.etag }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1570,
        150
      ],
      "id": "828103b7-983b-58b8-9a27-020e13d55fc1",
      "name": "Add internal calendar attendee"
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ 'https://api.airtable.com/v0/' + encodeURIComponent($(\"Setup\").first().json.config.airtableBaseId) + \"/\" + encodeURIComponent($(\"Setup\").first().json.config.contactsTableId) }}",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({performUpsert:{fieldsToMergeOn:['Email']},records:[{fields:{Email:$(\"Match calendar event\").first().json.email,Name:$(\"Match calendar event\").first().json.name}}]}) }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1805,
        150
      ],
      "id": "1225145f-872e-5e4d-aebb-fee433f8a0ac",
      "name": "Upsert Airtable contact"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nif (input.records?.length !== 1 || !input.records[0].id) throw new Error('Airtable did not confirm one contact.');\nreturn [{json:{contactId:input.records[0].id}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2040,
        150
      ],
      "id": "255db279-101c-5a28-a01e-4cb44a9bdb67",
      "name": "Check contact record"
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ 'https://api.airtable.com/v0/' + encodeURIComponent($(\"Setup\").first().json.config.airtableBaseId) + \"/\" + encodeURIComponent($(\"Setup\").first().json.config.meetingsTableId) }}",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({performUpsert:{fieldsToMergeOn:['Booking ID']},records:[{fields:{'Booking ID':$(\"Match calendar event\").first().json.bookingId,'Contact':[$json.contactId],'Event':$(\"Match calendar event\").first().json.eventName,'Starts At':$(\"Match calendar event\").first().json.start,'Ends At':$(\"Match calendar event\").first().json.end,'Calendar Event ID':$(\"Match calendar event\").first().json.eventId,'Calendar Status':$(\"Match calendar event\").first().json.calendarStatus}}]}) }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        2275,
        150
      ],
      "id": "ea96bcd8-abef-55d1-a7ff-54b8f729f4bd",
      "name": "Upsert Airtable meeting"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nif (input.records?.length !== 1 || !input.records[0].id) throw new Error('Airtable did not confirm one meeting.');\nreturn [{json:{recordId:input.records[0].id,notify:(input.createdRecords || []).includes(input.records[0].id) && Boolean($(\"Setup\").first().json.config.slackChannelId)}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2510,
        150
      ],
      "id": "eb40db3b-bb6a-5bfa-ae75-4b7f07a591cf",
      "name": "Check meeting result"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "9f6f2cb9-3914-597b-af6e-a53144b98361",
              "leftValue": "={{ $json.notify }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2745,
        150
      ],
      "id": "2dfba322-cb27-547f-9706-85553b5dd85c",
      "name": "Notify Slack for new booking?"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://slack.com/api/chat.postMessage",
        "authentication": "genericCredentialType",
        "options": {
          "timeout": 30000
        },
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({channel:$(\"Setup\").first().json.config.slackChannelId,text:['New sales booking',$(\"Match calendar event\").first().json.eventName+' · '+ $(\"Match calendar event\").first().json.start,'Calendar match: '+ $(\"Match calendar event\").first().json.calendarStatus,'Booking: '+ $(\"Match calendar event\").first().json.bookingId].join('\\n'),mrkdwn:false,unfurl_links:false,unfurl_media:false}) }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        2980,
        150
      ],
      "id": "5a598688-2013-5128-84fe-28a3e7fc6871",
      "name": "Notify sales in Slack"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nif (input.ok !== true) throw new Error(\"Slack did not confirm delivery: \" + String(input.error || \"unknown error\")); return [{json:{ok:true}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3215,
        150
      ],
      "id": "dc9d2f6d-e05e-509f-8033-349c22a82f17",
      "name": "Verify Slack delivery"
    }
  ],
  "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": "Normalize Calendly booking",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Calendly booking": {
      "main": [
        [
          {
            "node": "Read live calendar?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read live calendar?": {
      "main": [
        [
          {
            "node": "Wait for calendar sync",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Match calendar event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for calendar sync": {
      "main": [
        [
          {
            "node": "Find calendar events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find calendar events": {
      "main": [
        [
          {
            "node": "Match calendar event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match calendar event": {
      "main": [
        [
          {
            "node": "Write live booking?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write live booking?": {
      "main": [
        [
          {
            "node": "Add internal attendee?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add internal attendee?": {
      "main": [
        [
          {
            "node": "Add internal calendar attendee",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upsert Airtable contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add internal calendar attendee": {
      "main": [
        [
          {
            "node": "Upsert Airtable contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Airtable contact": {
      "main": [
        [
          {
            "node": "Check contact record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check contact record": {
      "main": [
        [
          {
            "node": "Upsert Airtable meeting",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Airtable meeting": {
      "main": [
        [
          {
            "node": "Check meeting result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check meeting result": {
      "main": [
        [
          {
            "node": "Notify Slack for new booking?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Slack for new booking?": {
      "main": [
        [
          {
            "node": "Notify sales in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify sales in Slack": {
      "main": [
        [
          {
            "node": "Verify Slack delivery",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "all"
  },
  "pinData": {},
  "tags": []
}
