{
  "nodes": [
    {
      "id": "7b8e3b87-3383-46b0-9cd6-49ea0e9d357f",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -480,
        16
      ],
      "parameters": {
        "width": 420,
        "height": 788,
        "content": "## \u2615 Gap Time Concierge - Smart Break Spot Recommender\n\n**What this workflow does:**\nAutomatically finds the perfect caf\u00e9 or spot to visit during gaps between your calendar appointments. It checks your schedule, calculates available time, considers current weather, and sends personalized recommendations to Slack.\n\n**Who is this for:**\n- Busy professionals who want to maximize their free time\n- Anyone who frequently travels between meetings\n- People who want weather-appropriate spot recommendations\n\n**How it works:**\n1. Runs every 30 minutes to check your calendar\n2. Calculates travel time to your next appointment\n3. Determines if you have enough gap time (default: 30+ minutes)\n4. Checks weather at destination\n5. Searches for indoor spots (rain/snow) or outdoor spots (sunny/cloudy)\n6. AI recommends top 3 spots based on your preferences\n7. Sends friendly notification to Slack\n\n**Setup Requirements:**\n- Google Calendar OAuth connection\n- Google Maps API key\n- Google Places API key\n- OpenWeatherMap API key\n- Notion database for user preferences\n- Slack OAuth connection\n- OpenAI API key"
      },
      "typeVersion": 1
    },
    {
      "id": "917b3df4-1ff8-42bc-a9fc-5f9530c2cef1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        64,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 232,
        "content": "### \u2699\ufe0f Step 1: Configuration\n\nSet your configuration values here:\n- `currentLocation`: Your starting point\n- `minGapTimeMinutes`: Minimum free time to trigger recommendations\n- API keys for Google Maps, Places, and OpenWeatherMap\n- Notion database ID for preferences"
      },
      "typeVersion": 1
    },
    {
      "id": "7d36bc0c-9a25-4367-ab0e-45e99f9fecc0",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        384,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 136,
        "content": "### \ud83d\udcc5 Step 2: Fetch Calendar & Preferences\n\nRetrieves your next calendar event and user preferences from Notion database."
      },
      "typeVersion": 1
    },
    {
      "id": "64549032-c64c-441a-938d-52582cc7352e",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        752,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 136,
        "content": "### \ud83c\udf24\ufe0f Step 3: Weather & Travel Time\n\nGets current weather at destination and calculates travel time via Google Maps Directions API (transit mode)."
      },
      "typeVersion": 1
    },
    {
      "id": "1e6589f4-5008-42ac-b6c5-860465baaaaa",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 188,
        "content": "### \u23f1\ufe0f Step 4: Gap Time Calculation\n\nCalculates available free time:\n`Gap Time = Time until event - Travel time`\n\nOnly proceeds if gap time exceeds minimum threshold."
      },
      "typeVersion": 1
    },
    {
      "id": "cd582fdb-18d6-4e83-8dfa-7e21429af41c",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1952,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 188,
        "content": "### \ud83c\udf27\ufe0f\u2600\ufe0f Step 5: Weather-Based Routing\n\nRoutes to different search queries:\n- **Rain/Snow** (ID < 700): Indoor spots\n- **Clear/Cloudy** (ID \u2265 700): Outdoor spots"
      },
      "typeVersion": 1
    },
    {
      "id": "6b1291d1-c3ad-4541-8e57-6be7b0a95276",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2400,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 276,
        "content": "### \ud83e\udd16 Step 6: AI Recommendation\n\nGPT-4.1-mini analyzes:\n- Your available time\n- Weather conditions\n- User preferences from Notion\n- Nearby spot options\n\nGenerates a friendly Slack message with top 3 recommendations."
      },
      "typeVersion": 1
    },
    {
      "id": "cb3d1e56-5b0d-4c59-9e01-8887eaaed515",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2752,
        416
      ],
      "parameters": {
        "color": 7,
        "height": 164,
        "content": "### \ud83d\udcf1 Step 7: Slack Notification\n\nSends the AI-generated recommendation to your configured Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "d4eb07b5-27f6-4dc9-b2bf-22f149423342",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        64,
        224
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "99f75d5c-1fca-418a-aac9-888801be1bab",
      "name": "Set Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        224,
        224
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "currentLocation",
              "type": "string",
              "value": "Tokyo Station"
            },
            {
              "id": "id-2",
              "name": "googleMapsApiKey",
              "type": "string",
              "value": ""
            },
            {
              "id": "id-3",
              "name": "googlePlacesApiKey",
              "type": "string",
              "value": ""
            },
            {
              "id": "id-4",
              "name": "openWeatherApiKey",
              "type": "string",
              "value": ""
            },
            {
              "id": "id-5",
              "name": "notionDatabaseId",
              "type": "string",
              "value": ""
            },
            {
              "id": "id-6",
              "name": "minGapTimeMinutes",
              "type": "number",
              "value": 30
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "187a33f6-8e41-4e6f-b717-a1c1e38dca53",
      "name": "Get Next Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        576,
        224
      ],
      "parameters": {
        "limit": 1,
        "options": {
          "orderBy": "startTime",
          "recurringEventHandling": "expand"
        },
        "timeMin": "={{ new Date().toISOString() }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": ""
        },
        "operation": "getAll"
      },
      "typeVersion": 1.3
    },
    {
      "id": "3d6678dc-42f5-48e1-8393-905b01c51abd",
      "name": "Get User Preferences from Notion",
      "type": "n8n-nodes-base.notion",
      "position": [
        400,
        224
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "operation": "getAll",
        "returnAll": true,
        "databaseId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').first().json.notionDatabaseId }}"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d03c7278-b35f-42f1-a269-afacdd1914dc",
      "name": "Get Weather at Destination",
      "type": "n8n-nodes-base.openWeatherMap",
      "position": [
        768,
        224
      ],
      "parameters": {
        "cityName": "={{ $('Get Next Calendar Event').first().json.location }}"
      },
      "typeVersion": 1
    },
    {
      "id": "54e09e95-6eef-4711-a296-be09c77d5423",
      "name": "Get Travel Time via Google Maps",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        224
      ],
      "parameters": {
        "url": "=https://maps.googleapis.com/maps/api/directions/json?origin={{ encodeURIComponent($('Set Configuration').first().json.currentLocation) }}&destination={{ encodeURIComponent($('Get Next Calendar Event').first().json.location) }}&mode=transit&key={{ $('Set Configuration').first().json.googleMapsApiKey }}",
        "options": {}
      },
      "typeVersion": 4.3
    },
    {
      "id": "020dd272-b2cf-4c95-b71a-129cd8d1a316",
      "name": "Calculate Available Gap Time",
      "type": "n8n-nodes-base.code",
      "position": [
        1296,
        224
      ],
      "parameters": {
        "jsCode": "// Extract data from previous nodes\nconst calendarEvent = $('Get Next Calendar Event').first().json;\nconst travelData = $('Get Travel Time via Google Maps').first().json;\nconst config = $('Set Configuration').first().json;\n\n// Get event start time\nconst eventStartTime = new Date(calendarEvent.start.dateTime || calendarEvent.start.date);\n\n// Get travel duration in seconds from Google Maps API response\nconst travelDurationSeconds = travelData.routes?.[0]?.legs?.[0]?.duration?.value || 0;\nconst travelTimeMinutes = Math.ceil(travelDurationSeconds / 60);\n\n// Calculate current time\nconst now = new Date();\n\n// Calculate total available time until event (in minutes)\nconst totalAvailableMinutes = Math.floor((eventStartTime - now) / 1000 / 60);\n\n// Calculate gap time (available time - travel time)\nconst gapTimeMinutes = totalAvailableMinutes - travelTimeMinutes;\n\n// Get minimum gap time from config\nconst minGapTime = config.minGapTimeMinutes || 30;\n\n// Determine if there's enough gap time\nconst hasGapTime = gapTimeMinutes >= minGapTime;\n\n// Return the calculated data\nreturn [{\n  json: {\n    eventStartTime: eventStartTime.toISOString(),\n    travelTimeMinutes: travelTimeMinutes,\n    totalAvailableMinutes: totalAvailableMinutes,\n    gapTimeMinutes: gapTimeMinutes,\n    minGapTimeMinutes: minGapTime,\n    hasGapTime: hasGapTime,\n    currentTime: now.toISOString(),\n    eventLocation: calendarEvent.location,\n    eventSummary: calendarEvent.summary\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d234d4e7-55ac-4c7f-b6b6-61608f7a1b76",
      "name": "Has Sufficient Gap Time?",
      "type": "n8n-nodes-base.if",
      "position": [
        1520,
        224
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.hasGapTime }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "0e0eb752-8c39-4c67-adb1-904e8cacb85f",
      "name": "Route by Weather Condition",
      "type": "n8n-nodes-base.switch",
      "position": [
        1968,
        224
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Rain/Snow",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "number",
                      "operation": "lt"
                    },
                    "leftValue": "={{ $json.weatherId }}",
                    "rightValue": 700
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Sunny/Cloudy",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "number",
                      "operation": "gte"
                    },
                    "leftValue": "={{ $json.weatherId }}",
                    "rightValue": 700
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "c5aa5a59-38e3-4134-8fa3-94ec06c043bb",
      "name": "Search Indoor Spots",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2192,
        128
      ],
      "parameters": {
        "url": "=https://maps.googleapis.com/maps/api/place/nearbysearch/json?location={{ $json.weatherData.coord.lat }},{{ $json.weatherData.coord.lon }}&radius=1000&type=cafe&keyword=indoor+shopping+mall+station+underground&key={{ $('Set Configuration').first().json.googlePlacesApiKey }}",
        "options": {}
      },
      "typeVersion": 4.3
    },
    {
      "id": "a1c7e6cc-6196-4aa1-8a9b-c35b5b9b9cdb",
      "name": "Search Outdoor Spots",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2192,
        320
      ],
      "parameters": {
        "url": "=https://maps.googleapis.com/maps/api/place/nearbysearch/json?location={{ $json.weatherData.coord.lat }},{{ $json.weatherData.coord.lon }}&radius=1000&type=cafe&keyword=terrace+view+open+air+outdoor&key={{ $('Set Configuration').first().json.googlePlacesApiKey }}",
        "options": {}
      },
      "typeVersion": 4.3
    },
    {
      "id": "cb0cb5bb-967c-4eaf-8e6b-bc082b6f1462",
      "name": "AI Generate Recommendations",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2416,
        224
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini",
          "cachedResultName": "GPT-4.1-MINI"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "content": "=You are a \"Gap Time Concierge\" assistant.\nBased on the following conditions and the searched caf\u00e9 list, recommend the top 3 optimal spots for the user.\n\n## User's Situation\n- Current Location: {{ $('Set Configuration').first().json.currentLocation }}\n- Next Appointment: {{ $('Merge All Context Data').first().json.eventSummary }} ({{ $('Merge All Context Data').first().json.eventLocation }})\n- Available Time: {{ $('Merge All Context Data').first().json.gapTimeMinutes }} minutes\n- Weather: {{ $('Merge All Context Data').first().json.weatherMain }} ({{ $('Merge All Context Data').first().json.weatherDescription }})\n\n## User Preferences\n{{ JSON.stringify($('Merge All Context Data').first().json.preferences) }}\n\n## Candidate Caf\u00e9 List\n{{ JSON.stringify($json) }}\n\n## Instructions\nCreate a friendly recommendation message for Slack notification.\nOutput as plain text that can be sent directly to Slack, not in JSON format."
            },
            {}
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "4939276b-6a7e-4b2c-974d-57f894ae8b41",
      "name": "Send Slack Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        2768,
        224
      ],
      "parameters": {
        "text": "={{ $json.output[0].content[0].text }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": ""
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "abdd7ed9-b06d-4ea6-b9b3-0c189f7d00ce",
      "name": "Merge All Context Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1744,
        224
      ],
      "parameters": {
        "jsCode": "// Merge all context data from previous nodes\nconst gapTimeData = $('Calculate Available Gap Time').first().json;\nconst weatherData = $('Get Weather at Destination').first().json;\nconst userPreferences = $('Get User Preferences from Notion').all().map(item => item.json);\nconst calendarEvent = $('Get Next Calendar Event').first().json;\nconst config = $('Set Configuration').first().json;\n\n// Extract weather ID for routing decision\nconst weatherId = weatherData.weather?.[0]?.id || 800;\nconst weatherMain = weatherData.weather?.[0]?.main || 'Clear';\nconst weatherDescription = weatherData.weather?.[0]?.description || '';\n\n// Return merged context\nreturn [{\n  json: {\n    // Gap time information\n    gapTimeMinutes: gapTimeData.gapTimeMinutes,\n    travelTimeMinutes: gapTimeData.travelTimeMinutes,\n    eventStartTime: gapTimeData.eventStartTime,\n    eventLocation: gapTimeData.eventLocation,\n    eventSummary: gapTimeData.eventSummary,\n    \n    // Weather information\n    weatherId: weatherId,\n    weatherMain: weatherMain,\n    weatherDescription: weatherDescription,\n    weatherData: weatherData,\n    \n    // User preferences\n    preferences: userPreferences,\n    \n    // Configuration\n    currentLocation: config.currentLocation,\n    googlePlacesApiKey: config.googlePlacesApiKey\n  }\n}];"
      },
      "typeVersion": 2
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Set Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration": {
      "main": [
        [
          {
            "node": "Get User Preferences from Notion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Indoor Spots": {
      "main": [
        [
          {
            "node": "AI Generate Recommendations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Outdoor Spots": {
      "main": [
        [
          {
            "node": "AI Generate Recommendations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Context Data": {
      "main": [
        [
          {
            "node": "Route by Weather Condition",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Next Calendar Event": {
      "main": [
        [
          {
            "node": "Get Weather at Destination",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Sufficient Gap Time?": {
      "main": [
        [
          {
            "node": "Merge All Context Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Weather at Destination": {
      "main": [
        [
          {
            "node": "Get Travel Time via Google Maps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Weather Condition": {
      "main": [
        [
          {
            "node": "Search Indoor Spots",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Search Outdoor Spots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Generate Recommendations": {
      "main": [
        [
          {
            "node": "Send Slack Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Available Gap Time": {
      "main": [
        [
          {
            "node": "Has Sufficient Gap Time?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Travel Time via Google Maps": {
      "main": [
        [
          {
            "node": "Calculate Available Gap Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get User Preferences from Notion": {
      "main": [
        [
          {
            "node": "Get Next Calendar Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}