Build an AI scheduling assistant from your inbox
An AI assistant that classifies incoming emails, checks your calendar, and books meetings automatically. We built this, ran it in production, and then hit a wall. Here's what worked, what didn't, and how you'd finish it.
An honest note
This flow worked well with Google Calendar. When we moved to CalDAV (self-hosted calendar), the calendar integration broke and we never did the refactor. We're sharing it anyway because the architecture, the prompts, and the classification logic are the hard part — the calendar integration is a connector swap. If you're on Google Calendar, this works as-is.
What this flow does
Someone emails assistant@yourdomain.com with "Can we meet next Tuesday?" The AI reads the email, classifies the intent, checks your calendar for availability, and either books the meeting and confirms, or proposes alternative times. It handles new meetings, rescheduling, and cancellations.
No service gets access to your mailbox. No service gets permanent access to your calendar. The AI only sees the single email that was routed to it, and it only accesses calendar data at the moment it needs to check availability or create an event.
The architecture
This is a two-flow design. The first flow handles classification — figuring out what the email is actually asking for. The second flow handles execution — the actual calendar operations.
Email arrives
EmailConnect receives the message and webhooks the parsed JSON to n8n
Classification agent
LLM classifies intent: meeting request, information request, information sharing, or fallback
Route to handler
n8n routes to the appropriate sub-workflow based on classification
Meeting handler (sub-workflow)
Checks calendar, books/updates/cancels meetings, and drafts the reply
Reply sent
Confirmation, proposed times, or a "forwarded to human" message sent via SMTP
Flow 1: Email classification
The classification agent is the brain of the system. It looks at each incoming email and determines what kind of request it is, how confident it is, and what information might be missing.
The four categories
MEETING_REQUEST
Schedule, reschedule, or cancel a meeting
Triggers: "meet", "schedule", "available", "reschedule", "cancel", time references
INFORMATION_REQUEST
Asking for information, documents, procedures
Triggers: "Do you know", "Can you tell me", "Where can I find"
INFORMATION_SHARING
FYI updates, status reports, forwarded context
Triggers: "FYI", "Update on", "Thought you should know"
FALLBACK
Unclear intent, sensitive topics, needs human attention
Triggers: ambiguous requests, legal/financial topics, confidence < 7
Classification prompt
The classification agent outputs a structured JSON with the category, action type (for meetings), tone detection, urgency, and a confidence score. If confidence drops below 7, it defaults to fallback — better to escalate than to misclassify.
Classification output schema
{
"category": "MEETING_REQUEST",
"action_type": "schedule",
"completeness": true,
"tone": "informal",
"urgency": "medium",
"confidence": 9,
"reasoning": "Sender explicitly asks to meet next week",
"missing_info": null
} The completeness field is key. If someone says "let's meet sometime" without proposing dates, the agent knows it needs to ask for time preferences before it can book anything. The tone field carries forward to the reply — a casual email gets a casual response.
You're an AI assistant listening to assistant@yourdomain.com.
Your first job is to classify incoming emails and determine
the appropriate response strategy.
## Email Details
Sender: {{ sender.name }} <{{ sender.email }}>
Subject: {{ subject }}
Message: {{ content.text }}
## Classification Rules
Analyze the email and classify it into ONE category:
1. MEETING_REQUEST - Schedule, change, or cancel a meeting
2. INFORMATION_REQUEST - Asking for information
3. INFORMATION_SHARING - Sharing information (FYI)
4. FALLBACK - Unclear, ambiguous, or sensitive
## Meeting Action Types (if MEETING_REQUEST):
- "schedule" - New meeting request
- "update" - Changing existing meeting
- "cancel" - Canceling or postponing
## Assessment Requirements
- completeness: Sufficient info to act? (true/false)
- tone: "formal" or "informal"
- urgency: "low", "medium", or "high"
- confidence: How confident? (1-10)
If confidence < 7, default to FALLBACK.
Always assume one-off meetings unless recurring was
explicitly mentioned.Flow 2: Meeting handler
When the classifier identifies a meeting request, n8n triggers the meeting handler as a sub-workflow. It receives the original email plus the classification results (action type, tone, urgency, completeness).
What the meeting handler does
Schedule (new meeting)
Complete request: Checks calendar for the proposed times. If a slot works, books it directly. If not, proposes 2-3 alternatives on nearby days.
Incomplete request: Asks for missing details — preferred times, duration, topic — in a tone that matches the sender.
Update (reschedule)
Complete request: Finds the existing meeting on the calendar, checks availability for the new time, updates the event.
Incomplete request: Asks which meeting they mean (date/subject reference) and what needs to change.
Cancel
Complete request: Identifies and cancels the meeting.
Incomplete request: Asks which meeting to cancel.
Meeting handler defaults
| Parameter | Default |
|---|---|
| Duration | 30 minutes |
| Format | Virtual (Google Meet) |
| Available hours | Mon-Fri 9:30am - 4:30pm |
| Timezone | From calendar settings |
| Type | One-off (unless explicitly recurring) |
You're an AI assistant. Handle all meeting-related requests
(scheduling, updates, cancellations) professionally and
efficiently.
## Context
Sender: {{ sender.name }} <{{ sender.email }}>
Subject: {{ subject }}
Message: {{ content.text }}
## Classification Results
Action Type: {{ assessment.action_type }}
Tone: {{ assessment.tone }}
Urgency: {{ assessment.urgency }}
Completeness: {{ assessment.completeness }}
Missing Info: {{ assessment.missing_info }}
## If action_type = "schedule" and complete:
1. Check calendar for requested timeframes
2. If a proposed slot fits, book directly
3. If no overlap, propose 2-3 alternatives on nearby days
4. Draft confirmation with meeting details
5. Default: 30 min, virtual (Google Meet), Mon-Fri 9:30-4:30
## If action_type = "schedule" and incomplete:
1. Request missing info in matching tone
2. Ask for: topic, preferred duration, 2-3 time slots,
timezone, virtual vs physical
## If action_type = "update" and complete:
1. Find the specific meeting on calendar
2. Check availability for new proposed times
3. Update the meeting
## If action_type = "cancel" and complete:
1. Find and cancel the specific meeting
## Response Guidelines
- Match the sender's tone ({{ assessment.tone }})
- Don't mention other calendar items
- For high urgency, prioritize sooner slots
- Confirm specific meeting before updating/canceling
- The assistant won't attend the meetingThe calendar integration
The meeting handler uses n8n's tool-calling capability to give the LLM access to calendar operations: read events, create events, update events, delete events. With Google Calendar, n8n has built-in nodes that handle OAuth and provide these operations as tools the agent can call.
Works with Google Calendar
If you're using Google Calendar, the n8n Google Calendar nodes work well as agent tools. The agent can check availability, create events with Google Meet links, and handle updates/cancellations.
Where it got tricky: CalDAV
This flow ran in production with Google Calendar. It worked well — the agent could schedule, reschedule, and cancel meetings with natural language emails. Then we moved to a self-hosted CalDAV setup (for data sovereignty reasons, naturally), and the integration broke.
The problems with CalDAV in this context:
- No native n8n nodes — Google Calendar has first-class n8n support. CalDAV requires custom HTTP request nodes with iCalendar (ICS) format handling.
- ICS is verbose — creating and parsing .ics events in a Code node is more work than calling a Google Calendar API.
- Free/busy queries — checking availability in CalDAV requires either FREEBUSY requests or fetching all events in a range and checking for conflicts. Both are more complex than Google's simple API.
- Different server quirks — Nextcloud Calendar, Radicale, and Baikal all handle CalDAV slightly differently. What works on one may not work on another.
We haven't done the refactor yet. If you're on Google Calendar, the original approach works. If you're on CalDAV, the classification flow and prompts still work — you'd need to replace the calendar tool nodes with CalDAV HTTP requests.
Community challenge
If you build the CalDAV integration for this flow, we'd love to hear about it. The prompts and classification logic are the hard part — the calendar connector is a well-defined problem waiting for someone to solve it. Reach out at hello@emailconnect.eu.
Lessons learned
Two-flow architecture is worth it
Separating classification from execution keeps each flow focused. The classifier doesn't need calendar access, and the meeting handler doesn't need to worry about identifying email intent. Easier to debug, easier to extend.
Completeness detection saves awkward exchanges
The completeness field in the classification prevents the agent from trying to book a meeting when it doesn't have enough information. Without it, the agent would guess — and guessing wrong about meeting times is worse than asking.
Confidence thresholds prevent misclassification
The "if confidence < 7, default to fallback" rule is simple but effective. A misrouted email (booking a meeting when someone just wanted information) is far worse than escalating to a human.
Tone matching matters more than you'd think
A formal scheduling request getting a casual "sure, how about Tuesday?" response feels wrong. Carrying the tone assessment from classification to the reply draft makes the AI feel natural rather than robotic.
Calendar portability is an unsolved problem
The move from Google Calendar to CalDAV broke this flow. That's a real constraint to consider when building calendar integrations — if you might switch providers, abstract the calendar layer early.
The broader pattern
Even if you don't need a scheduling assistant, the classification → routing → handler pattern is reusable. The same architecture works for:
- Customer support triage — classify by department, urgency, and topic, then route to different handler flows
- Lead qualification — classify inbound inquiries as hot/warm/cold and route accordingly
- Document processing — classify by document type (invoice, contract, report) and trigger different extraction flows
The key insight is that one LLM call for classification, followed by specialized handlers, works better than one monolithic agent trying to do everything. Each handler can have its own tools, its own prompt tuning, and its own error handling.
Build on this
The classification prompts and architecture described here are free to use and adapt. Start with a Google Calendar integration to get the core working, then swap connectors as needed.
Related
Built the CalDAV version? Want to compare notes on the classification approach? Reach out at hello@emailconnect.eu.