Check availability workflow - collision detection ignoring active vs passive time

Describe the problem:
Building hair salon booking system with Retell AI voice agent + n8n + Google Calendar. AI Agent is blocking entire event duration instead of only the active time window, making collision detection completely wrong.
Business Logic:
Some hair services have two time components:

  • ACTIVE time = stylist physically working on client (blocks calendar for other appointments)
  • PASSIVE time = client waiting while product processes (stylist is FREE to start another client)
    Example: Hair coloring = 120 minutes total
  • 45 minutes active (stylist applying color)
  • 75 minutes passive (client sits while color develops)
  • Event in calendar: 14:00-16:00 (full duration)
  • Stylist is ONLY busy: 14:00-14:45
  • From 14:45-16:00: stylist can start new appointments
    The Problem (CRITICAL):
    SCENARIO:
  1. Existing appointment in Google Calendar:
    • Service: Balayage (180 min: 60 active + 120 passive)
    • Time: 13:00-16:00 (event start/end)
    • ACTIVE_TIME stored in description: 13:00-14:00
  2. Customer calls asking for appointment at 14:00 for haircut (60 min, all active)
  3. EXPECTED behavior:
    • Check: Does 14:00-15:00 overlap with 13:00-14:00? NO
    • Return: available = TRUE
  4. ACTUAL behavior:
    • AI Agent checks: Does 14:00-15:00 overlap with 13:00-16:00? YES
    • Return: available = FALSE
    • WRONG - it’s blocking the passive time 14:00-16:00 when stylist is actually free
      Workflow Structure (detailed):
┌──────────────┐
│   Webhook    │ ← Receives from Retell AI
│              │   Input: { time, service, worker }
└──────┬───────┘
       │
       ▼
┌──────────────┐
│    Switch    │ ← Routes by worker name
│ mode: Rules  │   Outputs: "Alex", "Marta", "Carolina"
└──┬───┬───┬───┘
   │   │   │
   ▼   ▼   ▼
 Alex Marta Carolina (3 parallel branches, identical structure)
EACH BRANCH (Alex example):
┌─────────────────┐
│    AI Agent     │ ← Receives webhook data
│                 │   Has prompt with active/passive logic
└────────┬────────┘
         │
         ├─→ [Tool: Google Calendar Get Many]
         │   └─→ Connected to Alex's calendar
         │       Operation: getAll event
         │
         └─→ [Tool: OpenAI Chat Model]
             └─→ GPT model for reasoning
         │
         ▼
┌─────────────────┐
│ Respond to      │ ← Should return:
│ Webhook         │   { available: bool, worker: string, time: string }
└─────────────────┘

How Data Flows:

  1. Webhook receives:
{
  "body": {
    "args": {
      "time": "2026-03-18T14:00:00Z",
      "service": "Balayage / sombre / ombre",
      "worker": "Alex"
    }
  }
}
  1. Switch routes to Alex branch
  2. AI Agent has this data available:
    • {{$json.body.args.time}} = requested time
    • {{$json.body.args.service}} = service name
    • {{$json.body.args.worker}} = worker name
  3. AI Agent uses Google Calendar tool to fetch events from Alex’s calendar
  4. Google Calendar returns events like:
{
  "summary": "Balayage - Jan Kowalski - 555123456",
  "start": { "dateTime": "2026-03-18T13:00:00+01:00" },
  "end": { "dateTime": "2026-03-18T16:00:00+01:00" },
  "description": "Imię: Jan Kowalski\nTelefon: 555123456\nUsługa: Balayage / sombre / ombre\nStart wizyty: 2026-03-18T13:00:00Z\nACTIVE_TIME: 2026-03-18T13:00:00Z - 2026-03-18T14:00:00Z"
}
  1. AI Agent is supposed to:
    • Extract ACTIVE_TIME from description: 13:00-14:00
    • Calculate active time for requested service (Balayage = 60 min active)
    • Check if 14:00-15:00 (new) overlaps with 13:00-14:00 (existing)
    • Return available = TRUE (no overlap)
  2. WHAT ACTUALLY HAPPENS:
    • AI Agent sees event 13:00-16:00 (start/end fields)
    • Ignores or doesn’t correctly parse ACTIVE_TIME from description
    • Checks if 14:00-15:00 overlaps with 13:00-16:00
    • Returns available = FALSE (wrong!)
      AI Agent Prompt (exact):
Role: Check appointment availability for hair salon.
Input data:
- Current time: {{$now}}
- Requested time: {{$json.body.args.time}}
- Service: {{$json.body.args.service}}
Service durations (CRITICAL - MUST USE THESE):
- Strzyżenie damskie krótkie: 40 min (40 active, 0 passive)
- Strzyżenie damskie długie: 60 min (60 active, 0 passive)
- Strzyżenie męskie klasyczne: 20 min (20 active, 0 passive)
- Koloryzacja jeden kolor: 120 min (45 active, 75 passive)
- Balayage / sombre / ombre: 180 min (60 active, 120 passive)
- Dekoloryzacja + farbowanie: 240 min (90 active, 150 passive)
Rules (AI MUST FOLLOW):
1. ACTIVE time = stylist working, blocks calendar
2. PASSIVE time = client waiting, does NOT block calendar
3. Check Google Calendar for existing appointments
4. For EACH existing appointment:
   a. Look in the "description" field for line starting with "ACTIVE_TIME:"
   b. If found: extract the start and end times from that line
   c. If NOT found: use entire event duration as active time (fallback)
5. For the requested appointment:
   a. Calculate active end time = requested time + active minutes from service list
6. Check collision:
   a. Does requested active time overlap with existing active time?
   b. Overlap formula: (requestedStart < existingActiveEnd) AND (requestedActiveEnd > existingActiveStart)
7. If NO overlap with ANY existing active time: return available = true
8. If overlap detected: return available = false
CRITICAL: Do NOT check against event.start/event.end - ONLY check against ACTIVE_TIME extracted from description!
Return format:
{
  "available": true/false,
  "worker": "Alex",
  "time": "2026-03-18T14:00:00Z"
}

Google Calendar Tool Configuration:

  • Resource: Event
  • Operation: Get Many
  • Calendar: [Alex’s calendar ID]
  • Return All: false
  • Limit: 50
  • After: (should fetch events from requested date)
  • Before: (should fetch events up to 24h after requested date)
    What I’ve Tried:
  1. Current approach: AI Agent with detailed prompt
    • Result: AI ignores ACTIVE_TIME, blocks entire event duration
  2. Prompt variations:
    • “Extract ACTIVE_TIME from description field”
    • “Only check active time, not total event time”
    • “Parse description for ACTIVE_TIME: [start] - [end]”
    • Result: No improvement, still blocks entire duration
  3. Explicit step-by-step instructions in prompt
    • Numbered steps for extraction and comparison
    • Result: Still doesn’t work
      Why This is Critical:
      This is the ONLY blocking issue preventing launch. The rest works:
  • :white_check_mark: book_appointment creates events correctly (with ACTIVE_TIME in description)
  • :white_check_mark: cancel_appointment works
  • :white_check_mark: reschedule_appointment works
  • :cross_mark: check_availability is completely broken due to this active/passive time issue
    Without this working, system blocks customers from booking valid time slots, making it unusable.
    Expected vs Actual Examples:
    Test Case 1:
  • Existing: Balayage 13:00-16:00 (ACTIVE: 13:00-14:00)
  • Check: 14:00 Haircut (60 min active)
  • Expected: TRUE (14:00-15:00 doesn’t overlap 13:00-14:00)
  • Actual: FALSE :cross_mark:
    Test Case 2:
  • Existing: Balayage 13:00-16:00 (ACTIVE: 13:00-14:00)
  • Check: 13:30 Haircut (60 min active)
  • Expected: FALSE (13:30-14:30 overlaps 13:00-14:00)
  • Actual: FALSE :white_check_mark: (correct but for wrong reason - checking entire event)
    Test Case 3:
  • Existing: None (empty calendar)
  • Check: 14:00 Haircut
  • Expected: TRUE
  • Actual: TRUE :white_check_mark:
    Questions:
  1. Can AI Agent actually handle this logic? Or is this too complex for LLM reasoning and I need to switch to deterministic Code node?
  2. Is there a way to force AI Agent to correctly parse and use ACTIVE_TIME from description field instead of event.start/event.end?
  3. Should I restructure the workflow completely? (e.g., Code node to extract ACTIVE_TIME → pass to AI Agent)
  4. Is this a known limitation of AI Agent + Google Calendar tool combination?

What is the error message (if any)?
No error message - workflow executes successfully but returns incorrect availability status (available: false when should be true due to active/passive time logic not working).

Please share your workflow:
Cannot share full workflow JSON due to sensitive Calendar IDs, but the structure is:
WebhookSwitch (mode: Rules, routes by worker: Alex/Marta/Carolina) → AI Agent (connected tools: Google Calendar Get Many + OpenAI Chat Model) → Respond to Webhook
Each worker branch (Alex/Marta/Carolina) is identical except for the Google Calendar ID used in the Calendar tool.
Key configuration:

  • Switch node: Routes based on {{$json.body.args.worker}}
  • AI Agent: Contains the prompt shown above
  • Google Calendar tool: Get Many operation, limit 50, fetches from requested date range
  • OpenAI Chat Model: GPT-4

Share the output returned by the last node:
Current output (INCORRECT):

{
  "available": false,
  "worker": "Alex",
  "time": "2026-03-18T14:00:00Z"
}

Expected output (CORRECT):

{
  "available": true,
  "worker": "Alex",
  "time": "2026-03-18T14:00:00Z"
}

This happens when checking 14:00 availability against an existing Balayage appointment at 13:00-16:00 (with ACTIVE_TIME: 13:00-14:00 in description). The 14:00 slot should be available since it doesn’t overlap with the 13:00-14:00 active window.

Information on your n8n setup:

  • n8n version: Cloud (latest)
  • Database: n8n Cloud default
  • n8n EXECUTIONS_PROCESS setting: n8n Cloud default
  • Running n8n via: n8n Cloud
  • Operating system: N/A (cloud-hosted)

Any help massively appreciated! Been stuck for 2 weeks on this single issue.

hello @mistii

Why do you need an AI in this setup? It may hallucinate, especially with arithmetic operations, and it is better to use strict logic without the AI.

If you want to stay with AI, use the system prompt for strict logic (timeslots, duration, rules, where and what to extract) and use a user prompt for the role

yeah had exactly this problem with date logic in an AI Agent — the model just gravitates toward the obvious JSON fields even with explicit instructions. for this kind of check i’d just pull the calendar events into a Code node, extract ACTIVE_TIME with a simple split or regex on the description string, then do the overlap math directly. way more reliable than hoping the LLM parses it correctly everytime

Benjamin’s got the right idea. Pulling this out of the AI agent and into a Code node is the move.

What I’d do: store active_start and active_end as separate fields in your calendar event description (or custom properties if your calendar supports it). Something like ACTIVE:14:00-14:45 in the description field.

Then in your availability check workflow:

  1. Pull all events for the requested day from Google Calendar
  2. Code node loops through each event, parses out the active window from the description
  3. Compare the requested time slot against only the active windows, not the full event duration
  4. Return available = true/false

The overlap check itself is pretty simple math: two time ranges overlap if start_a < end_b AND start_b < end_a. Just make sure you’re comparing against the active times, not the calendar event boundaries.

One thing to watch out for: if a stylist starts a new client during passive time, that new appointment’s active window could overlap with the END of the passive window when the first client needs attention again (like rinsing out color). So you’d want to also check that the new appointment’s active time finishes before the first client’s passive time ends. Edge case but it’ll bite you if you don’t handle it.

yeah the end-of-passive thing is exactly what trips salon booking systems up. basically the new appointments active window needs to fit within a free block — either before existing_active_start, or after the whole event ends. starting in the passive window is only valid if it also finishes before the stylist needs to come back (ie new_active_end <= existing_event_end)