AutomationFlowsAI & RAG › Monitor Multi-city Courier Weather Risk with Open-meteo, Gpt-4o, Gemini and…

Monitor Multi-city Courier Weather Risk with Open-meteo, Gpt-4o, Gemini and…

Original n8n title: Monitor Multi-city Courier Weather Risk with Open-meteo, Gpt-4o, Gemini and Slack

ByMychel Garzon @mychel-garzon on n8n.io

This workflow checks 48-hour forecasts from Open-Meteo for multiple cities on a 6-hour schedule or via an on-demand n8n form, scores conditions against courier-impact thresholds, uses OpenAI (with Google Gemini fallback) to draft Slack-ready alerts, and posts operational updates…

Event trigger★★★★☆ complexityAI-powered23 nodesOpenAI ChatChain LlmGoogle Gemini ChatSlackFormHTTP RequestForm Trigger
AI & RAG Trigger: Event Nodes: 23 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Chainllm → Form Trigger 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": "WD4cxrAAUH6XjbbU",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Monitor multi-city courier weather risk with Open-Meteo, GPT-4o and Slack",
  "tags": [],
  "nodes": [
    {
      "id": "90550ca5-61fe-4977-8c85-cccacbeaa193",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -208,
        384
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "aa672b5a-659a-4c31-95bc-929bde7ba6c0",
      "name": "OpenAi \u2014 write alerts",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "onError": "continueErrorOutput",
      "position": [
        -96,
        176
      ],
      "parameters": {
        "text": "=Write a Slack alert for each city below where courier-impacting weather is forecast.\n\nFor each city write exactly 3 sentences:\n1. What conditions are coming and when.\n2. What this means for courier operations \u2014 acceptance rate, ETAs, rider supply.\n3. One concrete action the OM should take now.\n\nClimate context (thresholds are city-relative, not global):\n- Helsinki: cold baseline, snow normal \u2014 an alert means genuinely severe conditions\n- Lisbon: Mediterranean \u2014 heat above 42C and any snow are the primary risks\n- Tallinn: Baltic continental \u2014 harsh winters, high snow tolerance, wind from the sea\n- Nairobi: East African highland \u2014 long rains cause flooding, cool nights make heat alerts rare\n\n\nSeverity: URGENT = next 6h or peak window (11-14h or 17-22h local). HEADS_UP = 6-48h out.\nTone: direct, calm, no alarmism. No markdown. No bullet points.\n\nRespond ONLY with a valid JSON array, no preamble, no code fences:\n[{ \"city\": \"string\", \"severity\": \"URGENT\" or \"HEADS_UP\", \"message\": \"string\" }]\n\nCities:\n{{ JSON.stringify($json.toAlert) }}",
        "promptType": "define",
        "needsFallback": true
      },
      "typeVersion": 1.4
    },
    {
      "id": "f51bc0c3-6590-44df-b81b-cfbf4e8eb3c9",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        80,
        384
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "d914b099-035a-479e-ae2c-3cb58990b59b",
      "name": "Slack \u2014 Post Error",
      "type": "n8n-nodes-base.slack",
      "position": [
        288,
        688
      ],
      "parameters": {
        "select": "channel",
        "blocksUi": "={{ $json.blocks }}",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#ops-weather-alerts"
        },
        "messageType": "block",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3e1c3637-1d23-4209-893d-a9e7cf2805b6",
      "name": "Format Error Msg",
      "type": "n8n-nodes-base.code",
      "position": [
        0,
        688
      ],
      "parameters": {
        "jsCode": "const src=$json;\nconst failed=src.failed||[],isTotal=src.totalOutage;\nconst names=failed.map(f=>f.name).join(', ');\nconst header=isTotal?':rotating_light: Weather feed outage \u2014 ALL MARKETS':':warning: Feed unavailable \u2014 '+names;\nconst body=isTotal?'All city weather feeds failed. The system cannot assess courier risk. Manual checks required across all markets before the next courier briefing.':'Could not fetch forecast for '+names+'. Manual weather check required before next courier briefing for these markets. Other cities are unaffected.';\nreturn [{json:{blocks:[{type:'header',text:{type:'plain_text',text:header}},{type:'section',text:{type:'mrkdwn',text:body}},{type:'context',elements:[{type:'mrkdwn',text:'Run: '+new Date().toUTCString()}]}]}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cf15d71d-8a93-4d95-8fc3-cba12d2b8369",
      "name": "Form",
      "type": "n8n-nodes-base.form",
      "position": [
        976,
        144
      ],
      "parameters": {
        "options": {},
        "operation": "completion",
        "completionTitle": "Alert sent to Slack",
        "completionMessage": "=Your on-demand weather check for {{ $('Score + dedup').first().json.toAlert[0]?.city || 'the selected city' }} has been processed.\n\nSeverity: {{ $('Score + dedup').first().json.toAlert[0]?.severity || 'no alert \u2014 conditions within normal thresholds' }}\n\nCheck #ops-weather-alerts in Slack."
      },
      "typeVersion": 2.5
    },
    {
      "id": "486d586a-b730-442d-8198-6d2a6056f20e",
      "name": "Slack \u2014 Post Alerts",
      "type": "n8n-nodes-base.slack",
      "onError": "continueErrorOutput",
      "position": [
        624,
        160
      ],
      "parameters": {
        "text": "={{ $json.blocks[0].text.text }} {{ $json.blocks[1].text.text }}",
        "select": "channel",
        "blocksUi": "={{ $json.blocks }}",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "ops-weather-alerts"
        },
        "messageType": "block",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9d263e84-a68f-4f64-9b54-07822ebf27d0",
      "name": "Format Alerts & Fallback",
      "type": "n8n-nodes-base.code",
      "position": [
        288,
        160
      ],
      "parameters": {
        "jsCode": "const src=$('Score + dedup').first().json;\nconst toAlert=src.toAlert||[];\nconst input=$input.first().json,parsed=input?.output,rawText=input?.text;\n\nfunction fallback(city){\n  const w=city.windows?.next6h?.maxScore>=5?'next 6 hours':'next 24-48 hours';\n  const h=city.windows?.next6h?.peak||city.windows?.tonight?.peak||{};\n  const cnd=h.label||'adverse weather',wnd=h.wind?' with '+Math.round(h.wind)+' kph winds from the '+h.dir:'',pk=city.localCtx?.isPeak?' during the current peak window':'';\n  return cnd.charAt(0).toUpperCase()+cnd.slice(1)+wnd+' forecast in the '+w+pk+'. Conditions exceed normal thresholds and are likely to reduce courier supply and extend delivery times. Brief the courier team and consider activating contingency incentives for the affected period.';}\n\nlet alerts;\ntry{\n  let data;\n  if(Array.isArray(parsed)){data=parsed;}else{\n    const raw=(rawText||'').trim().replace(/^```json\\s*/i,'').replace(/^```\\s*/i,'').replace(/\\s*```$/i,'').trim();\n    if(!raw)throw new Error('empty');data=JSON.parse(raw);if(!Array.isArray(data))throw new Error('not array');}\n  const VALID=['HEADS_UP','URGENT'];\n  alerts=data.map(a=>({city:a.city,severity:VALID.includes(a.severity)?a.severity:'HEADS_UP',message:a.message,aiWritten:true}));\n}catch(e){\n  alerts=toAlert.map(c=>({city:c.city,severity:c.severity,message:fallback(c),aiWritten:false}));}\n\nreturn alerts.map(a=>{\n  const urgent=a.severity==='URGENT';\n  return {json:{city:a.city,blocks:[\n    {type:'header',text:{type:'plain_text',text:(urgent?':red_circle: URGENT':':large_yellow_circle: HEADS-UP')+' | '+a.city}},\n    {type:'section',text:{type:'mrkdwn',text:a.message+(a.aiWritten?'':' _(auto-generated)_')}},\n    {type:'context',elements:[{type:'mrkdwn',text:'Open-Meteo  \u00b7  '+new Date().toUTCString()+'  \u00b7  <https://open-meteo.com|Verify forecast>'}]}\n  ]}};});"
      },
      "typeVersion": 2
    },
    {
      "id": "0ce85ab3-ffdd-4d6a-b189-f2b5c2f303f6",
      "name": "Route (Alerts / Errors)",
      "type": "n8n-nodes-base.switch",
      "position": [
        -624,
        512
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "has_alerts",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "d1",
                    "operator": {
                      "type": "boolean",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.hasAlerts }}",
                    "rightValue": true
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "has_errors",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "d2",
                    "operator": {
                      "type": "boolean",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.hasErrors }}",
                    "rightValue": true
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "none",
          "allMatchingOutputs": true
        }
      },
      "typeVersion": 3
    },
    {
      "id": "9542bafe-6609-4841-a7fb-1bb9046e3be7",
      "name": "Score + dedup",
      "type": "n8n-nodes-base.code",
      "position": [
        -848,
        512
      ],
      "parameters": {
        "jsCode": "const WMO={0:'clear sky',1:'mainly clear',2:'partly cloudy',3:'overcast',45:'fog',48:'icy fog',51:'light drizzle',53:'moderate drizzle',55:'dense drizzle',61:'light rain',63:'moderate rain',65:'heavy rain',71:'light snow',73:'moderate snow',75:'heavy snow',77:'snow grains',80:'light rain showers',81:'moderate rain showers',82:'violent rain showers',85:'light snow showers',86:'heavy snow showers',95:'thunderstorm',96:'thunderstorm with hail',99:'thunderstorm heavy hail'};\nconst RANK={none:0,HEADS_UP:1,URGENT:2};\nconst nowMs=Date.now(),nowDate=new Date(nowMs),state=$getWorkflowStaticData('global');\n\nlet cfg;\ntry { cfg=$('City config').first().json; }\ncatch(e) { cfg=$('Build form city config').first().json; }\n\nconst CITIES=cfg.CITIES||[],forceSeverity=cfg.forceSeverity||null,triggeredBy=cfg.route||'SCHEDULED';\nconst toAlert=[],failed=[],allItems=$input.all();\n\nif(!allItems.length||allItems[0].json.error){\n  return [{json:{toAlert:[],failed:CITIES.map(c=>({name:c.name,error:'fetch failed'})),hasAlerts:false,hasErrors:true,totalOutage:true,triggeredBy}}];}\n\nfor(let i=0;i<CITIES.length;i++){\n  const city=CITIES[i],item=allItems[i];\n  const t={rain_mm:parseFloat(city.thresholds.rain_mm),wind_kph:parseFloat(city.thresholds.wind_kph),snow_cm:parseFloat(city.thresholds.snow_cm),heat_c:parseFloat(city.thresholds.heat_c)};\n  if(!item||!item.json.hourly){failed.push({name:city.name,error:'no data'});continue;}\n  const h=item.json.hourly;\n  function dir(deg){return['N','NE','E','SE','S','SW','W','NW'][Math.round((deg??0)/45)%8];}\n  const hours=h.time.map((ts,j)=>({ts,hoursFromNow:(new Date(ts+'Z')-nowDate)/3600000,temp:h.temperature_2m[j]??0,rain:h.precipitation[j]??0,wind:h.windspeed_10m[j]??0,dir:dir(h.winddirection_10m[j]),code:h.weathercode[j]??0,snow:h.snowfall[j]??0,label:WMO[h.weathercode[j]]||'unknown'})).filter(h=>h.hoursFromNow>=-0.5&&h.hoursFromNow<=48);\n  function score(h){let s=0;if(h.rain>t.rain_mm)s+=2;if(h.wind>t.wind_kph)s+=2;if(h.temp>t.heat_c)s+=1;if(h.snow>t.snow_cm&&t.snow_cm>0)s+=3;if(h.code>=95)s+=3;if(h.code>=71&&h.code<=77)s+=2;if(h.rain>t.rain_mm*0.7&&h.wind>t.wind_kph*0.7)s+=2;return s;}\n  function bestIn(from,to){const w=hours.filter(h=>h.hoursFromNow>=from&&h.hoursFromNow<=to);if(!w.length)return{maxScore:0,peak:null};let best=0,peak=null;for(const h of w){const s=score(h);if(s>best){best=s;peak=h;}}return{maxScore:best,peak};}\n  const windows={next6h:bestIn(0,6),tonight:bestIn(6,18),tomorrow:bestIn(18,36),extended:bestIn(36,48)};\n  const top=Math.max(...Object.values(windows).map(w=>w.maxScore));\n  let severity=forceSeverity||(windows.next6h.maxScore>=5||windows.tonight.maxScore>=5?'URGENT':top>=2?'HEADS_UP':'none');\n  const key='city_'+city.name,last=state[key]||{severity:'none',ts:0},hrs=(nowMs-last.ts)/3600000;\n  if(triggeredBy==='SCHEDULED'){\n    if(severity==='none'){if(last.severity!=='none'&&hrs>=12)state[key]={severity:'none',ts:nowMs};continue;}\n    if(!(RANK[severity]>RANK[last.severity]||hrs>=18))continue;\n    state[key]={severity,ts:nowMs};\n  }\n  const fmt=new Intl.DateTimeFormat('en-GB',{timeZone:city.tz,hour:'2-digit',minute:'2-digit',weekday:'short',hour12:false});\n  const parts=fmt.formatToParts(new Date()),hp=parts.find(p=>p.type==='hour'),wp=parts.find(p=>p.type==='weekday'),hour=hp?parseInt(hp.value,10):new Date().getUTCHours();\n  toAlert.push({city:city.name,severity,windows,triggeredBy,localCtx:{time:fmt.format(new Date()),weekday:wp?wp.value:'unknown',isPeak:(hour>=11&&hour<=14)||(hour>=17&&hour<=22)}});\n}\nreturn [{json:{toAlert,failed,triggeredBy,hasAlerts:toAlert.length>0,hasErrors:failed.length>0,totalOutage:failed.length===CITIES.length}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "bb3a8821-35da-4085-9809-a0f89bd2db4c",
      "name": "Fetch Open-Meteo",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        -1264,
        528
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 10000
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "bec80667-f090-45cf-be26-3ba94d00f2aa",
      "name": "Build form city config",
      "type": "n8n-nodes-base.code",
      "position": [
        -1648,
        688
      ],
      "parameters": {
        "jsCode": "const f=($input.first()||{json:{}}).json;\nconst name=f['City'],sev=f['Force severity'];\nconst D={\n  Helsinki:{lat:60.17,lon:24.94,tz:'Europe/Helsinki',thresholds:{rain_mm:10,wind_kph:60,snow_cm:15,heat_c:30}},\n  Tallinn: {lat:59.44,lon:24.75,tz:'Europe/Tallinn', thresholds:{rain_mm:10,wind_kph:55,snow_cm:20,heat_c:28}},\n  Lisbon:  {lat:38.72,lon:-9.14,tz:'Europe/Lisbon',  thresholds:{rain_mm:15,wind_kph:50,snow_cm:1, heat_c:42}},\n  Nairobi: {lat:-1.29,lon:36.82,tz:'Africa/Nairobi', thresholds:{rain_mm:40,wind_kph:45,snow_cm:999,heat_c:38}}\n};\nconst city=D[name];if(!city)throw new Error('Unknown city: '+name);\nconst CITIES=[{name,...city}];\nconst url=`https://api.open-meteo.com/v1/forecast?latitude=${city.lat}&longitude=${city.lon}&hourly=temperature_2m,precipitation,windspeed_10m,winddirection_10m,weathercode,snowfall&forecast_days=2&timezone=UTC`;\nreturn [{json:{route:'FORM',CITIES,url,forceSeverity:sev==='Auto-detect'?null:sev}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cd5bb7ee-fc2d-40b7-a7cb-295b6e6fdac1",
      "name": "OM on-demand form",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -1968,
        688
      ],
      "parameters": {
        "options": {},
        "formTitle": "Ops \u2014 On-demand weather check",
        "formFields": {
          "values": [
            {
              "fieldType": "dropdown",
              "fieldLabel": "City",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Helsinki"
                  },
                  {
                    "option": "Tallinn"
                  },
                  {
                    "option": "Lisbon"
                  },
                  {
                    "option": "Nairobi"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Force severity",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Auto-detect"
                  },
                  {
                    "option": "HEADS_UP"
                  },
                  {
                    "option": "URGENT"
                  }
                ]
              },
              "requiredField": true
            }
          ]
        },
        "formDescription": "Select a city to force an immediate weather assessment. Result posts to #ops-weather-alerts in Slack."
      },
      "typeVersion": 2.2
    },
    {
      "id": "e9235c91-3c36-4dd0-a54d-cea64ee3033b",
      "name": "City config",
      "type": "n8n-nodes-base.code",
      "position": [
        -1648,
        288
      ],
      "parameters": {
        "jsCode": "const CITIES=[\n  {name:'Helsinki',lat:60.17,lon:24.94,tz:'Europe/Helsinki',thresholds:{rain_mm:10,wind_kph:60,snow_cm:15,heat_c:30}},\n  {name:'Tallinn', lat:59.44,lon:24.75,tz:'Europe/Tallinn', thresholds:{rain_mm:10,wind_kph:55,snow_cm:20,heat_c:28}},\n  {name:'Lisbon',  lat:38.72,lon:-9.14,tz:'Europe/Lisbon',  thresholds:{rain_mm:15,wind_kph:50,snow_cm:1, heat_c:42}},\n  {name:'Nairobi', lat:-1.29,lon:36.82,tz:'Africa/Nairobi', thresholds:{rain_mm:40,wind_kph:45,snow_cm:999,heat_c:38}}\n];\nconst lats=CITIES.map(c=>c.lat).join(','),lons=CITIES.map(c=>c.lon).join(',');\nconst url=`https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}&hourly=temperature_2m,precipitation,windspeed_10m,winddirection_10m,weathercode,snowfall&forecast_days=2&timezone=UTC`;\nreturn [{json:{route:'SCHEDULED',CITIES,url,forceSeverity:null}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "13b1888a-81f7-4b61-b07a-c68cd3b3e9cc",
      "name": "Every 6 hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1968,
        288
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e4b2b9f2-d1a2-44d4-93ad-bf278da66a7f",
      "name": "Note \u2014 Claude1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 1032,
        "height": 320,
        "content": "## Post error notifications\n\nFormats workflow or data errors into a Slack-ready message and posts them to the configured error channel."
      },
      "typeVersion": 1
    },
    {
      "id": "a707dc2a-1ad8-4865-a934-a75c1361907a",
      "name": "Note \u2014 Claude",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 792,
        "height": 512,
        "content": "## Generate alert copy\n\nUses an LLM chain, backed by configured OpenAI and Gemini chat models, to write courier-friendly weather alert text, then formats the result with fallback handling before posting."
      },
      "typeVersion": 1
    },
    {
      "id": "0491c81a-01cc-4b71-812a-e6141e99b03a",
      "name": "Note \u2014 Merge1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -960,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 580,
        "height": 492,
        "content": "## Score and route results\n\nScores incoming weather conditions, removes duplicate alerts, and branches the workflow into the alert-generation path or the error-notification path."
      },
      "typeVersion": 1
    },
    {
      "id": "a56af61a-c437-4e3b-be2a-9383cc32944e",
      "name": "Note \u2014 Merge",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1440,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 436,
        "height": 492,
        "content": "## Fetch weather data\n\nCalls the Open-Meteo API using the prepared URL from either the scheduled or on-demand city configuration path."
      },
      "typeVersion": 1
    },
    {
      "id": "bf852f2e-e991-4568-9ec5-3b52eaaa7cd6",
      "name": "Note \u2014 Schedule1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2048,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 568,
        "height": 352,
        "content": "## On-demand city setup\n\nProvides a manual form-triggered path and converts submitted city details into the same weather request format used by the scheduled run."
      },
      "typeVersion": 1
    },
    {
      "id": "5acd63db-fa94-432e-8325-7995f49e3bcb",
      "name": "Note \u2014 Schedule",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2048,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 568,
        "height": 336,
        "content": "## Scheduled city setup\n\nStarts the recurring monitoring path every six hours and builds the default multi-city weather request configuration."
      },
      "typeVersion": 1
    },
    {
      "id": "aa001a6f-69ab-4779-ac52-c2ef1f8510b4",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2736,
        0
      ],
      "parameters": {
        "width": 624,
        "height": 864,
        "content": "## Monitor multi-city courier weather risk with Open-Meteo, GPT-4o and Slack\n\n### How it works\n\nThis workflow monitors weather for multiple cities on a six-hour schedule and also supports on-demand checks through a form. It fetches Open-Meteo data, scores conditions for courier risk, deduplicates repeated alerts, and routes results to either an AI-generated alert path or an error path. For valid alerts, GPT-style messaging is generated and posted to Slack for food delivery couriers; errors are formatted and sent to Slack separately.\n\n### Setup steps\n\n- Configure the schedule trigger interval and verify it should run every six hours in the desired timezone.\n- Update the city configuration code with target cities, coordinates, Open-Meteo URL parameters, and courier alert thresholds.\n- Configure the on-demand form fields so submitted cities can be converted into valid Open-Meteo request settings.\n- Add credentials for the OpenAI and/or Google Gemini chat model nodes used by the LLM chain.\n- Add Slack credentials and set the alert and error Slack channels in the Slack nodes.\n- Test both the scheduled path and the form-triggered path to confirm weather data, scoring, LLM output, and Slack posting work end to end.\n\n### Customization\n\nAdjust city lists, scoring thresholds, deduplication windows, Slack channels, and the LLM prompt to match local courier operations and the severity level that should trigger alerts."
      },
      "typeVersion": 1
    },
    {
      "id": "ec8c389c-9252-4351-bf4a-8faa1d4d38cd",
      "name": "Note \u2014 Claude2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 792,
        "height": 512,
        "content": "## Post courier alerts\n\nPublishes finalized weather alerts to Slack and then displays or returns a follow-up form response for the alert flow."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "6ac527ed-af74-4e8d-a9cd-242aae2226e1",
  "nodeGroups": [],
  "connections": {
    "City config": {
      "main": [
        [
          {
            "node": "Fetch Open-Meteo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 6 hours": {
      "main": [
        [
          {
            "node": "City config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score + dedup": {
      "main": [
        [
          {
            "node": "Route (Alerts / Errors)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Open-Meteo": {
      "main": [
        [
          {
            "node": "Score + dedup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Error Msg": {
      "main": [
        [
          {
            "node": "Slack \u2014 Post Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OM on-demand form": {
      "main": [
        [
          {
            "node": "Build form city config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "OpenAi \u2014 write alerts",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Slack \u2014 Post Alerts": {
      "main": [
        [
          {
            "node": "Form",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build form city config": {
      "main": [
        [
          {
            "node": "Fetch Open-Meteo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAi \u2014 write alerts": {
      "main": [
        [
          {
            "node": "Format Alerts & Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route (Alerts / Errors)": {
      "main": [
        [
          {
            "node": "OpenAi \u2014 write alerts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Msg",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Alerts & Fallback": {
      "main": [
        [
          {
            "node": "Slack \u2014 Post Alerts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "OpenAi \u2014 write alerts",
            "type": "ai_languageModel",
            "index": 1
          }
        ]
      ]
    }
  }
}

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 checks 48-hour forecasts from Open-Meteo for multiple cities on a 6-hour schedule or via an on-demand n8n form, scores conditions against courier-impact thresholds, uses OpenAI (with Google Gemini fallback) to draft Slack-ready alerts, and posts operational updates…

Source: https://n8n.io/workflows/16535/ — 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

Episode 11: AI shorts factory app. Uses httpRequest, googleSheets, lmChatOpenAi, lmChatOllama. Event-driven trigger; 96 nodes.

HTTP Request, Google Sheets, OpenAI Chat +15
AI & RAG

Host Your Own AI Deep Research Agent with n8n, Apify and OpenAI. Uses outputParserStructured, lmChatOpenAi, formTrigger, chainLlm. Event-driven trigger; 87 nodes.

Output Parser Structured, OpenAI Chat, Form Trigger +7
AI & RAG

This template attempts to replicate OpenAI's DeepResearch feature which, at time of writing, is only available to their pro subscribers.

Output Parser Structured, OpenAI Chat, Form Trigger +8
AI & RAG

My workflow 53. Uses formTrigger, httpRequest, lmChatOpenAi, form. Event-driven trigger; 74 nodes.

Form Trigger, HTTP Request, OpenAI Chat +15
AI & RAG

Content - Write Best Tools In Category Article. Uses formTrigger, httpRequest, slack, chainLlm. Event-driven trigger; 41 nodes.

Form Trigger, HTTP Request, Slack +6