This workflow corresponds to n8n.io template #17948 — 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 →
{
"id": "4FDgfnxGjKzZtTzQ",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Migratory Pest Wind-Vector Tracker",
"tags": [],
"nodes": [
{
"id": "57aa1e0d-18df-47be-9372-51822c8a4e69",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 480,
"height": 720,
"content": "## Migratory Pest Wind-Vector Tracker\n### How it works\n\n1. Automatically triggers every day at 6 AM with preset farm configuration.\n2. Queries the GBIF API to find pest occurrences near the farm location.\n3. Retrieves weather and wind vector data to calculate wind alignment threat.\n4. Uses an AI agent with Google Gemini to draft a scout alert if conditions are met.\n5. Notifies field scouts via Slack with the generated alert.\n\n### Setup steps\n\n- - [ ] Configure farm coordinates, radius, and lookback days in the Config set node.\n- - [ ] Set up credentials for the Google Gemini chat model used by the AI agent.\n- - [ ] Configure Slack credentials and destination channel for the field scout alerts.\n\n### Customization\n\nAdjust farm coordinates and search radius in the Config node or modify the lookback days and wind thresholds to suit specific crop requirements."
},
"typeVersion": 1
},
{
"id": "d6ff980f-7247-4b7b-9802-ec7cfb137634",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
0
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Trigger and configuration setup\n\nTriggers the workflow daily at 6 AM and initializes the farm location and search radius configuration."
},
"typeVersion": 1
},
{
"id": "dd8731e2-1fea-497c-ac55-f935dea46ec2",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1008,
32
],
"parameters": {
"color": 7,
"width": 416,
"height": 304,
"content": "## Search and filter pest occurrences\n\nSearches GBIF for recent pest occurrences and checks if any pests were reported nearby."
},
"typeVersion": 1
},
{
"id": "d00e489b-105b-4093-8e0d-97c858ca97d3",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1456,
48
],
"parameters": {
"color": 7,
"width": 640,
"height": 304,
"content": "## Fetch weather and calculate threat\n\nFetches wind data, calculates wind alignment vector threat, and checks if wind is blowing toward the farm."
},
"typeVersion": 1
},
{
"id": "7c04bbc6-b3ee-4abf-b53a-b09780036f38",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2128,
64
],
"parameters": {
"color": 7,
"width": 576,
"height": 496,
"content": "## Generate AI alert and notify Slack\n\nDrafts a scout alert using an AI agent powered by Gemini and sends the notification to Slack."
},
"typeVersion": 1
},
{
"id": "11877398-8379-4c41-a915-e141e913a5b8",
"name": "Every Morning at 6am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
608,
160
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 6 * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "fc2ea7f0-d333-4776-af24-91ffdae22dd4",
"name": "Set Search Parameters",
"type": "n8n-nodes-base.set",
"position": [
832,
160
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "10e70c86-160e-40e0-badf-b2a09b6529dc",
"name": "farm_latitude",
"type": "string",
"value": "enter-your-lat-here-and-change-string-to-number"
},
{
"id": "85d27b39-5817-4fda-b32a-e41331526b0e",
"name": "farm_longitude",
"type": "string",
"value": "enter-your-long-here-and-change-string-to-number"
},
{
"id": "294f2973-9e61-4c88-a28f-6519d4632eb9",
"name": "radius_km",
"type": "number",
"value": 100
},
{
"id": "9315e0d1-0243-4805-a799-b4a6557eacbf",
"name": "lookback_days",
"type": "number",
"value": 2
},
{
"id": "da24085b-8cba-42bc-a920-e7023c963624",
"name": "wind_lookback_days",
"type": "number",
"value": 2
},
{
"id": "e8c4e0b2-5ba5-4307-ac66-9ac398f77a57",
"name": "alignment_tolerance_deg",
"type": "number",
"value": 45
},
{
"id": "07a47498-0366-47c5-87e5-578f5cf7fc18",
"name": "slack_channel",
"type": "string",
"value": "#general [edit this to your preferred slack channel name]"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "9855253c-4efe-458f-8d39-0203274e041e",
"name": "Fetch GBIF Pest Occurrences",
"type": "n8n-nodes-base.httpRequest",
"position": [
1056,
160
],
"parameters": {
"url": "https://api.gbif.org/v1/occurrence/search",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "geoDistance",
"value": "={{ $json.farm_latitude }},{{ $json.farm_longitude }},{{ $json.radius_km }}km"
},
{
"name": "establishmentMeans",
"value": "INTRODUCED"
},
{
"name": "establishmentMeans",
"value": "INVASIVE"
},
{
"name": "eventDate",
"value": "={{ $now.minus({ days: $json.lookback_days }).toFormat('yyyy-MM-dd') }},{{ $now.toFormat('yyyy-MM-dd') }}"
},
{
"name": "hasCoordinate",
"value": "true"
},
{
"name": "limit",
"value": "50"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "e56644e3-9509-4db3-a10e-11a6c00f73e0",
"name": "Check for Nearby Pests",
"type": "n8n-nodes-base.if",
"position": [
1280,
160
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "12dcef8c-a100-4a27-9554-2dfb094d1cc6",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.count }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "49acd782-7bba-4db6-bd44-e67c8237d820",
"name": "Fetch Wind Data",
"type": "n8n-nodes-base.httpRequest",
"position": [
1504,
176
],
"parameters": {
"url": "https://api.open-meteo.com/v1/forecast",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "latitude",
"value": "={{ $('Set Search Parameters').first().json.farm_latitude }}"
},
{
"name": "longitude",
"value": "={{ $('Set Search Parameters').first().json.farm_longitude }}"
},
{
"name": "hourly",
"value": "wind_speed_10m,wind_direction_10m"
},
{
"name": "past_days",
"value": "={{ $('Set Search Parameters').first().json.wind_lookback_days }}"
},
{
"name": "forecast_days",
"value": "1"
},
{
"name": "timezone",
"value": "auto"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "60ef602b-6286-41da-b60a-35f4e66d1104",
"name": "Calculate Wind Threat Alignment",
"type": "n8n-nodes-base.code",
"position": [
1728,
176
],
"parameters": {
"jsCode": "// Wind-Vector Threat Calculator\n// Compares each GBIF pest occurrence against the 48h wind history\n// and flags occurrences the wind is actively carrying toward the farm.\n\nconst cfg = $('Set Search Parameters').first().json;\nconst farmLat = parseFloat(cfg.farm_latitude);\nconst farmLon = parseFloat(cfg.farm_longitude);\nconst toleranceDeg = parseFloat(cfg.alignment_tolerance_deg);\n\nconst occurrences = $input.first().json.results || [];\n\n// --- wind history comes from the previous node (Open-Meteo) ---\nconst wind = $('Fetch Wind Data').first().json.hourly;\nconst speeds = wind.wind_speed_10m || [];\nconst dirs = wind.wind_direction_10m || [];\n\n// Circular mean of wind direction over the lookback window\nlet sinSum = 0, cosSum = 0;\nfor (const d of dirs) {\n const rad = (d * Math.PI) / 180;\n sinSum += Math.sin(rad);\n cosSum += Math.cos(rad);\n}\nlet avgWindFrom = (Math.atan2(sinSum, cosSum) * 180) / Math.PI;\nif (avgWindFrom < 0) avgWindFrom += 360;\nconst avgWindSpeed = speeds.reduce((a, b) => a + b, 0) / (speeds.length || 1);\n\n// Direction the wind is actually blowing TOWARD (opposite of \"from\")\nconst windBlowingToward = (avgWindFrom + 180) % 360;\n\nfunction toRad(d) { return (d * Math.PI) / 180; }\nfunction toDeg(r) { return (r * 180) / Math.PI; }\n\n// Great-circle bearing from point A to point B (degrees, 0-360)\nfunction bearing(lat1, lon1, lat2, lon2) {\n const y = Math.sin(toRad(lon2 - lon1)) * Math.cos(toRad(lat2));\n const x =\n Math.cos(toRad(lat1)) * Math.sin(toRad(lat2)) -\n Math.sin(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.cos(toRad(lon2 - lon1));\n let brng = toDeg(Math.atan2(y, x));\n return (brng + 360) % 360;\n}\n\n// Haversine distance in km\nfunction distanceKm(lat1, lon1, lat2, lon2) {\n const R = 6371;\n const dLat = toRad(lat2 - lat1);\n const dLon = toRad(lon2 - lon1);\n const a =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n\nfunction angularDiff(a, b) {\n const diff = Math.abs(a - b) % 360;\n return diff > 180 ? 360 - diff : diff;\n}\n\nfunction compass(deg) {\n const points = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];\n return points[Math.round(deg / 45) % 8];\n}\n\nconst threats = [];\nfor (const occ of occurrences) {\n const lat = occ.decimalLatitude;\n const lon = occ.decimalLongitude;\n if (lat === undefined || lon === undefined) continue;\n\n const distance = distanceKm(farmLat, farmLon, lat, lon);\n const bearingToFarm = bearing(lat, lon, farmLat, farmLon);\n const diff = angularDiff(bearingToFarm, windBlowingToward);\n const windAligned = diff <= toleranceDeg;\n\n threats.push({\n species: occ.species || occ.scientificName || 'Unknown species',\n establishmentMeans: occ.establishmentMeans || 'UNKNOWN',\n locality: occ.locality || occ.stateProvince || occ.country || 'Unknown locality',\n eventDate: occ.eventDate || occ.year || 'Unknown date',\n distance_km: Math.round(distance * 10) / 10,\n bearing_to_farm_deg: Math.round(bearingToFarm),\n wind_alignment_diff_deg: Math.round(diff),\n wind_aligned: windAligned,\n upwind_edge_to_inspect: compass(avgWindFrom),\n });\n}\n\nconst alignedThreats = threats\n .filter((t) => t.wind_aligned)\n .sort((a, b) => a.distance_km - b.distance_km);\n\nreturn [\n {\n json: {\n farm_latitude: farmLat,\n farm_longitude: farmLon,\n avg_wind_from_deg: Math.round(avgWindFrom),\n avg_wind_toward_deg: Math.round(windBlowingToward),\n avg_wind_speed_kmh: Math.round(avgWindSpeed * 10) / 10,\n total_occurrences_checked: occurrences.length,\n aligned_threat_count: alignedThreats.length,\n aligned_threats: alignedThreats,\n all_threats: threats,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "abe9b906-0649-4d7b-9f83-9657e81ab62c",
"name": "Check Wind Alignment Threat",
"type": "n8n-nodes-base.if",
"position": [
1952,
176
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "35dd836e-9fe7-4b33-91ce-ff2cb5e37d56",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.aligned_threat_count }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "90813279-95d2-44e2-9f0a-2b9d985e820d",
"name": "Scout Alert Generator Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
2208,
192
],
"parameters": {
"text": "=You are an agricultural biosecurity analyst writing an URGENT field alert for crop scouts.\n\nWind-vector threat data (JSON):\n{{ JSON.stringify($json, null, 2) }}\n\nWrite a Slack message for the field scout channel. Rules:\n- Only reference occurrences in \"aligned_threats\" \u2014 these are the ones the wind is actively carrying toward the farm.\n- Assign a threat level per occurrence: CRITICAL if distance_km < 25 and avg_wind_speed_kmh > 15; HIGH if distance_km < 50; MEDIUM otherwise.\n- Open with a one-line summary (count of threats, worst threat level).\n- For each aligned threat, give: species name, distance from farm, which compass edge to inspect first (upwind_edge_to_inspect), and the threat level.\n- Close with a single clear action instruction: prioritize inspecting the named upwind edge(s) in the next scouting pass.\n- Use Slack markdown (bold with *asterisks*, bullet points with -). Keep it under 150 words. No preamble, no sign-off \u2014 output ONLY the message text.",
"options": {
"systemMessage": "You are a precise, no-nonsense agricultural biosecurity analyst. You never invent data that isn't in the input JSON."
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "909fd66b-6cca-48b0-b850-9a63d3117edb",
"name": "Gemini LLM Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
2176,
400
],
"parameters": {
"options": {},
"modelName": "models/gemini-3.1-flash-lite"
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "63a28ac0-07e4-48cc-b5ec-c37bd1f8d68e",
"name": "Send Slack Alert to Scouts",
"type": "n8n-nodes-base.slack",
"position": [
2560,
192
],
"parameters": {
"text": "={{ $json.output }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Search Parameters').first().json.slack_channel }}"
},
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "aa7c9c87-b543-435b-beff-c883a5ec03da",
"nodeGroups": [],
"connections": {
"Fetch Wind Data": {
"main": [
[
{
"node": "Calculate Wind Threat Alignment",
"type": "main",
"index": 0
}
]
]
},
"Gemini LLM Model": {
"ai_languageModel": [
[
{
"node": "Scout Alert Generator Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Every Morning at 6am": {
"main": [
[
{
"node": "Set Search Parameters",
"type": "main",
"index": 0
}
]
]
},
"Set Search Parameters": {
"main": [
[
{
"node": "Fetch GBIF Pest Occurrences",
"type": "main",
"index": 0
}
]
]
},
"Check for Nearby Pests": {
"main": [
[],
[
{
"node": "Fetch Wind Data",
"type": "main",
"index": 0
}
]
]
},
"Check Wind Alignment Threat": {
"main": [
[],
[
{
"node": "Scout Alert Generator Agent",
"type": "main",
"index": 0
}
]
]
},
"Fetch GBIF Pest Occurrences": {
"main": [
[
{
"node": "Check for Nearby Pests",
"type": "main",
"index": 0
}
]
]
},
"Scout Alert Generator Agent": {
"main": [
[
{
"node": "Send Slack Alert to Scouts",
"type": "main",
"index": 0
}
]
]
},
"Calculate Wind Threat Alignment": {
"main": [
[
{
"node": "Check Wind Alignment Threat",
"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.
googlePalmApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Workflow Link: https://youtu.be/3ExkAWTyEXU?si=R7bAUzHqf1_LpHnT
Source: https://n8n.io/workflows/17948/ — 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 is the AI analysis and alerting engine for a complete social media monitoring system. It's designed to work with data scraped from X (formerly Twitter) using a tool like the Apify Tweet
This workflow is designed for Japanese-speaking professionals, and learners who want to efficiently stay up to date with practical productivity, lifehack, and efficiency-related insights from Japanese
Video Explanation: https://youtu.be/5TNw4aaHtHI?si=wLY3Gc_i9n3vKYNQ
This workflow collects end-of-day updates from team members in Slack, stores them in Google Sheets, and posts a concise executive summary back to Slack using Google Gemini. Runs daily at 3pm, reads th
Business Post Pipeline. Uses postgres, httpRequest, agent, lmChatGoogleGemini. Scheduled trigger; 30 nodes.