{
  "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 &nbsp;|&nbsp; \u2713 No data sent to cloud &nbsp;|&nbsp; \u2713 Ollama qwen2.5:3b &nbsp;|&nbsp; \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
          }
        ]
      ]
    }
  }
}