# Cal.com Booking Intake

Turns Cal.com bookings into CRM leads and meetings, creating the person in Core when needed.

| Platform | Workflow ID | Status | Version | Last saved | Nodes | Systems |
| --- | --- | --- | --- | --- | --- | --- |
| n8n | QtaJdIPuUy8l6AXn | Active | d1323e82-7bd7-4179-b1d9-2c00b8a9f260 | 2026-11-06T21:25:19.572Z | 32 | CRM, Core |

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
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:#111
```

## Systems 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](/systems/communication/automations) lists it as the Webhook channel `cal-com-bookings → n8n → CRM`. [Systems Central](/systems/systems-central/entity-system) has a System row for it. The Login and Tool Identifier it matches on are lookups that reach the CRM from [Tooling and Access](/systems/tooling/automations) 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.

1. **Duplicate check.** Find Duplicate looks for any Interaction with the same Booking UID. Resolve stops the execution when one exists.
2. **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.
3. **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.
4. **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](#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.
5. **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.
6. **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](#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 &lt;date&gt;." 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 &lt;today&gt; from &lt;old start&gt;." | Appends "Meeting cancelled via Cal.com on &lt;today&gt;." 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: &lt;title&gt;" | "Meeting rescheduled via Cal.com: &lt;title&gt;" | "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](#nodes) and [Connections](#connections).