This workflow corresponds to n8n.io template #16204 — we link there as the canonical source.
This workflow follows the Agent → OpenAI Chat recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "jjfpjTyKZ6GYgu31",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Smart Visa Slot Sniper \u2014 Auto-monitor and book visa appointments",
"tags": [],
"nodes": [
{
"id": "50147185-e5a8-41e8-a31d-23b9e3c2b803",
"name": "Sticky Note - Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-576,
-160
],
"parameters": {
"width": 740,
"height": 1008,
"content": "## \ud83d\udec2 Smart Visa Slot Sniper \u2014 Auto-monitor and book visa appointments\n\nNever miss a visa slot again. This workflow polls your embassy portal every 10 minutes, scores available appointments with AI, sends instant Telegram alerts, and auto-books when a slot meets your threshold.\n\n### Who's it for\n- Visa applicants in high-demand corridors (US, UK, Schengen)\n- Travel agents managing multiple applicants\n- Anyone tired of manually refreshing embassy portals\n\n### How it works\n1. Scheduler triggers every 10 minutes\n2. Your config (dates, location, urgency) is loaded from the Set node\n3. Code node fetches and parses available slots from the portal\n4. AI scores each slot from 0\u2013100 based on your preferences and urgency\n5. Telegram alert is sent immediately with the score and recommendation\n6. If score meets your auto-book threshold, booking is initiated after a 15-minute confirmation window\n7. Final booking status and confirmation ID are sent via Telegram and logged to Google Sheets\n\n### How to set up\n1. Import this workflow into n8n\n2. Add your OpenAI and Telegram Bot credentials\n3. Open the Set User Config node and fill in your details: visa type, passport number, preferred dates, location, urgency level, and auto-book threshold\n4. Replace the booking URL placeholder in Code 2 with your actual portal endpoint\n5. Set your Google Sheet ID in the config node\n6. Activate the workflow\n\n### Requirements\n- OpenAI API key (GPT-4.1-mini or above)\n- Telegram Bot token and your Chat ID\n- Google Sheets OAuth2 credentials\n- Access to your embassy or visa portal's appointment endpoint\n\n### How to customise\n- Adjust autoBookThreshold (0\u2013100) to control how selective the auto-booking is\n- Change the cron schedule in the trigger node for faster or slower polling\n- Add a filter node after AI scoring to skip SKIP-rated slots entirely\n- Extend Code 2 to handle multi-applicant bookings"
},
"typeVersion": 1
},
{
"id": "8d53871a-6612-4da6-9f4d-080e8173c115",
"name": "Sticky Note - Stage1",
"type": "n8n-nodes-base.stickyNote",
"position": [
272,
32
],
"parameters": {
"color": 4,
"width": 560,
"height": 380,
"content": "## Stage 1 \u2014 Trigger & Fetch\n\nPolls portal every 10 min.\nConfig is set once here."
},
"typeVersion": 1
},
{
"id": "271700c0-714e-43af-9aef-e9a486fa3345",
"name": "Sticky Note - Stage2",
"type": "n8n-nodes-base.stickyNote",
"position": [
928,
32
],
"parameters": {
"color": 3,
"width": 632,
"height": 588,
"content": "## Stage 2 \u2014 Parse, Score & Alert\n\nCode 1 detects new slots.\nAI scores 0\u2013100.\nTelegram alert dispatched instantly."
},
"typeVersion": 1
},
{
"id": "de892632-bcc7-42fb-bcf1-e719b8f2d93d",
"name": "Sticky Note - Stage3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1632,
32
],
"parameters": {
"color": 6,
"width": 824,
"height": 380,
"content": "## Stage 3 \u2014 Book & Log\n\nWait nodes control rate limiting\nand confirmation window.\nCode 2 handles booking + Sheets log."
},
"typeVersion": 1
},
{
"id": "a161ad1f-73e4-4cd0-b7fc-6287d3ada768",
"name": "Poll Every 10 Min",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
336,
240
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "*/10 * * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "6804b048-1cf6-43b1-9302-67c907b839f3",
"name": "Set User Config",
"type": "n8n-nodes-base.set",
"position": [
512,
240
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "portalUrl",
"type": "string",
"value": "https://visa.embassy-portal.example.com/appointments/available"
},
{
"name": "visaType",
"type": "string",
"value": "Tourist B-1/B-2"
},
{
"name": "applicantName",
"type": "string",
"value": "Jane Doe"
},
{
"name": "passportNumber",
"type": "string",
"value": "A12345678"
},
{
"name": "preferredDateFrom",
"type": "string",
"value": "2026-05-01"
},
{
"name": "preferredDateTo",
"type": "string",
"value": "2026-06-30"
},
{
"name": "preferredLocation",
"type": "string",
"value": "New Delhi"
},
{
"name": "urgencyLevel",
"type": "number",
"value": 8
},
{
"name": "autoBookThreshold",
"type": "number",
"value": 85
},
{
"name": "telegramChatId",
"type": "string",
"value": "YOUR_TELEGRAM_CHAT_ID"
},
{
"name": "googleSheetId",
"type": "string",
"value": "YOUR_GOOGLE_SHEET_ID"
},
{
"name": "runTimestamp",
"type": "string",
"value": "={{ new Date().toISOString() }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "ffde410a-634a-438e-bad1-78af4c8f2d5a",
"name": "Code 1 - Fetch & Parse Slots",
"type": "n8n-nodes-base.code",
"position": [
688,
240
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// ============================================================\n// FETCH & PARSE AVAILABLE VISA SLOTS\n// ============================================================\nconst item = $input.item.json;\n\n// In a real setup, you would use an HTTP Request node before this\n// to fetch the portal HTML/JSON, and pass it in as item.portalResponse\n// For now this node parses whatever the portal returns and normalises it\n\nconst rawData = item.portalResponse || null;\n\n// --- Mock slot data for testing (replace with real parsed response)\nconst mockSlots = [\n {\n slotDate: \"2026-06-15\",\n slotTime: \"10:30\",\n slotLocation: item.preferredLocation || \"New Delhi\",\n availableSeats: 3\n },\n {\n slotDate: \"2026-07-02\",\n slotTime: \"09:00\",\n slotLocation: \"Mumbai\",\n availableSeats: 1\n }\n];\n\n// --- Use real data if available, else fall back to mock\nlet slots = [];\n\nif (rawData) {\n try {\n const parsed = typeof rawData === \"string\" ? JSON.parse(rawData) : rawData;\n // Adjust these field names to match your actual portal's response shape\n slots = (parsed.appointments || parsed.slots || parsed.data || []).map(s => ({\n slotDate: s.date || s.appointmentDate || s.slot_date,\n slotTime: s.time || s.appointmentTime || s.slot_time,\n slotLocation: s.location || s.city || s.centre,\n availableSeats: s.seats || s.available || s.capacity || 1\n }));\n } catch (e) {\n slots = mockSlots;\n }\n} else {\n slots = mockSlots;\n}\n\n// --- Filter out slots outside preferred date range\nconst from = new Date(item.preferredDateFrom);\nconst to = new Date(item.preferredDateTo);\n\nconst filtered = slots.filter(s => {\n const d = new Date(s.slotDate);\n return d >= from && d <= to;\n});\n\n// --- Enrich each slot with derived fields\nconst today = new Date();\n\nconst enriched = filtered.map(s => {\n const slotDateObj = new Date(s.slotDate);\n const daysUntilSlot = Math.round((slotDateObj - today) / (1000 * 60 * 60 * 24));\n const withinPreference = slotDateObj >= from && slotDateObj <= to;\n const locationMatch = (s.slotLocation || \"\")\n .toLowerCase()\n .includes((item.preferredLocation || \"\").toLowerCase());\n\n return {\n ...item,\n slotDate: s.slotDate,\n slotTime: s.slotTime,\n slotLocation: s.slotLocation,\n availableSeats: s.availableSeats,\n daysUntilSlot,\n withinPreference,\n locationMatch\n };\n});\n\n// --- If no slots found, return a single no-op item so the workflow doesn't break\nif (enriched.length === 0) {\n return {\n json: {\n ...item,\n slotDate: null,\n slotTime: null,\n slotLocation: null,\n availableSeats: 0,\n daysUntilSlot: null,\n withinPreference: false,\n locationMatch: false,\n noSlotsFound: true\n }\n };\n}\n\n// --- Return one item per slot (n8n will process each through the AI node)\nreturn enriched.map(slot => ({ json: slot }));"
},
"typeVersion": 2
},
{
"id": "6eeb6c93-8e09-470b-acf9-307037297339",
"name": "AI - Score Slot",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
992,
240
],
"parameters": {
"text": "=You are a visa appointment optimization expert. Score this slot 0\u2013100 and decide the action.\n\nApplicant: {{ $json.applicantName }} | Visa: {{ $json.visaType }} | Urgency: {{ $json.urgencyLevel }}/10\nPreferred range: {{ $json.preferredDateFrom }} \u2192 {{ $json.preferredDateTo }} | Location: {{ $json.preferredLocation }}\nAuto-book threshold: {{ $json.autoBookThreshold }}\n\nSlot: {{ $json.slotDate }} {{ $json.slotTime }} @ {{ $json.slotLocation }}\nSeats: {{ $json.availableSeats }} | Days away: {{ $json.daysUntilSlot }}\nIn preferred range: {{ $json.withinPreference }} | Location match: {{ $json.locationMatch }}\n\nRespond ONLY with this JSON (no markdown):\n{\"score\":<0-100>,\"recommendation\":\"AUTO_BOOK|NOTIFY|SKIP\",\"alertMsg\":\"<2 sentence user-friendly message>\"}",
"options": {},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "9f15fa10-9a4b-4c2d-9445-913830af65dc",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
976,
448
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini"
},
"options": {
"temperature": 0.2
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "d9ce73ac-5290-48a9-853b-ff35ff11a262",
"name": "Send Telegram Alert",
"type": "n8n-nodes-base.telegram",
"onError": "continueErrorOutput",
"position": [
1328,
240
],
"parameters": {
"text": "=\ud83d\udec2 *VISA SLOT ALERT*\n\n{{ (() => { try { return JSON.parse(($json.output||\"{}\").replace(/```json|```/g,'')).alertMsg } catch(e){ return 'A new slot was detected.' } })() }}\n\n\ud83d\udcc5 {{ $json.slotDate }} \ud83d\udd50 {{ $json.slotTime }}\n\ud83d\udccd {{ $json.slotLocation }} | \ud83d\udcba {{ $json.availableSeats }} seat(s)\n\u2b50 Score: {{ (() => { try { return JSON.parse(($json.output||\"{}\").replace(/```json|```/g,'')).score } catch(e){ return '?' } })() }}/100\n\n{{ (() => { try { const r=JSON.parse(($json.output||\"{}\").replace(/```json|```/g,'')).recommendation; return r==='AUTO_BOOK'?'\u2705 Auto-booking initiated!':'\u23f3 Confirm within 15 min to secure.' } catch(e){ return '' } })() }}",
"chatId": "={{ $json.telegramChatId }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "5b510b0c-ebf1-4529-b5ec-c3d958363042",
"name": "Code 2 - Book & Log to Sheets",
"type": "n8n-nodes-base.code",
"position": [
1936,
240
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// ============================================================\n// AUTO-BOOK SLOT + LOG TO GOOGLE SHEETS\n// ============================================================\nconst item = $input.item.json;\n\n// --- Parse AI score and recommendation from previous node output\nlet aiScore = 0;\nlet recommendation = \"SKIP\";\n\ntry {\n const parsed = JSON.parse((item.output || \"{}\").replace(/```json|```/g, \"\").trim());\n aiScore = parsed.score || 0;\n recommendation = parsed.recommendation || \"SKIP\";\n} catch (e) {\n recommendation = \"SKIP\";\n}\n\n// --- Decide whether to book\nconst shouldAutoBook = recommendation === \"AUTO_BOOK\" && aiScore >= item.autoBookThreshold;\n\nlet bookingStatus = \"Logged - No Action\";\nlet confirmationId = null;\nlet bookingError = null;\n\nif (item.noSlotsFound) {\n bookingStatus = \"No Slots Found\";\n\n} else if (shouldAutoBook) {\n // ------------------------------------------------------------------\n // REAL BOOKING CALL\n // Replace the URL and body below with your actual portal's booking API\n // ------------------------------------------------------------------\n try {\n const bookingPayload = {\n applicantName: item.applicantName,\n passportNumber: item.passportNumber,\n visaType: item.visaType,\n slotDate: item.slotDate,\n slotTime: item.slotTime,\n slotLocation: item.slotLocation\n };\n\n // NOTE: Replace this URL with your real booking endpoint\n // For live use, move this to an HTTP Request node instead\n // and pass credentials via n8n credential manager\n const response = await fetch(\"https://YOUR_BOOKING_ENDPOINT/reserve\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(bookingPayload)\n });\n\n if (response.ok) {\n const result = await response.json();\n bookingStatus = \"Booked\";\n confirmationId = result.confirmationId || result.reference || \"CONF-\" + Date.now();\n } else {\n bookingStatus = \"Booking Failed - HTTP \" + response.status;\n bookingError = await response.text();\n }\n\n } catch (e) {\n bookingStatus = \"Booking Error\";\n bookingError = e.message;\n }\n\n} else if (recommendation === \"NOTIFY\") {\n bookingStatus = \"Notified - Awaiting Manual Confirmation\";\n\n} else {\n bookingStatus = \"Skipped - Score Too Low\";\n}\n\n// --- Build Google Sheets row\n// Matches columns: Date | RequestId | Flight | Airport | Terminal |\n// Departure | SecWait | ImmWait | BrdWait | TotalWait | Congestion |\n// RecommendedArrival | OptimalTime | AlertLevel | LoggedAt\nconst sheetRow = [\n new Date().toISOString().split(\"T\")[0], // Date\n item.requestId || \"\",\n item.applicantName || \"\",\n item.visaType || \"\",\n item.slotDate || \"\",\n item.slotTime || \"\",\n item.slotLocation || \"\",\n item.availableSeats || \"\",\n aiScore,\n recommendation,\n bookingStatus,\n confirmationId || \"\",\n bookingError || \"\",\n new Date().toISOString() // LoggedAt\n];\n\nreturn {\n json: {\n ...item,\n aiScore,\n recommendation,\n bookingStatus,\n confirmationId,\n bookingError,\n sheetRow\n }\n};"
},
"typeVersion": 2
},
{
"id": "344fa2a0-c70c-46a7-9fe7-b55646da86c4",
"name": "Send Final Status",
"type": "n8n-nodes-base.telegram",
"position": [
2192,
240
],
"parameters": {
"text": "={{ $json.bookingStatus === 'Booked' ? '\u2705 *BOOKED!* Confirmation: ' + $json.confirmationId + '\\n\\n\ud83d\udcc5 ' + $json.slotDate + ' ' + $json.slotTime + ' @ ' + $json.slotLocation : '\ud83d\udccb *Slot Logged* \u2014 ' + $json.bookingStatus + '\\n\\n\ud83d\udcc5 ' + $json.slotDate + ' @ ' + $json.slotLocation }}",
"chatId": "={{ $json.telegramChatId }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "f076b4a4-1fa5-465c-bbc9-78b8b56ea399",
"name": "Wait - Confirm Window (15 min)",
"type": "n8n-nodes-base.wait",
"unit": "minutes",
"amount": 15,
"position": [
1696,
240
],
"parameters": {},
"resumeWith": "timeInterval",
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "3095bffb-61e6-45a3-94d8-ce9236f20609",
"connections": {
"AI - Score Slot": {
"main": [
[
{
"node": "Send Telegram Alert",
"type": "main",
"index": 0
}
]
]
},
"Set User Config": {
"main": [
[
{
"node": "Code 1 - Fetch & Parse Slots",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI - Score Slot",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Poll Every 10 Min": {
"main": [
[
{
"node": "Set User Config",
"type": "main",
"index": 0
}
]
]
},
"Send Telegram Alert": {
"main": [
[
{
"node": "Wait - Confirm Window (15 min)",
"type": "main",
"index": 0
}
]
]
},
"Code 1 - Fetch & Parse Slots": {
"main": [
[
{
"node": "AI - Score Slot",
"type": "main",
"index": 0
}
]
]
},
"Code 2 - Book & Log to Sheets": {
"main": [
[
{
"node": "Send Final Status",
"type": "main",
"index": 0
}
]
]
},
"Wait - Confirm Window (15 min)": {
"main": [
[
{
"node": "Code 2 - Book & Log to Sheets",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
openAiApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs every 10 minutes to check a visa appointment portal for available slots, uses OpenAI to score each slot against your preferences, sends Telegram alerts, and optionally attempts to auto-book high-scoring slots before sending a final status update and preparing…
Source: https://n8n.io/workflows/16204/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This workflow is for beauty salons who want consistent, high‑quality social media content without writing every post manually. It also suits agencies and automation builders who manage multiple beauty
System Architecture Two integrated N8N workflows providing automated US stock portfolio management through Telegram:
staying consistent with personal brand content is hard when you're running everything yourself. you know what you want to say — but turning raw thoughts into polished posts takes hours.
Online Marketing Weekly Report. Uses scheduleTrigger, lmChatOpenAi, toolWorkflow, executeWorkflowTrigger. Scheduled trigger; 51 nodes.