Skip to content

Cal.com → Odoo CRM

The Cal.com → Odoo CRM workflow automatically syncs Cal.com events with Odoo CRM. It manages the full appointment lifecycle: creation, cancellation, rescheduling and meeting end.

Cal.com eventOdoo actionNotification
BOOKING_CREATEDCreate opportunityMessage + iCal file
BOOKING_CANCELLEDMark as lostCancellation message
BOOKING_RESCHEDULEDUpdate datesMessage + new iCal
MEETING_ENDEDIncrease probabilityMeeting-end message

ProblemWithout syncWith sync
Double entryManually create the opportunity after the meetingAutomatic
Missed follow-upNo reminder after the meetingCRM stage updated
Scattered calendarCal.com appointments ≠ work calendariCal file sent
Untracked cancellationsLose the cancellation infoLead marked as lost

BOOKING_CREATED · probability 10%

MEETING_ENDED · probability 30%

Manual qualification · probability 50%+

Won

Lost


Cal.com webhook

Switch · triggerEvent

BOOKING_CREATED → Create opportunity

BOOKING_CANCELLED → Mark Lost

BOOKING_RESCHEDULED → Update dates

MEETING_ENDED → Update stage

Notify + iCal

Notify

BOOKING_CREATED

Create Opportunity Odoo · probability 10

Load All Admins · Data Table

Format Booking Notification

Loop · for each admin

Generate iCal File

Send Calendar File · Telegram

📅 New Cal.com appointment
👤 Client: Jean Dupont
📧 Email: jean.dupont@example.com
🗓️ Date: Thursday 20 January 2026
⏰ Time: 14:00 - 15:00
📋 Type: Discovery call
🔗 Opportunity: https://odoo.guigpap.com/odoo/crm/123

Cal.com Trigger:

Events:
- BOOKING_CREATED
- BOOKING_CANCELLED
- BOOKING_RESCHEDULED
- MEETING_ENDED

Switch Node (Route by Event):

Type: Switch
Mode: Rules
Value: {{ $json.triggerEvent }}
Rules:
- Output 0: BOOKING_CREATED
- Output 1: BOOKING_CANCELLED
- Output 2: BOOKING_RESCHEDULED
- Output 3: MEETING_ENDED

Find Opportunity (Odoo):

Resource: Custom
Custom Resource: crm.lead
Operation: Get Many
Limit: 1
Filter:
- Field: email_from
Operator: =
Value: {{ $json.attendees[0].email }}
const booking = $json;
const attendee = booking.attendees[0];
const formatICalDate = (date) => {
return new Date(date).toISOString()
.replace(/[-:]/g, '').split('.')[0] + 'Z';
};
const icsContent = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//N8N//Booking//EN
BEGIN:VEVENT
UID:${booking.uid}@cal.com
DTSTART:${formatICalDate(booking.startTime)}
DTEND:${formatICalDate(booking.endTime)}
SUMMARY:Cal.com appointment - ${attendee.name}
DESCRIPTION:Appointment with ${attendee.name}
LOCATION:${booking.location || 'TBD'}
STATUS:CONFIRMED
END:VEVENT
END:VCALENDAR`;
return [{
json: { fileName: `appt-${attendee.name}.ics` },
binary: {
calendar: {
data: Buffer.from(icsContent).toString('base64'),
mimeType: 'text/calendar'
}
}
}];
# Correct configuration
Fields:
active: {{ false }} # Expression mode!
probability: {{ 0 }} # Expression mode!

LimitImpactMitigation
Email-based matchingDuplicates if emails differFuture broader search
No reverse syncOdoo → Cal.com not implementedTelegram actions
Single attendeeMulti-participants not handledPlanned if needed

If reverse sync is needed:

  • Webhook Odoo → N8N when an opportunity is updated
  • Update the Cal.com booking
  • Watch out for infinite loops

If multi-type appointments:

  • Create different opportunity types depending on the Cal.com type
  • Auto-assign by type
  • Differentiated initial probability

If appointment volume grows:

  • Daily digest instead of individual notifications
  • Filter by appointment type
  • Round-robin assignment
ProblemCheck
No triggerCal.com: webhook configured? N8N: workflow active?
Opportunity not foundAttendee email = email_from in Odoo?
Update failsCheck Odoo credential permissions
iCal not generatedGenerate iCal node connected? Binary data present?