DEV Community

Alex Kane
Alex Kane

Posted on

n8n for ClimateRisk/Physical Risk Analytics SaaS Vendors: 5 Automations for TCFD Physical Risk, SEC 33-11275, and FEMA NFIP (Free Workflow JSON)

n8n for ClimateRisk/Physical Risk Analytics SaaS Vendors: 5 Automations for TCFD Physical Risk, SEC 33-11275, and FEMA NFIP (Free Workflow JSON)

Customer tiers covered: LARGE_ASSET_MANAGER_SAAS / REAL_ESTATE_PHYSICAL_RISK_SAAS / INFRASTRUCTURE_CLIMATE_RISK_SAAS / BANK_CLIMATE_STRESS_TEST_SAAS / INSURANCE_CAT_RISK_SAAS / CORPORATE_TREASURY_CLIMATE_SAAS / CLIMATE_RISK_STARTUP_SAAS

Regulatory framework: TCFD Physical Risk Recommendations (2017) / SEC Rule 33-11275 (climate disclosure) / EU Taxonomy Regulation 2020/852 Art.10 (climate adaptation DNSH) / TNFD LEAP Framework / FEMA NFIP 44 CFR Part 61 / Swiss Re sigma natural catastrophe / ISSB IFRS S2 (2023)


Why this matters: Climate physical risk disclosure is no longer optional. The SEC Rule 33-11275 requires material physical risk quantification in annual filings. EU Taxonomy mandates DNSH climate adaptation assessment for sustainable finance labels. FEMA NFIP reclassifications create immediate insurance coverage gaps. ClimateRisk/Physical Risk Analytics SaaS vendors whose platforms process asset-level hazard data, TCFD scenario analysis, and disclosure timelines need automation infrastructure that doesn't route sensitive physical asset coordinates and regulatory deadlines through cloud iPaaS platforms — a SOC2 CC6.1 data egress finding in institutional security reviews.


Why n8n for Physical Risk Analytics SaaS

Physical risk data is sensitive: asset coordinates, flood zone classifications, FEMA map numbers, reinsurance pricing parameters. Routing this through Zapier or Make creates data egress exposure for enterprise customers who need to know exactly where their hazard data flows.

Self-hosted n8n keeps the entire TCFD scenario analysis pipeline — from raw hazard ingestion to SEC disclosure deadline tracking — inside your infrastructure. Git-versioned workflows are audit trail artifacts that answer due diligence questions from institutional investors and regulators. Distinct from Carbon/ESG/Sustainability workflows (emissions reporting, Scope 1-3, carbon credits) — this vertical is about physical asset exposure to acute and chronic climate hazards.


Workflow 1: TCFD Physical Risk Scenario Monitor

Polls your portfolio asset database daily, scores each asset against TCFD RCP 4.5/8.5 scenarios (flood probability, heat stress, wildfire hazard, sea level rise, tropical storm intensity), and routes CRITICAL and HIGH severity findings to Slack with SEC 33-11275 materiality flags and EU Taxonomy DNSH status.

Key logic: Composite physical risk score = flood (30%) + heat (20%) + wildfire (20%) + sea level rise (20%) + storm intensity (10%). Assets above $1M value with composite score >= 50 are flagged as SEC 33-11275 material. TNFD LEAP completeness and EU Taxonomy DNSH status appended per asset.

{
  "name": "TCFD Physical Risk Scenario Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        300
      ],
      "id": "schedule-1"
    },
    {
      "parameters": {
        "operation": "getAll",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.PORTFOLIO_ASSETS_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Assets"
        }
      },
      "name": "Get Portfolio Assets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        300
      ],
      "id": "sheets-1"
    },
    {
      "parameters": {
        "jsCode": "const assets = $input.all().map(i => i.json);\nconst now = new Date();\nconst alerts = [];\nfor (const asset of assets) {\n  const tier = asset.customer_tier || 'CLIMATE_RISK_STARTUP_SAAS';\n  const floodRisk = parseFloat(asset.flood_risk_score_rcp85 || 0);\n  const heatRisk = parseFloat(asset.heat_stress_score_rcp85 || 0);\n  const wildfireRisk = parseFloat(asset.wildfire_risk_score_rcp85 || 0);\n  const seaLevelRisk = parseFloat(asset.sea_level_rise_exposure_2050_m || 0);\n  const stormRisk = parseFloat(asset.tropical_storm_intensity_score || 0);\n  const assetValue = parseFloat(asset.asset_value_usd || 0);\n  const compositeScore = (floodRisk * 0.3) + (heatRisk * 0.2) + (wildfireRisk * 0.2) + (seaLevelRisk * 100 * 0.2) + (stormRisk * 0.1);\n  let severity = 'LOW';\n  let secDisclosureRequired = false;\n  if (compositeScore >= 75 || (assetValue >= 1000000 && compositeScore >= 50)) { severity = 'CRITICAL'; secDisclosureRequired = true; }\n  else if (compositeScore >= 50) { severity = 'HIGH'; }\n  else if (compositeScore >= 30) { severity = 'MEDIUM'; }\n  if (severity === 'CRITICAL' || severity === 'HIGH') {\n    const tnfdLeapStatus = asset.tnfd_leap_assess_complete === 'true' ? 'COMPLETE' : 'INCOMPLETE';\n    const euTaxonomyDnsh = asset.eu_taxonomy_dnsh_climate_adaptation === 'true' ? 'PASS' : 'FAIL';\n    alerts.push({ asset_id: asset.asset_id, asset_name: asset.asset_name, customer_tier: tier, composite_physical_risk_score: compositeScore.toFixed(1), flood_risk_rcp85: floodRisk, heat_stress_rcp85: heatRisk, wildfire_risk_rcp85: wildfireRisk, sea_level_rise_2050_m: seaLevelRisk, asset_value_usd: assetValue, severity, sec_33_11275_material: secDisclosureRequired, tnfd_leap_status: tnfdLeapStatus, eu_taxonomy_dnsh_climate_adaptation: euTaxonomyDnsh, fema_nfip_zone: asset.fema_flood_zone || 'UNKNOWN', swiss_re_nat_cat_region: asset.swiss_re_nat_cat_region || 'UNCLASSIFIED', assessed_at: now.toISOString() });\n  }\n}\nif (alerts.length === 0) return [{ json: { status: 'NO_HIGH_RISK_ASSETS', checked_at: now.toISOString() } }];\nreturn alerts.map(a => ({ json: a }));"
      },
      "name": "Score Physical Risk TCFD RCP8.5",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ],
      "id": "code-1"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "CRITICAL",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "name": "Is Critical?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "if-1"
    },
    {
      "parameters": {
        "webhookUri": "={{ $env.SLACK_CLIMATE_RISK_WEBHOOK }}",
        "text": ":rotating_light: *CRITICAL Physical Risk Alert*\nAsset: {{ $json.asset_id }} | Score: {{ $json.composite_physical_risk_score }}/100 (RCP 8.5)\nFlood: {{ $json.flood_risk_rcp85 }} | Heat: {{ $json.heat_stress_rcp85 }} | Wildfire: {{ $json.wildfire_risk_rcp85 }}\nSEC 33-11275 Material: {{ $json.sec_33_11275_material }} | TNFD LEAP: {{ $json.tnfd_leap_status }} | EU Taxonomy DNSH: {{ $json.eu_taxonomy_dnsh_climate_adaptation }}\nFEMA Zone: {{ $json.fema_nfip_zone }} | Swiss Re Region: {{ $json.swiss_re_nat_cat_region }}"
      },
      "name": "Slack climate-risk-ops",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        880,
        200
      ],
      "id": "slack-1"
    },
    {
      "parameters": {
        "operation": "append",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.CLIMATE_RISK_LOG_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "PhysicalRiskAlerts"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "asset_id": "={{ $json.asset_id }}",
            "severity": "={{ $json.severity }}",
            "composite_score": "={{ $json.composite_physical_risk_score }}",
            "sec_material": "={{ $json.sec_33_11275_material }}",
            "assessed_at": "={{ $json.assessed_at }}"
          }
        }
      },
      "name": "Log to Risk Register",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        880,
        400
      ],
      "id": "sheets-log-1"
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get Portfolio Assets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Portfolio Assets": {
      "main": [
        [
          {
            "node": "Score Physical Risk TCFD RCP8.5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Physical Risk TCFD RCP8.5": {
      "main": [
        [
          {
            "node": "Is Critical?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Critical?": {
      "main": [
        [
          {
            "node": "Slack climate-risk-ops",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log to Risk Register",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: SEC 33-11275 Physical Risk Disclosure Deadline Tracker

Runs weekdays at 8AM. Reads your disclosure deadline calendar — 10-K physical risk, 10-Q quarterly updates, EU Taxonomy DNSH assessments, TCFD annual reports, TNFD LEAP cycles, FEMA NFIP renewals, ISSB IFRS S2 filings. Classifies OVERDUE / CRITICAL (<=7d) / URGENT (<=21d) / WARNING (<=45d) / NOTICE (<=90d). Deduplicates alerts within 24h. Alerts Slack and emails compliance officer.

Why it matters: Missing the SEC 33-11275 annual physical risk disclosure triggers enforcement action AND shareholder litigation. Missing the EU Taxonomy DNSH assessment loses the sustainable finance label — EUR 20M or 4% global turnover penalty.

{
  "name": "SEC 33-11275 Physical Risk Disclosure Deadline Tracker",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "name": "Weekdays 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        300
      ],
      "id": "schedule-2"
    },
    {
      "parameters": {
        "operation": "getAll",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.COMPLIANCE_DEADLINES_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "SECDisclosureDeadlines"
        }
      },
      "name": "Get SEC Disclosure Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        300
      ],
      "id": "sheets-2"
    },
    {
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst now = new Date();\nconst results = [];\nconst DEADLINE_TYPES = {\n  'SEC_10K_CLIMATE_PHYSICAL_RISK': { label: 'SEC 10-K Climate Physical Risk Disclosure', citation: 'SEC Rule 33-11275 ss229.1500', penalty: 'SEC enforcement action + shareholder litigation' },\n  'SEC_10Q_QUARTERLY_CLIMATE_UPDATE': { label: 'SEC 10-Q Quarterly Climate Risk Update', citation: 'SEC Rule 33-11275 ss229.1501', penalty: 'Material misstatement risk' },\n  'EU_TAXONOMY_DNSH_CLIMATE_ADAPTATION': { label: 'EU Taxonomy DNSH Climate Adaptation Assessment', citation: 'EU Taxonomy Regulation 2020/852 Art.10', penalty: 'EUR 20M or 4% global turnover' },\n  'TCFD_ANNUAL_PHYSICAL_RISK_REPORT': { label: 'TCFD Annual Physical Risk Scenario Report', citation: 'TCFD 2017 Recommendations Physical Risk', penalty: 'ESG rating downgrade + institutional divestment' },\n  'TNFD_LEAP_ASSESSMENT_CYCLE': { label: 'TNFD LEAP Framework Assessment Cycle', citation: 'TNFD Beta v0.4 Locate/Evaluate/Assess/Prepare', penalty: 'Disclosure gap in sustainability report' },\n  'FEMA_NFIP_POLICY_RENEWAL': { label: 'FEMA NFIP Flood Insurance Policy Renewal', citation: '44 CFR Part 61 National Flood Insurance Program', penalty: 'Policy lapse uninsured flood loss' },\n  'ISSB_IFRS_S2_PHYSICAL_RISK': { label: 'ISSB IFRS S2 Physical Risk Disclosure', citation: 'IFRS S2 Climate-related Disclosures 2023', penalty: 'Non-compliance with ISSB-aligned jurisdiction' }\n};\nfor (const row of rows) {\n  const deadlineDate = new Date(row.deadline_date);\n  const lastAlert = row.alert_sent_date ? new Date(row.alert_sent_date) : null;\n  const daysUntil = Math.floor((deadlineDate - now) / 86400000);\n  const info = DEADLINE_TYPES[row.deadline_type] || { label: row.deadline_type, citation: 'N/A', penalty: 'Unknown' };\n  let urgency = null;\n  if (daysUntil < 0) urgency = 'OVERDUE';\n  else if (daysUntil <= 7) urgency = 'CRITICAL';\n  else if (daysUntil <= 21) urgency = 'URGENT';\n  else if (daysUntil <= 45) urgency = 'WARNING';\n  else if (daysUntil <= 90) urgency = 'NOTICE';\n  if (!urgency) continue;\n  if (lastAlert && (now - lastAlert) / 3600000 < 24) continue;\n  results.push({ customer_id: row.customer_id, customer_name: row.customer_name, customer_tier: row.customer_tier || 'CLIMATE_RISK_STARTUP_SAAS', deadline_type: row.deadline_type, deadline_label: info.label, regulatory_citation: info.citation, penalty_if_missed: info.penalty, deadline_date: row.deadline_date, days_until: daysUntil, urgency });\n}\nreturn results.length > 0 ? results.map(r => ({ json: r })) : [{ json: { status: 'NO_DEADLINES_DUE', checked_at: now.toISOString() } }];"
      },
      "name": "Classify Deadline Urgency",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ],
      "id": "code-2"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.urgency }}",
              "rightValue": "NO_DEADLINES_DUE",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "name": "Has Deadline?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "if-2"
    },
    {
      "parameters": {
        "webhookUri": "={{ $env.SLACK_COMPLIANCE_WEBHOOK }}",
        "text": ":calendar: *{{ $json.urgency }} \u2014 Climate Disclosure Deadline*\nCustomer: {{ $json.customer_name }} ({{ $json.customer_tier }})\nDeadline: {{ $json.deadline_label }}\nCitation: {{ $json.regulatory_citation }}\nDue: {{ $json.deadline_date }} \u2014 {{ $json.days_until }} days\nPenalty: {{ $json.penalty_if_missed }}"
      },
      "name": "Slack compliance-deadlines",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        880,
        200
      ],
      "id": "slack-2"
    },
    {
      "parameters": {
        "fromEmail": "={{ $env.CLIMATE_RISK_SENDER_EMAIL }}",
        "toEmail": "={{ $json.compliance_officer_email }}",
        "subject": "[{{ $json.urgency }}] {{ $json.deadline_label }} \u2014 {{ $json.days_until }} days",
        "emailType": "text",
        "message": "Deadline: {{ $json.deadline_label }}\nCitation: {{ $json.regulatory_citation }}\nDue: {{ $json.deadline_date }} ({{ $json.days_until }} days)\nPenalty: {{ $json.penalty_if_missed }}"
      },
      "name": "Email Compliance Officer",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        880,
        400
      ],
      "id": "email-2"
    }
  ],
  "connections": {
    "Weekdays 8AM": {
      "main": [
        [
          {
            "node": "Get SEC Disclosure Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get SEC Disclosure Deadlines": {
      "main": [
        [
          {
            "node": "Classify Deadline Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Deadline Urgency": {
      "main": [
        [
          {
            "node": "Has Deadline?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Deadline?": {
      "main": [
        [
          {
            "node": "Slack compliance-deadlines",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Compliance Officer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: FEMA NFIP Flood Zone Change Alert Pipeline

Polls every 6 hours. Detects FEMA flood zone reclassifications in your insured asset portfolio. Three alert types: REMAPPED_TO_HIGH_RISK_SFHA (critical — mandatory NFIP purchase within 12 months under 44 CFR ss61.7, lender force-placement risk on federally-backed mortgages), REMOVED_FROM_SFHA (informational — PRP eligibility), NFIP_COVERAGE_GAP_IN_SFHA (high — calculates gap between $250K NFIP max and actual asset value).

Regulatory trap: FEMA NFIP maximum coverage is $250K structure / $100K contents. An asset remapped to SFHA Zone AE with a $5M value has a $4.75M uninsured gap. Excess flood insurance must be placed within the 12-month mandatory purchase window.

{
  "name": "FEMA NFIP Flood Zone Change Alert Pipeline",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "name": "6h Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        300
      ],
      "id": "schedule-3"
    },
    {
      "parameters": {
        "operation": "getAll",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.INSURED_ASSETS_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "InsuredAssets"
        }
      },
      "name": "Get Insured Assets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        300
      ],
      "id": "sheets-3"
    },
    {
      "parameters": {
        "jsCode": "const assets = $input.all().map(i => i.json);\nconst results = [];\nconst SFHA_ZONES = ['A', 'AE', 'AH', 'AO', 'AR', 'A99', 'V', 'VE'];\nfor (const asset of assets) {\n  const currentZone = (asset.current_fema_zone || '').toUpperCase();\n  const previousZone = (asset.previous_fema_zone || '').toUpperCase();\n  if (!currentZone || currentZone === previousZone) continue;\n  const wasHighRisk = SFHA_ZONES.includes(previousZone);\n  const isHighRisk = SFHA_ZONES.includes(currentZone);\n  const nfipCoverage = parseFloat(asset.nfip_coverage_usd || 0);\n  const assetValue = parseFloat(asset.asset_value_usd || 0);\n  let changeType = 'ZONE_CHANGE';\n  let severity = 'MEDIUM';\n  let action_required = '';\n  if (!wasHighRisk && isHighRisk) {\n    changeType = 'REMAPPED_TO_HIGH_RISK_SFHA';\n    severity = 'CRITICAL';\n    action_required = 'Mandatory NFIP purchase within 12 months per 44 CFR ss61.7. Lender force-placement risk on federally-backed mortgages.';\n  } else if (wasHighRisk && !isHighRisk) {\n    changeType = 'REMOVED_FROM_SFHA';\n    severity = 'INFO';\n    action_required = 'Review NFIP policy -- may qualify for Preferred Risk Policy.';\n  } else if (isHighRisk && nfipCoverage < assetValue * 0.8) {\n    changeType = 'NFIP_COVERAGE_GAP_IN_SFHA';\n    severity = 'HIGH';\n    action_required = 'NFIP max $250K structure / $100K contents. Evaluate excess flood insurance for gap of $' + (assetValue - nfipCoverage).toFixed(0) + '.';\n  }\n  results.push({ asset_id: asset.asset_id, asset_name: asset.asset_name, previous_fema_zone: previousZone, current_fema_zone: currentZone, change_type: changeType, severity, action_required, nfip_coverage_usd: nfipCoverage, asset_value_usd: assetValue, fema_map_effective_date: asset.fema_map_effective_date, detected_at: new Date().toISOString() });\n}\nreturn results.length > 0 ? results.map(r => ({ json: r })) : [{ json: { status: 'NO_ZONE_CHANGES', checked_at: new Date().toISOString() } }];"
      },
      "name": "Detect FEMA Zone Changes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ],
      "id": "code-3"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "INFO",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            },
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "NO_ZONE_CHANGES",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "name": "Actionable Change?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "if-3"
    },
    {
      "parameters": {
        "webhookUri": "={{ $env.SLACK_NFIP_WEBHOOK }}",
        "text": ":flood: *{{ $json.severity }} -- FEMA Flood Zone Reclassification*\nAsset: {{ $json.asset_id }} -- {{ $json.asset_name }}\nZone Change: {{ $json.previous_fema_zone }} to {{ $json.current_fema_zone }}\nChange Type: {{ $json.change_type }}\nAction: {{ $json.action_required }}\nNFIP Coverage: ${{ $json.nfip_coverage_usd }} | Asset Value: ${{ $json.asset_value_usd }}"
      },
      "name": "Slack nfip-alerts",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        880,
        200
      ],
      "id": "slack-3"
    },
    {
      "parameters": {
        "operation": "append",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.CLIMATE_RISK_LOG_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "FEMAZoneChanges"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "asset_id": "={{ $json.asset_id }}",
            "change_type": "={{ $json.change_type }}",
            "prev_zone": "={{ $json.previous_fema_zone }}",
            "new_zone": "={{ $json.current_fema_zone }}",
            "severity": "={{ $json.severity }}",
            "detected_at": "={{ $json.detected_at }}"
          }
        }
      },
      "name": "Log Zone Change",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        880,
        400
      ],
      "id": "sheets-log-3"
    }
  ],
  "connections": {
    "6h Schedule": {
      "main": [
        [
          {
            "node": "Get Insured Assets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Insured Assets": {
      "main": [
        [
          {
            "node": "Detect FEMA Zone Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect FEMA Zone Changes": {
      "main": [
        [
          {
            "node": "Actionable Change?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Actionable Change?": {
      "main": [
        [
          {
            "node": "Slack nfip-alerts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Zone Change",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: Physical Asset Climate Risk Scoring Pipeline

Webhook-triggered scoring API. Ingests raw hazard data (flood annual probability, heat extreme days to 2050, wildfire hazard index, tropical cyclone track density, sea level rise meters, average temperature increase, precipitation change %, drought SPEI index change). Computes acute (60% weight) and chronic (40% weight) sub-scores. Returns composite physical risk score, SEC 33-11275 materiality flag, EU Taxonomy DNSH pass/fail, and TNFD LEAP scope recommendation.

Tier-aware materiality: SEC 33-11275 materiality threshold varies by customer tier — large asset managers flag at $500K, infrastructure platforms at $5M. Configurable per-tier thresholds prevent false positives for startup customers.

{
  "name": "Physical Asset Climate Risk Scoring Pipeline",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "climate-risk-score-asset",
        "responseMode": "responseNode"
      },
      "name": "Webhook Score Asset",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "id": "webhook-4"
    },
    {
      "parameters": {
        "jsCode": "const asset = $input.first().json.body || $input.first().json;\nconst now = new Date();\nconst floodAnnualProbability = parseFloat(asset.flood_1_in_100_year_probability || 0);\nconst heatExtremeDays = parseInt(asset.heat_days_above_35c_2050_rcp85 || 0);\nconst wildfireHazardScore = parseFloat(asset.wildfire_hazard_index || 0);\nconst tropicalCycloneTrack = parseFloat(asset.tropical_cyclone_track_density_rcp85 || 0);\nconst seaLevelRiseMeter = parseFloat(asset.sea_level_rise_2050_m || 0);\nconst averageTempIncrease = parseFloat(asset.avg_temp_increase_c_2050_rcp85 || 0);\nconst precipitationChangePercent = parseFloat(asset.precipitation_change_pct_2050 || 0);\nconst droughtIndexChange = parseFloat(asset.drought_spei_change_2050 || 0);\nconst acuteScore = Math.min(100, (floodAnnualProbability * 100 * 0.25) + (Math.min(heatExtremeDays, 180) / 180 * 100 * 0.20) + (wildfireHazardScore * 0.20) + (tropicalCycloneTrack * 0.15) + (Math.min(seaLevelRiseMeter, 2) / 2 * 100 * 0.20));\nconst chronicScore = Math.min(100, (Math.min(averageTempIncrease, 4) / 4 * 100 * 0.40) + (Math.abs(precipitationChangePercent) / 50 * 100 * 0.30) + (Math.abs(droughtIndexChange) / 3 * 100 * 0.30));\nconst compositeScore = (acuteScore * 0.6) + (chronicScore * 0.4);\nconst customerTier = asset.customer_tier || 'CLIMATE_RISK_STARTUP_SAAS';\nconst assetValue = parseFloat(asset.asset_value_usd || 0);\nconst SEC_THRESHOLDS = { 'LARGE_ASSET_MANAGER_SAAS': 500000, 'REAL_ESTATE_PHYSICAL_RISK_SAAS': 1000000, 'BANK_CLIMATE_STRESS_TEST_SAAS': 2000000, 'INSURANCE_CAT_RISK_SAAS': 1000000, 'CORPORATE_TREASURY_CLIMATE_SAAS': 1500000, 'INFRASTRUCTURE_CLIMATE_RISK_SAAS': 5000000, 'CLIMATE_RISK_STARTUP_SAAS': 250000 };\nconst secMaterialityFlag = assetValue >= (SEC_THRESHOLDS[customerTier] || 1000000) && compositeScore >= 50;\nconst dnshPass = compositeScore < 65 || asset.adaptation_measures_in_place === 'true';\nreturn [{ json: { asset_id: asset.asset_id, asset_name: asset.asset_name, customer_tier: customerTier, composite_physical_risk_score: compositeScore.toFixed(1), acute_hazard_score: acuteScore.toFixed(1), chronic_hazard_score: chronicScore.toFixed(1), sec_33_11275_material: secMaterialityFlag, eu_taxonomy_dnsh_climate_adaptation: dnshPass ? 'PASS' : 'FAIL', tnfd_leap_scope: compositeScore >= 50 ? 'ASSESS_REQUIRED' : 'EVALUATE_SUFFICIENT', scored_at: now.toISOString(), scenario: 'TCFD_RCP85_2050' } }];"
      },
      "name": "Score Multi-Hazard Risk",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        300
      ],
      "id": "code-4"
    },
    {
      "parameters": {
        "operation": "append",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.CLIMATE_RISK_LOG_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "RiskScores"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "asset_id": "={{ $json.asset_id }}",
            "composite_score": "={{ $json.composite_physical_risk_score }}",
            "sec_material": "={{ $json.sec_33_11275_material }}",
            "eu_taxonomy_dnsh": "={{ $json.eu_taxonomy_dnsh_climate_adaptation }}",
            "scored_at": "={{ $json.scored_at }}"
          }
        }
      },
      "name": "Persist Risk Score",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        440,
        300
      ],
      "id": "sheets-4"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}"
      },
      "name": "Return Score",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        660,
        300
      ],
      "id": "respond-4"
    }
  ],
  "connections": {
    "Webhook Score Asset": {
      "main": [
        [
          {
            "node": "Score Multi-Hazard Risk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Multi-Hazard Risk": {
      "main": [
        [
          {
            "node": "Persist Risk Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Persist Risk Score": {
      "main": [
        [
          {
            "node": "Return Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly Climate Risk Platform KPI Dashboard

Monday 8AM. Pulls platform metrics (MRR, active customers, trial conversions) and this week's risk assessment results (total assessments run, critical assets >= 75 score, SEC 33-11275 material flags, EU Taxonomy DNSH fails). Builds HTML KPI table. Emails CEO with Chief Risk Officer on BCC.

Governance note: BCC to Chief Risk Officer closes the SEC 33-11275 disclosure governance chain — your risk officers see the same physical risk data as executive leadership in a documented, timestamped audit trail.

{
  "name": "Weekly Climate Risk Platform KPI Dashboard",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        300
      ],
      "id": "schedule-5"
    },
    {
      "parameters": {
        "operation": "getAll",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.PLATFORM_METRICS_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "WeeklyMetrics"
        }
      },
      "name": "Get Platform Metrics",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        200
      ],
      "id": "sheets-5a"
    },
    {
      "parameters": {
        "operation": "getAll",
        "resource": "row",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.CLIMATE_RISK_LOG_SHEET_ID }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "RiskScores"
        }
      },
      "name": "Get This Weeks Risk Scores",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        400
      ],
      "id": "sheets-5b"
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      },
      "name": "Merge Metrics",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        440,
        300
      ],
      "id": "merge-5"
    },
    {
      "parameters": {
        "jsCode": "const platformRows = $input.all().filter(i => i.json.mrr_usd !== undefined).map(i => i.json);\nconst riskRows = $input.all().filter(i => i.json.composite_score !== undefined).map(i => i.json);\nconst now = new Date();\nconst thisWeek = platformRows[0] || {};\nconst lastWeek = platformRows[1] || {};\nconst mrrThisWeek = parseFloat(thisWeek.mrr_usd || 0);\nconst mrrLastWeek = parseFloat(lastWeek.mrr_usd || 0);\nconst mrrWoW = mrrLastWeek > 0 ? (((mrrThisWeek - mrrLastWeek) / mrrLastWeek) * 100).toFixed(1) + '%' : 'N/A';\nconst mrrFlag = mrrLastWeek > 0 && mrrThisWeek < mrrLastWeek * 0.95 ? 'WARNING ' : '';\nconst riskAssessmentsRun = riskRows.length;\nconst criticalAssets = riskRows.filter(r => parseFloat(r.composite_score || 0) >= 75).length;\nconst secMaterialFlags = riskRows.filter(r => r.sec_material === 'TRUE').length;\nconst euTaxonomyFails = riskRows.filter(r => r.eu_taxonomy_dnsh === 'FAIL').length;\nconst html = '<h2>FlowKit Climate Risk Platform Weekly KPI</h2><p>Week of: ' + now.toISOString().split('T')[0] + '</p><table border=\"1\" cellpadding=\"6\" style=\"border-collapse:collapse\"><tr><th>Metric</th><th>Value</th><th>WoW</th></tr><tr><td>MRR</td><td>$' + mrrThisWeek.toLocaleString() + '</td><td>' + mrrFlag + mrrWoW + '</td></tr><tr><td>Active Customers</td><td>' + (thisWeek.active_customers || 0) + '</td><td>--</td></tr><tr><td>Risk Assessments Run</td><td>' + riskAssessmentsRun + '</td><td>--</td></tr><tr><td>Critical Assets (Score 75+)</td><td>' + criticalAssets + '</td><td>--</td></tr><tr><td>SEC 33-11275 Material Flags</td><td>' + secMaterialFlags + '</td><td>--</td></tr><tr><td>EU Taxonomy DNSH Fails</td><td>' + euTaxonomyFails + '</td><td>--</td></tr></table><p>BCC to Chief Risk Officer closes SEC 33-11275 disclosure governance chain.</p>';\nreturn [{ json: { subject: 'FlowKit Climate Risk Weekly KPI ' + now.toISOString().split('T')[0], html, slack_summary: 'Climate Risk KPI: MRR $' + mrrThisWeek.toLocaleString() + ' (' + mrrFlag + mrrWoW + ' WoW) | ' + riskAssessmentsRun + ' assessments | ' + criticalAssets + ' critical | ' + secMaterialFlags + ' SEC material flags' } }];"
      },
      "name": "Build KPI Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "code-5"
    },
    {
      "parameters": {
        "fromEmail": "={{ $env.CLIMATE_RISK_SENDER_EMAIL }}",
        "toEmail": "={{ $env.CEO_EMAIL }}",
        "bccEmail": "={{ $env.CHIEF_RISK_OFFICER_EMAIL }}",
        "subject": "={{ $json.subject }}",
        "emailType": "html",
        "message": "={{ $json.html }}"
      },
      "name": "Email CEO plus CRO BCC",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        880,
        200
      ],
      "id": "email-5"
    },
    {
      "parameters": {
        "webhookUri": "={{ $env.SLACK_MANAGEMENT_WEBHOOK }}",
        "text": "={{ $json.slack_summary }}"
      },
      "name": "Slack management",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        880,
        400
      ],
      "id": "slack-5"
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "Get Platform Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get This Weeks Risk Scores",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Platform Metrics": {
      "main": [
        [
          {
            "node": "Merge Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get This Weeks Risk Scores": {
      "main": [
        [
          {
            "node": "Merge Metrics",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Metrics": {
      "main": [
        [
          {
            "node": "Build KPI Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI Report": {
      "main": [
        [
          {
            "node": "Email CEO plus CRO BCC",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack management",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

n8n vs Zapier/Make for Physical Risk Analytics

Feature n8n (self-hosted) Zapier/Make
Asset coordinate data residency Your VPC US/EU cloud
FEMA flood zone data sovereignty In-VPC Vendor-controlled
SEC 33-11275 audit trail Git-versioned JSON Vendor logs
EU Taxonomy DNSH workflow proof Code node + git Black-box SaaS
Custom TCFD RCP 4.5/8.5 scoring logic Code node (arbitrary) Zap steps (limited)
Institutional security review SOC2 CC6.1 clean Data egress finding

Store

All 5 workflows above are free to copy. Production-ready versions with error handling, Postgres persistence, multi-asset batch processing, and pre-built TCFD/TNFD/SEC integration connectors are available at stripeai.gumroad.com.


These workflows are illustrative. Regulatory deadlines and penalty figures are based on published rules as of 2024-2025 — verify current requirements with qualified legal/compliance counsel.

Top comments (0)