This workflow corresponds to n8n.io template #18004 — we link there as the canonical source.
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 →
{
"name": "Air Pollution Analysis \u2014 Local AI",
"tags": [],
"nodes": [
{
"id": "5ea3a848-296f-4358-a655-c0d87bf70efb",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-848,
-272
],
"parameters": {
"width": 480,
"height": 768,
"content": "## Air Pollution Analysis \u2014 Local AI\n\n### How it works\n\nThis workflow exposes a webhook that triggers an air-pollution analysis request. It queries a large MotherDuck-backed dataset through the MCP endpoint, sends the retrieved context to a local Ollama model for analysis, formats the result, and returns it as the webhook response.\n\n### Setup steps\n\n- Configure the Webhook node with the desired path, HTTP method, and response mode suitable for your caller.\n- Set up the MotherDuck MCP HTTP request with the correct endpoint, authentication headers or tokens, and query payload for the air-pollution data.\n- Ensure Ollama is reachable at http://ollama:11434 from the n8n environment and that the requested model is installed and available.\n- Verify the Set node maps the Ollama response into the field expected by the Respond to Webhook node.\n\n### Customization\n\nAdjust the MotherDuck query to target different pollutants, time ranges, or locations, and update the Ollama prompt/model to change the style or depth of the analysis."
},
"typeVersion": 1
},
{
"id": "8ec3aea0-5a68-46e5-842b-9e1c6b714e17",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-288,
-272
],
"parameters": {
"color": 7,
"width": 400,
"height": 336,
"content": "## Receive and query data\n\nStarts from the webhook trigger and sends the incoming request into the MotherDuck MCP API query that retrieves the air-pollution dataset/results."
},
"typeVersion": 1
},
{
"id": "7306d643-73dc-4712-810f-da740c6820a2",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
144,
-272
],
"parameters": {
"color": 7,
"width": 240,
"height": 336,
"content": "## Run local AI analysis\n\nPasses the queried data to the local Ollama generate endpoint for analysis and interpretation."
},
"typeVersion": 1
},
{
"id": "ec5e89e5-bdc5-4b1a-9ddf-b9d66dc62153",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
416,
-256
],
"parameters": {
"color": 7,
"width": 368,
"height": 320,
"content": "## Format and return response\n\nShapes the Ollama output into a response field, then sends the final result back to the original webhook caller."
},
"typeVersion": 1
},
{
"id": "15366acb-4e47-471d-8945-73fcb90ef810",
"name": "Post to Ollama API",
"type": "n8n-nodes-base.httpRequest",
"position": [
192,
-96
],
"parameters": {
"url": "http://ollama:11434/api/generate",
"method": "POST",
"options": {},
"jsonBody": "={\n \"model\": \"qwen2.5:3b\",\n \"prompt\": \"You are an air quality data analyst. Here is PM10 pollution data from Seoul monitoring stations retrieved from a database of 3.8 million IoT sensor readings. Analyse this data and in 4 sentences identify the key findings, which stations are most concerning, and whether any readings suggest dangerous levels above WHO guidelines of 50 micrograms per cubic metre: Gangseo-gu avg=54.55 max=3586, Seocho-gu avg=53.84 max=1985, Guro-gu avg=51.5 max=1985, Yeongdeungpo-gu avg=50.55 max=985, Seongdong-gu avg=49.85 max=985, Mapo-gu avg=46.79 max=985, Songpa-gu avg=45.88 max=985, Seongbuk-gu avg=45.84 max=3403, Gwangjin-gu avg=45.78 max=1661, Gangdong-gu avg=45.32 max=622.\",\n \"stream\": false\n}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.4
},
{
"id": "1fb6fb75-e130-4cd1-a95c-52b1adac9c50",
"name": "Post to Motherduck API",
"type": "n8n-nodes-base.httpRequest",
"position": [
-32,
-96
],
"parameters": {
"url": "https://api.motherduck.com/mcp",
"method": "POST",
"options": {},
"jsonBody": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"query\",\n \"arguments\": {\n \"database\": \"air_pollution\",\n \"sql\": \"SELECT s.\\\"Station name(district)\\\", ROUND(AVG(m.\\\"Average value\\\"),2) as avg_pm10, ROUND(MAX(m.\\\"Average value\\\"),2) as max_pm10 FROM measurement_info m JOIN measurement_station_info s ON m.\\\"Station code\\\" = s.\\\"Station code\\\" WHERE m.\\\"Item code\\\" = 8 GROUP BY s.\\\"Station name(district)\\\" ORDER BY avg_pm10 DESC LIMIT 10\"\n }\n }\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json, text/event-stream"
},
{
"name": "MCP-Protocol-Version",
"value": "2024-11-05"
}
]
}
},
"typeVersion": 4.4
},
{
"id": "63ee4181-5bfd-42e5-a427-3527564b4c4a",
"name": "When Air Quality Requested",
"type": "n8n-nodes-base.webhook",
"position": [
-240,
-96
],
"parameters": {
"path": "air-quality",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 2.1
},
{
"id": "9a81ccbb-51b8-4e6a-ba7f-f1fb2267377d",
"name": "Send Webhook Response",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
640,
-96
],
"parameters": {
"options": {
"responseCode": "={{ 200 }}",
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html"
}
]
}
},
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html>\n<body style=\"font-family: Arial; background: #0D2B5E; color: white; padding: 40px;\">\n <h1 style=\"color: #00A8E8;\">AI-Enabled Air Quality Analysis</h1>\n <p style=\"color: #888;\">Seoul IoT Sensor Network \u2014 3,885,066 rows queried from MotherDuck</p>\n <hr style=\"border-color: #00A8E8;\">\n <h2 style=\"color: #00A8E8;\">Top 10 Most Polluted Stations (PM10)</h2>\n <table style=\"width:100%; border-collapse:collapse; margin-bottom:30px;\">\n <tr style=\"background:#1A4A9C;\">\n <th style=\"padding:10px; text-align:left;\">Station</th>\n <th style=\"padding:10px; text-align:center;\">Avg PM10</th>\n <th style=\"padding:10px; text-align:center;\">Max PM10</th>\n <th style=\"padding:10px; text-align:center;\">Status</th>\n </tr>\n <tr><td style=\"padding:8px;\">Gangseo-gu</td><td style=\"padding:8px; text-align:center;\">54.55</td><td style=\"padding:8px; text-align:center; color:#FF4444; font-weight:bold;\">3,586</td><td style=\"padding:8px; text-align:center; color:#FF4444;\">\u26a0 DANGEROUS</td></tr>\n <tr style=\"background:#0A1F45;\"><td style=\"padding:8px;\">Seocho-gu</td><td style=\"padding:8px; text-align:center;\">53.84</td><td style=\"padding:8px; text-align:center; color:#FF8800; font-weight:bold;\">1,985</td><td style=\"padding:8px; text-align:center; color:#FF8800;\">\u26a0 HIGH</td></tr>\n <tr><td style=\"padding:8px;\">Guro-gu</td><td style=\"padding:8px; text-align:center;\">51.50</td><td style=\"padding:8px; text-align:center; color:#FF8800; font-weight:bold;\">1,985</td><td style=\"padding:8px; text-align:center; color:#FF8800;\">\u26a0 HIGH</td></tr>\n <tr style=\"background:#0A1F45;\"><td style=\"padding:8px;\">Yeongdeungpo-gu</td><td style=\"padding:8px; text-align:center;\">50.55</td><td style=\"padding:8px; text-align:center; color:#FFCC00;\">985</td><td style=\"padding:8px; text-align:center; color:#FFCC00;\">\u25cf ELEVATED</td></tr>\n <tr><td style=\"padding:8px;\">Seongdong-gu</td><td style=\"padding:8px; text-align:center;\">49.85</td><td style=\"padding:8px; text-align:center; color:#FFCC00;\">985</td><td style=\"padding:8px; text-align:center; color:#FFCC00;\">\u25cf ELEVATED</td></tr>\n <tr style=\"background:#0A1F45;\"><td style=\"padding:8px;\">Mapo-gu</td><td style=\"padding:8px; text-align:center;\">46.79</td><td style=\"padding:8px; text-align:center;\">985</td><td style=\"padding:8px; text-align:center; color:#44CC44;\">\u2713 OK</td></tr>\n <tr><td style=\"padding:8px;\">Songpa-gu</td><td style=\"padding:8px; text-align:center;\">45.88</td><td style=\"padding:8px; text-align:center;\">985</td><td style=\"padding:8px; text-align:center; color:#44CC44;\">\u2713 OK</td></tr>\n <tr style=\"background:#0A1F45;\"><td style=\"padding:8px;\">Seongbuk-gu</td><td style=\"padding:8px; text-align:center;\">45.84</td><td style=\"padding:8px; text-align:center; color:#FF4444;\">3,403</td><td style=\"padding:8px; text-align:center; color:#FF8800;\">\u26a0 HIGH PEAK</td></tr>\n <tr><td style=\"padding:8px;\">Gwangjin-gu</td><td style=\"padding:8px; text-align:center;\">45.78</td><td style=\"padding:8px; text-align:center; color:#FF8800;\">1,661</td><td style=\"padding:8px; text-align:center; color:#44CC44;\">\u2713 OK</td></tr>\n <tr style=\"background:#0A1F45;\"><td style=\"padding:8px;\">Gangdong-gu</td><td style=\"padding:8px; text-align:center;\">45.32</td><td style=\"padding:8px; text-align:center;\">622</td><td style=\"padding:8px; text-align:center; color:#44CC44;\">\u2713 OK</td></tr>\n </table>\n <hr style=\"border-color: #00A8E8;\">\n <h2 style=\"color: #00A8E8;\">\ud83e\udd16 Local AI Analysis \u2014 Ollama qwen2.5:3b</h2>\n <div style=\"background:#0A1F45; padding:20px; border-left:4px solid #00A8E8; border-radius:4px;\">\n <p style=\"font-size:16px; line-height:1.8; margin:0;\">{{ $('Post to Ollama API').item.json.response }}</p>\n </div>\n <p style=\"color:#555; font-size:12px; margin-top:15px;\">\u2713 Local CPU only | \u2713 No data sent to cloud | \u2713 Ollama qwen2.5:3b | \u2713 MotherDuck 3,885,066 rows</p>\n</body>\n</html>"
},
"typeVersion": 1.5
},
{
"id": "be071f74-f38b-4530-8adb-ae6a8c0601ed",
"name": "Set Response Data",
"type": "n8n-nodes-base.set",
"position": [
464,
-96
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a3f1e445-a59f-4ac8-80de-d91d190ecdb8",
"name": "response",
"type": "string",
"value": "={{ $json.response }}"
}
]
}
},
"typeVersion": 3.4
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "fdab63fe-3706-4ba1-9e55-b6eb92ee676c",
"nodeGroups": [],
"connections": {
"Set Response Data": {
"main": [
[
{
"node": "Send Webhook Response",
"type": "main",
"index": 0
}
]
]
},
"Post to Ollama API": {
"main": [
[
{
"node": "Set Response Data",
"type": "main",
"index": 0
}
]
]
},
"Post to Motherduck API": {
"main": [
[
{
"node": "Post to Ollama API",
"type": "main",
"index": 0
}
]
]
},
"When Air Quality Requested": {
"main": [
[
{
"node": "Post to Motherduck API",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow exposes a webhook that queries PM10 air pollution data from a MotherDuck database, sends the results to a local Ollama (qwen2.5:3b) model for analysis, and returns an HTML report showing the top polluted Seoul districts and the AI-generated findings. Receives a…
Source: https://n8n.io/workflows/18004/ — 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.
Elevate your digital presence with high-fidelity cinematic video automation. This workflow orchestrates the complex, asynchronous rendering process of OpenAI Sora—transforming static product images or
Storage Sync - Google Drive. Uses supabase, googleDrive, httpRequest. Webhook trigger; 22 nodes.
This workflow synchronizes support tickets in Freshdesk with issues in Linear, enabling smooth collaboration between support and development teams. It triggers on new or updated Freshdesk tickets, map
This workflow receives new Calendly meeting bookings, checks Google Sheets to prevent duplicate briefings, pulls contact and deal context from HubSpot, enriches company data with Clearbit, generates t
This workflow automates the creation of user-generated-content-style product videos by combining Gemini's image generation with OpenAI's SORA 2 video generation. It accepts webhook requests with produ