AutomationFlowsAI & RAG › Route Patients to the Best Nearby Hospital Using Google Maps, Gpt-4.1 and Twilio

Route Patients to the Best Nearby Hospital Using Google Maps, Gpt-4.1 and Twilio

ByOneclick AI Squad @oneclick-ai on n8n.io

This workflow accepts emergency location requests (or runs on a 5-minute schedule), finds and ranks nearby hospitals/clinics using Google Places, capacity data, and Google Distance Matrix, generates a routing message with OpenAI, sends the recommendation via Twilio SMS, logs the…

Webhook trigger★★★★☆ complexityAI-powered28 nodesHTTP RequestAgentOpenAI Chat
AI & RAG Trigger: Webhook Nodes: 28 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #16760 — we link there as the canonical source.

This workflow follows the Agent → HTTP Request 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 →

Download .json
{
  "id": "xGzJosFbmbozPCGr",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Find nearest available hospital and send emergency routing via SMS with AI",
  "tags": [],
  "nodes": [
    {
      "id": "ac908607-7254-497a-910f-1627ef05d7c5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -464
      ],
      "parameters": {
        "width": 960,
        "height": 1288,
        "content": "## Emergency Medical Facility Finder\n\nThis workflow monitors nearby hospitals and clinics in real time, scores them by travel time, ED capacity, and wait times, and instantly routes patients to the fastest accessible care during a medical emergency \u2014 delivering the recommendation via SMS and returning a full JSON response to the caller.\n\n### Who's it for\n- Travelers experiencing sudden medical emergencies away from home\n- Healthcare dispatchers managing multi-facility triage decisions\n- Mobile health apps requiring real-time facility routing APIs\n- Emergency services needing live bed and ED availability data\n\n### How it works\n1. Patient or dispatcher sends a POST request with GPS coordinates and severity level\n2. Python validates coordinates, maps severity to a score (1\u20133), and sets the search radius\n3. Google Places API fetches nearby hospitals and clinics within the radius\n4. Results are merged, deduplicated, and enriched with live ED capacity data per facility\n5. Google Distance Matrix API calculates real-time driving duration to each facility\n6. Python scoring engine ranks facilities using a weighted composite score (travel time, beds, wait time) adjusted by severity\n7. The top-ranked facility is selected with two backup alternatives\n8. AI generates a calm, concise routing message for the patient or dispatcher\n9. SMS alert is sent via Twilio with directions link\n10. Routing event is logged to Google Sheets for audit and analytics\n11. Webhook returns the full routing recommendation as JSON\n\n### How to set up\n1. Import this workflow into n8n\n2. Store your Google Maps API key as a Header Auth credential and attach it to the three Maps nodes\n3. Add Twilio credentials (Account SID + Auth Token) via n8n Basic Auth credential manager\n4. Replace YOUR_TWILIO_NUMBER with your Twilio phone number in the SMS node\n5. Add OpenAI credentials to the OpenAI Chat Model node\n6. Add Google Sheets OAuth2 credentials and replace YOUR_SHEET_ID\n7. Activate the workflow and POST a test request with latitude, longitude, and severity\n\n### Requirements\n- Google Maps Platform API key (Places API + Distance Matrix API enabled)\n- Hospital capacity data API (HHS Protect, state health API, or internal FHIR endpoint)\n- Twilio account with an SMS-capable phone number\n- Google Sheets with OAuth2 credentials\n- OpenAI API key (GPT-4.1-mini or above)\n\n### How to customize\n- Adjust severity scoring weights in Python - Score Facilities (default: critical weights travel 50%, wait 30%, beds 20%)\n- Change search radius per severity tier in Python - Validate & Score Severity (default: 20 km critical, 10 km moderate)\n- Add urgent care and pharmacy facility types to the Places query URLs\n- Modify the SMS template in JS - Format Final Output to match your brand or language\n- Swap Twilio SMS for WhatsApp by changing the Twilio API endpoint\n- Add a Google Maps Directions API call after facility selection to return turn-by-turn directions\n\n### Google Sheets Column Layout\nSet up your EmergencyLog tab with these headers in row 1:\nRequest ID | Date | Timestamp | Severity | Facility Name | Address | Travel (min) | Wait (min) | Available Beds | ED Status | Composite Score | Status"
      },
      "typeVersion": 1
    },
    {
      "id": "2ed864e4-bc32-4998-9eb8-b99d7d1296db",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1904,
        -320
      ],
      "parameters": {
        "color": 3,
        "width": 780,
        "height": 740,
        "content": "## 1. Trigger & Location Intake\n\nAccepts emergency requests via webhook POST in real time, or runs a periodic facility refresh every 5 minutes via the scheduler (useful for pre-caching nearby facilities for known routes).\n\nRequired POST fields: latitude, longitude, severity (critical / moderate / minor), phone.\n\nThe Set node normalizes all fields and generates a unique requestId. The Python node validates coordinate ranges, maps severity to a numeric score, and sets the appropriate search radius \u2014 wider for critical cases (20 km) to find more options fast.\n\nIf coordinates are missing or invalid, the filter routes to an error handler that immediately returns a clear error response to the caller."
      },
      "typeVersion": 1
    },
    {
      "id": "36a4d359-2599-4c51-9ce7-f33d9552065e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        -272
      ],
      "parameters": {
        "color": 3,
        "width": 820,
        "height": 992,
        "content": "## 2. Facility Discovery & Capacity Fetch\n\nTwo parallel Google Places API calls fetch nearby hospitals and medical clinics within the calculated search radius. Results are merged and deduplicated by Place ID into a single facility list.\n\nEach facility then receives a live capacity data lookup (ED status, available beds, wait time in minutes). This node uses continueOnFail so that if a facility's capacity data is unavailable, the workflow continues with safe fallback defaults rather than failing the entire request.\n\nTravel time to each facility is then fetched from the Google Distance Matrix API using real-time traffic conditions."
      },
      "typeVersion": 1
    },
    {
      "id": "0145e08d-c418-418d-83b6-8ed13d6a8d83",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3584,
        -176
      ],
      "parameters": {
        "color": 3,
        "width": 860,
        "height": 640,
        "content": "## 3. Scoring, Routing & AI Message\n\nThe Python scoring engine calculates a composite score for each facility using three weighted factors: travel time, available beds, and wait time. Weights shift based on severity \u2014 critical cases prioritize travel time above all else, while minor cases weigh capacity and wait time more heavily. Facilities on ED diversion or closure receive a heavy penalty.\n\nJS - Select Best Facility sorts all scored facilities and picks the top result, keeping two alternatives as backups.\n\nThe AI node generates a clear, calm routing message under 200 words \u2014 appropriate for a stressed patient or dispatcher \u2014 including the recommended facility, reasoning, backup options, and any urgency advice based on severity."
      },
      "typeVersion": 1
    },
    {
      "id": "e969dea0-01a6-440d-be1d-115758885da4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4480,
        -240
      ],
      "parameters": {
        "color": 3,
        "width": 1144,
        "height": 864,
        "content": "## 4. Notify & Log\n\nThe final output is formatted with an SMS-ready body including a Google Maps directions link.\n\nSend SMS Routing Alert fires via Twilio to the patient's phone number. Both the SMS node and the Sheets logger use continueOnFail so a Twilio outage never blocks the webhook response.\n\nLog to Google Sheet Tracker appends a full audit row for every routing event \u2014 useful for analytics, compliance, and identifying consistently overloaded facilities.\n\nWebhook Response - Send Result returns the complete routing recommendation as JSON to the original caller, including the selected facility, travel time, ED status, wait time, routing message, and the two backup options."
      },
      "typeVersion": 1
    },
    {
      "id": "a146b4f7-2708-403b-9149-014c8771eea7",
      "name": "Webhook - Emergency Request",
      "type": "n8n-nodes-base.webhook",
      "position": [
        2064,
        64
      ],
      "parameters": {
        "path": "emergency-facility-finder",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1.1
    },
    {
      "id": "f9ddba86-5eef-4ad1-bc9a-5de34e25d69a",
      "name": "Poll - Periodic Facility Refresh",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        2064,
        256
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/5 * * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "5c5152aa-6d01-4504-8fb4-c23241c6fb3a",
      "name": "Prepare Location Context",
      "type": "n8n-nodes-base.set",
      "position": [
        2288,
        160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "latitude",
              "type": "number",
              "value": "={{ $json.latitude || $json.body?.latitude || 0 }}"
            },
            {
              "name": "longitude",
              "type": "number",
              "value": "={{ $json.longitude || $json.body?.longitude || 0 }}"
            },
            {
              "name": "address",
              "type": "string",
              "value": "={{ $json.address || $json.body?.address || '' }}"
            },
            {
              "name": "severityLevel",
              "type": "string",
              "value": "={{ $json.severity || $json.body?.severity || 'moderate' }}"
            },
            {
              "name": "patientPhone",
              "type": "string",
              "value": "={{ $json.phone || $json.body?.phone || '' }}"
            },
            {
              "name": "requestId",
              "type": "string",
              "value": "={{ $json.requestId || 'EM-' + Date.now().toString() }}"
            },
            {
              "name": "requestTimestamp",
              "type": "string",
              "value": "={{ new Date().toISOString() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "80f282a0-f88c-4e59-b185-f699975c39f0",
      "name": "Python - Validate & Score Severity",
      "type": "n8n-nodes-base.code",
      "position": [
        2512,
        160
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "python",
        "pythonCode": "# Validate and normalize location input\n# Accepts GPS coordinates or a text address\nimport json\n\nitem = _input.item['json']\n\nlat = item.get('latitude', 0)\nlng = item.get('longitude', 0)\naddress = item.get('address', '')\nseverity = item.get('severityLevel', 'moderate').lower()\n\n# Severity scoring: critical=3, moderate=2, minor=1\nseverity_map = {'critical': 3, 'severe': 3, 'high': 3,\n                'moderate': 2, 'medium': 2,\n                'minor': 1, 'low': 1}\nseverityScore = severity_map.get(severity, 2)\n\n# Determine if coordinates are valid\nhasCoords = (lat != 0 and lng != 0 and\n             -90 <= lat <= 90 and -180 <= lng <= 180)\n\n# Radius (meters): critical cases search wider (20 km)\nsearchRadius = 20000 if severityScore == 3 else 10000\n\nreturn {\n    'latitude': lat,\n    'longitude': lng,\n    'address': address,\n    'severityLevel': severity,\n    'severityScore': severityScore,\n    'hasCoords': hasCoords,\n    'searchRadius': searchRadius,\n    'requestId': item.get('requestId', ''),\n    'patientPhone': item.get('patientPhone', ''),\n    'requestTimestamp': item.get('requestTimestamp', '')\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "1de4a796-b5a2-484f-8e3e-3ffe34854bf4",
      "name": "Filter - Valid Location",
      "type": "n8n-nodes-base.filter",
      "position": [
        2736,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.hasCoords }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "78054e78-ee0e-429c-808b-6fd68bdabbd7",
      "name": "Fetch Nearby Hospitals",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2976,
        64
      ],
      "parameters": {
        "url": "=https://maps.googleapis.com/maps/api/place/nearbysearch/json?location={{ $json.latitude }},{{ $json.longitude }}&radius={{ $json.searchRadius }}&type=hospital&key=YOUR_GOOGLE_MAPS_API_KEY",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "a579f496-9e0a-44de-97a7-537c32660e03",
      "name": "Fetch Nearby Clinics",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2976,
        256
      ],
      "parameters": {
        "url": "=https://maps.googleapis.com/maps/api/place/nearbysearch/json?location={{ $json.latitude }},{{ $json.longitude }}&radius={{ $json.searchRadius }}&type=doctor|clinic|urgent_care&key=YOUR_GOOGLE_MAPS_API_KEY",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "8b867c2a-262b-453c-ab53-93d513d8be28",
      "name": "JS - Merge & Deduplicate Facilities",
      "type": "n8n-nodes-base.code",
      "position": [
        3216,
        160
      ],
      "parameters": {
        "jsCode": "// Merge hospitals and clinics into a single facility list\nconst allItems = $input.all();\nconst facilityList = [];\n\nfor (const item of allItems) {\n  const results = item.json.results || [];\n  for (const place of results) {\n    facilityList.push({\n      placeId: place.place_id,\n      name: place.name,\n      address: place.vicinity,\n      latitude: place.geometry?.location?.lat,\n      longitude: place.geometry?.location?.lng,\n      rating: place.rating || null,\n      openNow: place.opening_hours?.open_now ?? true,\n      types: place.types || []\n    });\n  }\n}\n\n// Deduplicate by placeId\nconst seen = new Set();\nconst unique = facilityList.filter(f => {\n  if (seen.has(f.placeId)) return false;\n  seen.add(f.placeId);\n  return true;\n});\n\nreturn unique.map(f => ({ json: f }));"
      },
      "typeVersion": 2
    },
    {
      "id": "21624ff7-7b95-433c-a742-25397345262d",
      "name": "Fetch Facility Capacity",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3440,
        160
      ],
      "parameters": {
        "url": "=https://api.healthdata.gov/v1/facility-capacity?placeId={{ $json.placeId }}&fields=ed_status,available_beds,wait_time_minutes",
        "options": {
          "response": {
            "response": {}
          }
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "e6ecdeaa-715b-47e5-974b-d87a6c52dd28",
      "name": "JS - Enrich with Capacity",
      "type": "n8n-nodes-base.code",
      "position": [
        3664,
        160
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Merge capacity data with facility info\n// Falls back to safe defaults if capacity API is unavailable\nconst facility = $input.item.json;\nconst capacity = facility.error ? {} : facility;\n\nreturn {\n  json: {\n    ...facility,\n    edStatus: capacity.ed_status || 'unknown',\n    availableBeds: capacity.available_beds ?? -1,\n    waitTimeMinutes: capacity.wait_time_minutes ?? 999,\n    capacityDataAvailable: !facility.error\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "7e7e2f10-dde3-44cf-af45-8637d7d33767",
      "name": "Fetch Travel Time",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3888,
        160
      ],
      "parameters": {
        "url": "=https://maps.googleapis.com/maps/api/distancematrix/json?origins={{ $('Python - Validate & Score Severity').item.json.latitude }},{{ $('Python - Validate & Score Severity').item.json.longitude }}&destinations={{ $json.latitude }},{{ $json.longitude }}&mode=driving&departure_time=now&key=YOUR_GOOGLE_MAPS_API_KEY",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "b0a50c6e-c591-4af7-bf72-ae6af38ba6b7",
      "name": "Python - Score Facilities",
      "type": "n8n-nodes-base.code",
      "position": [
        4112,
        160
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "python",
        "pythonCode": "# Composite facility scoring algorithm\n# Score = (1/travel_time)*w1 + bed_availability*w2 + (1/wait_time)*w3\n# Weights are adjusted by severity level\n\nitem = _input.item['json']\n\n# Extract travel info from Distance Matrix response\nelements = (item.get('rows') or [{}])[0].get('elements') or [{}]\nelement = elements[0] if elements else {}\ntravelSeconds = element.get('duration_in_traffic', {}).get('value') or \\\n                element.get('duration', {}).get('value') or 3600\ntravelMinutes = travelSeconds / 60\n\navailableBeds = item.get('availableBeds', -1)\nwaitMinutes = item.get('waitTimeMinutes', 999)\nedStatus = item.get('edStatus', 'unknown').lower()\n\n# If ED is on diversion, penalize heavily\nedPenalty = 0 if edStatus not in ('diversion', 'closed') else 1000\n\n# Scoring weights (critical cases weight travel more)\nseverityScore = int(item.get('severityScore', 2))\nif severityScore == 3:  # critical\n    w1, w2, w3 = 50, 20, 30\nelif severityScore == 1:  # minor\n    w1, w2, w3 = 20, 40, 40\nelse:  # moderate\n    w1, w2, w3 = 35, 30, 35\n\n# Normalize and compute score (higher = better)\nscoreTravelTime = w1 * (1 / max(travelMinutes, 1))\nscoreBeds = w2 * (max(availableBeds, 0) / 100) if availableBeds >= 0 else 0\nscoreWaitTime = w3 * (1 / max(waitMinutes, 1))\n\ntotalScore = scoreTravelTime + scoreBeds + scoreWaitTime - edPenalty\n\nreturn {\n    **item,\n    'travelMinutes': round(travelMinutes, 1),\n    'compositeScore': round(totalScore, 4),\n    'edPenalty': edPenalty,\n    'scoringWeights': {'travel': w1, 'beds': w2, 'wait': w3}\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "69de2f58-f04d-4c5a-be25-693a59389e77",
      "name": "JS - Select Best Facility",
      "type": "n8n-nodes-base.code",
      "position": [
        4336,
        160
      ],
      "parameters": {
        "jsCode": "// Select the top-ranked facility\n// Sort by compositeScore descending, pick #1\nconst items = $input.all().map(i => i.json);\n\nitems.sort((a, b) => (b.compositeScore || 0) - (a.compositeScore || 0));\n\nconst best = items[0];\nconst alternatives = items.slice(1, 3); // keep 2 backups\n\nif (!best) {\n  return [{ json: { error: 'No facilities found in range', requestId: items[0]?.requestId } }];\n}\n\nreturn [{\n  json: {\n    ...best,\n    alternativeFacilities: alternatives.map(f => ({\n      name: f.name,\n      address: f.address,\n      travelMinutes: f.travelMinutes,\n      waitTimeMinutes: f.waitTimeMinutes,\n      compositeScore: f.compositeScore\n    }))\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ae5b77c1-3ecd-4b43-b592-3e50244dc385",
      "name": "AI - Generate Routing Message",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        4496,
        160
      ],
      "parameters": {
        "text": "=You are an emergency medical routing assistant. A patient needs urgent care.\n\nPatient Severity: {{ $json.severityLevel }} (score: {{ $json.severityScore }}/3)\nRequest ID: {{ $json.requestId }}\nTimestamp: {{ $json.requestTimestamp }}\n\nRecommended Facility:\n- Name: {{ $json.name }}\n- Address: {{ $json.address }}\n- Travel Time: {{ $json.travelMinutes }} minutes\n- ED Status: {{ $json.edStatus }}\n- Available Beds: {{ $json.availableBeds }}\n- Wait Time: {{ $json.waitTimeMinutes }} minutes\n\nAlternatives: {{ JSON.stringify($json.alternativeFacilities) }}\n\nWrite a concise, calm, and clear routing message (under 200 words) for the patient or dispatcher. Include:\n1. The recommended facility with travel time\n2. One sentence on why it was chosen\n3. A brief note on the two backup options\n4. Any urgent action advice based on severity level\n\nDo NOT include any disclaimers about being an AI.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "fe85a905-abf9-40fe-9341-a53dc1e203bf",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        4512,
        336
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "ad62e415-94c7-41ad-ab09-68a22d32a9f1",
      "name": "JS - Format Final Output",
      "type": "n8n-nodes-base.code",
      "position": [
        4784,
        160
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Format final routing output for notification and logging\nconst item = $input.item.json;\n\nconst routingMessage = item.output || item.response || item.text ||\n  `Head to ${item.name} (${item.address}). Estimated travel: ${item.travelMinutes} min.`;\n\nreturn {\n  json: {\n    ...item,\n    routingMessage,\n    smsBody: `\ud83d\udea8 EMERGENCY ROUTING\\n${routingMessage}\\n\\nDirections: https://maps.google.com/?q=${item.latitude},${item.longitude}`,\n    logDate: new Date().toISOString().split('T')[0],\n    logTimestamp: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "3a6b00e7-a3e9-4bcd-88ab-ad1c755be829",
      "name": "Wait - Review Buffer",
      "type": "n8n-nodes-base.wait",
      "position": [
        5008,
        160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "36e00ed1-b27a-4b77-8752-af527a095d21",
      "name": "Send SMS Routing Alert",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5232,
        64
      ],
      "parameters": {
        "url": "https://api.twilio.com/2010-04-01/Accounts/YOUR_TWILIO_ACCOUNT_SID/Messages.json",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "form-urlencoded",
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "To",
              "value": "={{ $json.patientPhone }}"
            },
            {
              "name": "From",
              "value": "+1YOUR_TWILIO_NUMBER"
            },
            {
              "name": "Body",
              "value": "={{ $json.smsBody }}"
            }
          ]
        },
        "genericAuthType": "httpBasicAuth"
      },
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "9fb4c65d-bb7f-462d-95bc-1975aab2d740",
      "name": "Log to Google Sheet Tracker",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5232,
        256
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/EmergencyLog!A1:append?valueInputOption=USER_ENTERED",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"values\": [[\n    \"{{ $json.requestId }}\",\n    \"{{ $json.logDate }}\",\n    \"{{ $json.logTimestamp }}\",\n    \"{{ $json.severityLevel }}\",\n    \"{{ $json.name }}\",\n    \"{{ $json.address }}\",\n    \"{{ $json.travelMinutes }}\",\n    \"{{ $json.waitTimeMinutes }}\",\n    \"{{ $json.availableBeds }}\",\n    \"{{ $json.edStatus }}\",\n    \"{{ $json.compositeScore }}\",\n    \"Routed\"\n  ]]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "94ee244a-28ce-4226-83f8-05123e69d152",
      "name": "Webhook Response - Send Result",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        5456,
        160
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, requestId: $json.requestId, recommendedFacility: $json.name, address: $json.address, travelMinutes: $json.travelMinutes, edStatus: $json.edStatus, waitTimeMinutes: $json.waitTimeMinutes, routingMessage: $json.routingMessage, alternatives: $json.alternativeFacilities }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "e2c35748-9513-4c8e-be94-4e9c16428ea8",
      "name": "Error - Invalid Location",
      "type": "n8n-nodes-base.code",
      "position": [
        2976,
        464
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Error handler: no valid location coordinates provided\nconst item = $input.item.json;\nconsole.error(`[EmergencyFinder] Invalid location for requestId=${item.requestId}`);\nreturn {\n  json: {\n    ...item,\n    error: 'Invalid or missing GPS coordinates. Please provide valid latitude and longitude.',\n    errorCode: 'INVALID_LOCATION',\n    success: false\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "5e2d632c-2327-475e-8a61-576b710ad1e3",
      "name": "Webhook Response - Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3344,
        464
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error, errorCode: $json.errorCode, requestId: $json.requestId }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "dc433072-4703-42f1-a5c9-2a823bad81bb",
      "name": "Wait For Result",
      "type": "n8n-nodes-base.wait",
      "position": [
        3168,
        464
      ],
      "parameters": {
        "unit": "minutes"
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "9cfdfbca-340f-48be-a739-cf56d4557c9a",
  "connections": {
    "Wait For Result": {
      "main": [
        [
          {
            "node": "Webhook Response - Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Travel Time": {
      "main": [
        [
          {
            "node": "Python - Score Facilities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Routing Message",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Nearby Clinics": {
      "main": [
        [
          {
            "node": "JS - Merge & Deduplicate Facilities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Review Buffer": {
      "main": [
        [
          {
            "node": "Send SMS Routing Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log to Google Sheet Tracker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Nearby Hospitals": {
      "main": [
        [
          {
            "node": "JS - Merge & Deduplicate Facilities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send SMS Routing Alert": {
      "main": [
        [
          {
            "node": "Webhook Response - Send Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Facility Capacity": {
      "main": [
        [
          {
            "node": "JS - Enrich with Capacity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter - Valid Location": {
      "main": [
        [
          {
            "node": "Fetch Nearby Hospitals",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Nearby Clinics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Error - Invalid Location",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error - Invalid Location": {
      "main": [
        [
          {
            "node": "Wait For Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Format Final Output": {
      "main": [
        [
          {
            "node": "Wait - Review Buffer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Location Context": {
      "main": [
        [
          {
            "node": "Python - Validate & Score Severity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Enrich with Capacity": {
      "main": [
        [
          {
            "node": "Fetch Travel Time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Select Best Facility": {
      "main": [
        [
          {
            "node": "AI - Generate Routing Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Python - Score Facilities": {
      "main": [
        [
          {
            "node": "JS - Select Best Facility",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to Google Sheet Tracker": {
      "main": [
        [
          {
            "node": "Webhook Response - Send Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Emergency Request": {
      "main": [
        [
          {
            "node": "Prepare Location Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Routing Message": {
      "main": [
        [
          {
            "node": "JS - Format Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll - Periodic Facility Refresh": {
      "main": [
        [
          {
            "node": "Prepare Location Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Python - Validate & Score Severity": {
      "main": [
        [
          {
            "node": "Filter - Valid Location",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Merge & Deduplicate Facilities": {
      "main": [
        [
          {
            "node": "Fetch Facility Capacity",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow accepts emergency location requests (or runs on a 5-minute schedule), finds and ranks nearby hospitals/clinics using Google Places, capacity data, and Google Distance Matrix, generates a routing message with OpenAI, sends the recommendation via Twilio SMS, logs the…

Source: https://n8n.io/workflows/16760/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

VEP WAPP. Uses openAi, lmChatOpenAi, toolCalculator, agent. Webhook trigger; 100 nodes.

OpenAI, OpenAI Chat, Tool Calculator +7
AI & RAG

⏺ 🚀 How it works

Agent, Anthropic Chat, Output Parser Structured +6
AI & RAG

L&D_AgentsAI_ATIVO. Uses httpRequest, agent, googleCalendarTool, toolSerpApi. Webhook trigger; 93 nodes.

HTTP Request, Agent, Google Calendar Tool +9
AI & RAG

AI Customer Service Automation - Portfolio. Uses googleSheets, httpRequest, agent, memoryBufferWindow. Webhook trigger; 91 nodes.

Google Sheets, HTTP Request, Agent +9
AI & RAG

CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.

Postgres, Mcp Client Tool, Google Drive Tool +14