Cal.com Booking Intake
Turns Cal.com bookings into CRM leads and meetings, creating the person in Core when needed.
- Platform
- n8n · Open in n8n
- Status
- Active
- Workflow ID
QtaJdIPuUy8l6AXn- Nodes
- 32
- n8n version
d1323e82-7bd7-4179-b1d9-2c00b8a9f260- Last saved in n8n
- 2026-11-06T21:25:19.572Z
Cal.com Booking Intake turns a Cal.com booking made on a seller’s landing page into CRM records. A new booking becomes a Deal (the lead) at Meeting Scheduled, or moves the person’s open Deal with that seller to Meeting Scheduled, plus an Interaction (the meeting) for the right seller, and the person is created in Core when nobody has that e-mail yet. A reschedule moves the same meeting; a cancellation or rejection marks it cancelled and sets the meeting’s next contact to today. One webhook serves every seller’s Cal.com account.
The CRM maintainer owns the seller routing and recovery; the Core maintainer reviews the people it creates.
Flow
Generated from the n8n version in the box above: every box is a node and every arrow is a connection in n8n. Pink pills are triggers, yellow hexagons are If and Switch nodes with their outputs on the arrows, blue cylinders read (Airtable, Data Table, HTTP GET), green cylinders write, purple boxes call another workflow, and dashed arrows are error outputs.
Mermaid source
flowchart TD
subgraph g0["Receive and verify"]
n0(["Cal.com Webhook"]):::trigger
n26["Sign Body"]:::logic
n29["Verify & Parse"]:::logic
n13{{"Is New Booking?"}}:::decision
n14{{"Is Reschedule?"}}:::decision
end
subgraph g1["New booking"]
n8[("Find Duplicate<br/><small>GET CRM Interaction</small>")]:::read
n12[("Get Lead Sources<br/><small>GET CRM Lead Source</small>")]:::read
n11[("Find Person (CRM)<br/><small>GET CRM People</small>")]:::read
n25["Resolve"]:::logic
n17{{"Needs Core?"}}:::decision
n10[("Find Person (Core)<br/><small>GET Core People</small>")]:::read
n5{{"Exists in Core?"}}:::decision
n3[("Create Person (Core)<br/><small>POST Core People</small>")]:::write
n30["Wait 1 min"]:::wait
n24[("Recheck Person (CRM)<br/><small>GET CRM People</small>")]:::read
n27["Sync Check"]:::logic
n19{{"Person Ready?"}}:::decision
n7[("Find Deals<br/><small>GET CRM Deal</small>")]:::read
n21["Prepare Deal"]:::logic
n4{{"Existing Deal?"}}:::decision
n28[("Update Deal<br/><small>PATCH CRM Deal</small>")]:::write
n1[("Create Deal<br/><small>POST CRM Deal</small>")]:::write
n22["Prepare Interaction"]:::logic
n2[("Create Interaction<br/><small>POST CRM Interaction</small>")]:::write
end
subgraph g2["Reschedule"]
n9[("Find Original Meeting<br/><small>GET CRM Interaction</small>")]:::read
n23["Prepare Reschedule"]:::logic
n18{{"Original Found?"}}:::decision
n16[("Move Meeting<br/><small>PATCH CRM Interaction</small>")]:::write
end
subgraph g3["Cancellation and rejection"]
n6[("Find Booked Meeting<br/><small>GET CRM Interaction</small>")]:::read
n20["Prepare Cancellation"]:::logic
n15[("Mark Meeting Cancelled<br/><small>PATCH CRM Interaction</small>")]:::write
end
n0 --> n26
n26 --> n29
n29 --> n13
n13 -->|true| n8
n13 -->|false| n14
n14 -->|true| n9
n14 -->|false| n6
n9 --> n23
n23 --> n18
n18 -->|true| n16
n18 -->|false| n8
n6 --> n20
n20 --> n15
n8 --> n12
n12 --> n11
n11 --> n25
n25 --> n17
n17 -->|true| n10
n17 -->|false| n7
n10 --> n5
n5 -->|true| n30
n5 -->|false| n3
n3 --> n30
n30 --> n24
n24 --> n27
n27 --> n19
n19 -->|true| n7
n19 -->|false| n30
n7 --> n21
n21 --> n4
n4 -->|true| n28
n4 -->|false| n1
n1 --> n22
n22 --> n2
n28 --> n22
classDef trigger fill:#fde7f3,stroke:#d6409f,color:#111
classDef decision fill:#fff7e0,stroke:#c99a00,color:#111
classDef read fill:#e8f1ff,stroke:#3b82f6,color:#111
classDef write fill:#e7f8ee,stroke:#16a34a,color:#111
classDef subflow fill:#f3e8ff,stroke:#9333ea,color:#111
classDef wait fill:#f1f1f1,stroke:#888,color:#111
classDef logic fill:#ffffff,stroke:#999,color:#111Systems it touches
| System | Table | Reads | Writes |
|---|---|---|---|
| CRM | Interaction | Find Duplicate, Find Original Meeting, Find Booked Meeting | Create Interaction, Move Meeting, Mark Meeting Cancelled |
| CRM | Deal | Find Deals | Create Deal, Update Deal |
| CRM | People (mirror of Core People) | Find Person (CRM), Recheck Person (CRM) | Nothing |
| CRM | Automation: Lead Source | Get Lead Sources | Nothing |
| Core | People | Find Person (Core) | Create Person (Core) |
Every Airtable call is an HTTP Request node with one Airtable token credential; the workflow uses no Airtable node. Reads filter by field name ({Booking UID}, {Email} in the CRM mirror, {Email*} in Core); writes use field IDs. Renaming one of those three fields breaks the lookups without any error: the formula fails and the node returns nothing.
Other systems only register the workflow. Communication lists it as the Webhook channel cal-com-bookings → n8n → CRM. Systems Central has a System row for it. The Login and Tool Identifier it matches on are lookups that reach the CRM from Tooling and Access through the Tool Account mirror.
Trigger and signature
Cal.com Webhook accepts a POST on a fixed path. It uses the default response mode, so Cal.com gets HTTP 200 as soon as the request arrives, before any check or write. A 200 in Cal.com’s webhook log only says that n8n received the call.
Sign Body computes HMAC-SHA256 over JSON.stringify(body) with the Cal.com webhook secret. Verify & Parse compares the result with the x-cal-signature-256 header and throws “Invalid Cal.com signature (x-cal-signature-256)” when they differ or the header is missing. That execution ends in error and writes nothing.
Verify & Parse continues only for BOOKING_CREATED, BOOKING_RESCHEDULED, BOOKING_CANCELLED and BOOKING_REJECTED. Any other event returns no items and the execution ends there.
What Verify & Parse reads from the payload
| Value | Taken from, in this order | When missing |
|---|---|---|
| First and last name | responses.name (object with firstName/lastName, or text split on the first space), then the first attendee’s name |
Full name becomes “Cal.com lead” |
| Lead e-mail | responses.email, then the first attendee’s e-mail; trimmed and lower-cased |
Every e-mail lookup becomes FALSE() |
| Phone | responses.attendeePhoneNumber, responses.phone, then the attendee’s phone number |
Not written |
| Lead notes | additionalNotes, responses.notes, then description |
Not written |
| Seller account keys | Organizer e-mail, username and id, plus externalId and primaryEmail of each destination calendar; lower-cased |
No seller match |
| Meeting start | startTime |
The time n8n processed the call |
| Meeting day and today | Calendar date in the organizer’s time zone | Time zone America/Sao_Paulo; an invalid zone falls back to the UTC date |
| Booking UID | uid, then bookingId |
The duplicate and cancellation lookups become FALSE() |
| Original UID (reschedule) | rescheduleUid, fromReschedule, then originalRescheduledBooking.uid |
The original lookup becomes FALSE() |
| Reason | cancellationReason, then rejectionReason |
Not appended |
| Meeting URL | metadata.videoCallUrl, videoCallData.url, location when it starts with http, then https://app.cal.com/booking/<uid> |
Not written |
| Title | title |
“Cal.com meeting” |
New booking
BOOKING_CREATED goes through these steps in order.
- Duplicate check. Find Duplicate looks for any Interaction with the same Booking UID. Resolve stops the execution when one exists.
- Seller routing. Get Lead Sources reads Automation: Lead Source. Resolve takes the first row that is Active, whose Tool includes
cal.com, and whose Tool Identifier or Login equals one of the seller account keys. The row’s Follow up Target: Person becomes the accountable person and Follow up Target: Company the accountable company. When no row matches, the flow continues without a seller. - Person in the CRM. Find Person (CRM) looks for the e-mail in the CRM People mirror. When it is found, the flow goes straight to step 5.
- Person in Core. With no CRM person and an e-mail present, Find Person (Core) searches Core People. When the person is not there either, Create Person (Core) adds them with the fields under Fields written. Either way the flow then waits for the CRM mirror: Wait 1 min, Recheck Person (CRM), Sync Check. It leaves the loop when the person appears or after the tenth attempt, about ten minutes. Without an e-mail, steps 3 and 4 find nobody and nothing is created in Core.
- Deal. Find Deals reads the Deals linked to the person (their Deals (Counterparty)). Prepare Deal keeps the open ones (Deal Status Open) and reuses the first whose Accountable (Person)* contains the routed person, or, with no seller routed, the first one with no accountable person. Update Deal moves that Deal to Meeting Scheduled when its Deal Stage* is Lead, Contacted or empty; a later stage (Meeting Scheduled, Pitched, Proposal Sent, Negotiation) is kept. Without a match, or without a CRM person, Create Deal adds a new Deal at Meeting Scheduled.
- Interaction. Create Interaction adds the meeting, linked to that Deal in Deal° and to the person in People°, with Result Meeting booked. No Relationship is created, and the flow does not set Campaign° on the Deal.
Reschedule
BOOKING_RESCHEDULED runs Find Original Meeting with the original UID. Prepare Reschedule keeps only an Interaction whose Result is Meeting booked.
When it finds one, Move Meeting updates that same Interaction: Date*, Title*, Booking UID (now the new UID), Next Contact Date° and Next Action°, the Evidence URL when the payload has a meeting URL, and a line appended to Note. The Deal does not change, and neither do Result and Accountable.
When it finds none (the original was cancelled or never reached the CRM), the item goes to Find Duplicate; see Known gaps for what happens there.
Cancellation and rejection
BOOKING_CANCELLED and BOOKING_REJECTED run Find Booked Meeting with the current UID. Prepare Cancellation picks the Interaction whose Result is Meeting booked. When there is none (already cancelled, or the booking never reached the CRM) the execution ends without writing.
Mark Meeting Cancelled sets Result to Meeting cancelled, Next Contact Date° to today, Next Action° to “Meeting cancelled, reach out to rebook” (“rejected” for a rejection) and appends a line with the reason to Note. That Interaction is where the follow-up shows. The Deal does not change: its Deal Stage* stays at Meeting Scheduled, or wherever it was.
Fields written
Core People (Create Person (Core))
| Field | Value |
|---|---|
| First Name* | First name, or the full name when there is no first name |
| Last Name* | Last name, only when present |
| Email* | Lead e-mail |
| Telephone* | Phone, only when present |
| Status | Proposed |
| Tags | cal-lead-n8n |
| Notes | “Created by n8n from a Cal.com booking (uid …) on <date>.” plus the Lead Source name and the Cal.com account |
CRM Deal
The workflow writes nothing to Relationship, which was deleted on 2026-11-04.
| Field | Create Deal | Update Deal |
|---|---|---|
| Deal Stage* | Meeting Scheduled | Meeting Scheduled when it was Lead, Contacted or empty; otherwise the current stage |
| Source° | Inbound | Not changed |
| Notes° | Lead card, lead notes, and warnings when the person or the seller is missing | Not changed |
| Tags | cal-lead-n8n | Not changed |
| People° | The CRM person, when found | Not changed |
| Accountable (Person)* | Routed person, when found | Not changed |
| Accountable (Company)* | Routed company, when found | Not changed |
Company° is not written; the seller fills it when the lead’s company is known.
CRM Interaction
| Field | Create Interaction | Move Meeting | Mark Meeting Cancelled |
|---|---|---|---|
| Deal° | The Deal from step 5 | Not changed | Not changed |
| People° | The CRM person, when found | Not changed | Not changed |
| Date* | Meeting start | New start | Not changed |
| Title* | Meeting title | New title | Not changed |
| Type | Meeting | Not changed | Not changed |
| Result | Meeting booked | Not changed | Meeting cancelled |
| Note | Lead card, lead notes, location, Cal.com account, Lead Source name | Appends “Rescheduled via Cal.com on <today> from <old start>.” | Appends “Meeting cancelled via Cal.com on <today>.” and the reason |
| Booking UID | Booking UID | New UID | Not changed |
| Evidence URL | Meeting URL, when present | New meeting URL, when present | Not changed |
| Next Contact Date° | Meeting day | New meeting day | Today |
| Next Action° | “Meeting booked via Cal.com: <title>” | “Meeting rescheduled via Cal.com: <title>” | “Meeting cancelled (or rejected), reach out to rebook” |
| Accountable (Person) | Routed person, when found | Not changed | Not changed |
| Accountable (Company) | Routed company, when found | Not changed | Not changed |
| Tags | cal-lead-n8n | Not changed | Not changed |
Company° is not written. Writes to Deal, Interaction and Core People send typecast: true, so a select value that does not exist yet (a renamed Result or Deal Stage*, for example) is created as a new option instead of failing.
Nodes
The workflow has 32 nodes: 31 that run and one sticky note. For an If node, output 0 is true and output 1 is false.
| Node | Type | What it does |
|---|---|---|
| Cal.com Webhook | webhook | Receives the POST from Cal.com and answers at once |
| Sign Body | crypto | HMAC-SHA256 of the parsed body with the Cal.com secret |
| Verify & Parse | code | Checks the signature, drops other events, normalizes the booking |
| Is New Booking? | if | BOOKING_CREATED or not |
| Find Duplicate | httpRequest | CRM Interaction with the same Booking UID |
| Get Lead Sources | httpRequest | All rows of CRM Automation: Lead Source (first page) |
| Find Person (CRM) | httpRequest | CRM People mirror by e-mail |
| Resolve | code | Stops on duplicate, matches the seller, prepares the Core person |
| Needs Core? | if | No CRM person and an e-mail exists |
| Find Person (Core) | httpRequest | Core People by e-mail |
| Exists in Core? | if | Core person found or not |
| Create Person (Core) | httpRequest | POST a Proposed person to Core People |
| Wait 1 min | wait | Waits one minute for the mirror sync |
| Recheck Person (CRM) | httpRequest | CRM People mirror by e-mail again |
| Sync Check | code | Counts attempts; done when found or after 10 |
| Person Ready? | if | Leave the loop or wait again |
| Find Deals | httpRequest | The person’s CRM Deals |
| Prepare Deal | code | Picks the seller’s open Deal and its new stage, or builds a new Deal |
| Existing Deal? | if | Update or create |
| Update Deal | httpRequest | PATCH Deal Stage* of the reused Deal |
| Create Deal | httpRequest | POST a new Deal at Meeting Scheduled |
| Prepare Interaction | code | Builds the meeting Interaction |
| Create Interaction | httpRequest | POST the Interaction |
| Is Reschedule? | if | BOOKING_RESCHEDULED or cancellation/rejection |
| Find Original Meeting | httpRequest | CRM Interaction by the original Booking UID |
| Prepare Reschedule | code | Picks the Meeting booked Interaction and builds its patch |
| Original Found? | if | Original Meeting booked Interaction found or not |
| Move Meeting | httpRequest | PATCH the original Interaction |
| Find Booked Meeting | httpRequest | CRM Interaction by the current Booking UID |
| Prepare Cancellation | code | Picks the Meeting booked Interaction and builds its patch |
| Mark Meeting Cancelled | httpRequest | PATCH the Interaction to Meeting cancelled |
| Sticky Note | stickyNote | Setup note on the canvas; does not run |
Connections
| From | Output | To |
|---|---|---|
| Cal.com Webhook | 0 | Sign Body |
| Sign Body | 0 | Verify & Parse |
| Verify & Parse | 0 | Is New Booking? |
| Is New Booking? | 0 | Find Duplicate |
| Is New Booking? | 1 | Is Reschedule? |
| Find Duplicate | 0 | Get Lead Sources |
| Get Lead Sources | 0 | Find Person (CRM) |
| Find Person (CRM) | 0 | Resolve |
| Resolve | 0 | Needs Core? |
| Needs Core? | 0 | Find Person (Core) |
| Needs Core? | 1 | Find Deals |
| Find Person (Core) | 0 | Exists in Core? |
| Exists in Core? | 0 | Wait 1 min |
| Exists in Core? | 1 | Create Person (Core) |
| Create Person (Core) | 0 | Wait 1 min |
| Wait 1 min | 0 | Recheck Person (CRM) |
| Recheck Person (CRM) | 0 | Sync Check |
| Sync Check | 0 | Person Ready? |
| Person Ready? | 0 | Find Deals |
| Person Ready? | 1 | Wait 1 min |
| Find Deals | 0 | Prepare Deal |
| Prepare Deal | 0 | Existing Deal? |
| Existing Deal? | 0 | Update Deal |
| Existing Deal? | 1 | Create Deal |
| Update Deal | 0 | Prepare Interaction |
| Create Deal | 0 | Prepare Interaction |
| Prepare Interaction | 0 | Create Interaction |
| Is Reschedule? | 0 | Find Original Meeting |
| Is Reschedule? | 1 | Find Booked Meeting |
| Find Original Meeting | 0 | Prepare Reschedule |
| Prepare Reschedule | 0 | Original Found? |
| Original Found? | 0 | Move Meeting |
| Original Found? | 1 | Find Duplicate |
| Find Booked Meeting | 0 | Prepare Cancellation |
| Prepare Cancellation | 0 | Mark Meeting Cancelled |
Settings and error handling
| Setting | Value |
|---|---|
| Execution order | v1 |
| Error workflow | None |
| Retry on fail | Off on every node |
| On error | Stop the workflow (default) on every node |
| Caller policy | Workflows from the same owner |
| Available in MCP | Yes |
| Execution data retention | Instance default; this workflow sets none |
There is no schedule; the workflow runs only when Cal.com calls it. A failed Airtable call stops the execution where it is, after any earlier writes have already been saved.
Known gaps
These come from the node configuration. The Deal version was published on 2026-11-04 and tested only in a local simulation (four cases); no real booking has run through it yet. With the earlier Relationship version, three paths ran in n8n: a new booking for an existing person, a reschedule with the original found, and a cancellation.
| Gap | Effect |
|---|---|
| A reschedule whose original is not found reaches Find Duplicate without the duplicate formula | Find Duplicate filters on an empty formula, Airtable returns the first Interaction of the table and Resolve stops. The rescheduled booking writes nothing |
| The webhook secret sits in the Sign Body parameters, not in an n8n credential | Anyone who can open the workflow can read it, and a workflow export carries it |
| The signature is computed over the parsed body, not the raw request | A payload whose re-serialized JSON differs from what Cal.com signed fails as an invalid signature |
| Cal.com gets 200 before the signature check | A rejected or failed delivery looks successful in Cal.com; only n8n’s execution list shows the error |
| The duplicate check is a lookup, not a lock | Two deliveries of the same booking at the same moment can both create records |
| Get Lead Sources and Find Deals read one page (up to 100 records) | Above 100 Lead Source rows, or 100 Deals for one person, a match can be missed |
| A booking without e-mail, or a person still missing from the mirror after ten minutes, creates a Deal without People° and an Interaction without People° | The Notes° line always says the person did not reach the mirror in time, even when the cause is a missing e-mail. A later booking from the same person creates another Deal |
| Only open Deals are reused | A booking from a person whose Deal with that seller is Won, Lost or Abandoned creates a new Deal at Meeting Scheduled |
| Company° is never written on the Deal or the Interaction | The Deal’s Name shows only the person until the seller links the company |
Recover a partial write
Look at what was saved before you replay a delivery.
| What was saved | What to do |
|---|---|
| Core person created, CRM mirror still missing | Wait for or fix the sync, then link the person in People° of the new Deal and of the Interaction |
| Deal written, Interaction failed | Search Interaction by Booking UID before replaying; replay reuses the open Deal when it has People° and the same seller, and creates the Interaction |
| Two records for one Booking UID | Keep one, move its links, then delete the other; nothing merges them automatically |
Keep this page in sync
The box at the top shows the n8n version this page describes. Open the workflow in n8n: when its last save is newer than the date in the box, this page is out of date until the next review. Every node and every connection of that version is listed in Nodes and Connections.