DEV Community

Alex Kane
Alex Kane

Posted on

n8n for Carbon/ESG/Sustainability SaaS Vendors: 5 Automations for SEC Climate Disclosure, EU CSRD, and GHG Protocol Compliance (Free Workflow JSON)

If your ESG or sustainability SaaS platform helps companies track emissions, report under EU CSRD, or satisfy SEC climate disclosure requirements, your operational workflows are under the same deadline pressure as your customers' compliance programs.

This guide shows 5 production-ready n8n workflows built specifically for Carbon/ESG/Sustainability SaaS vendors — the engineering and ops teams at platforms like carbon accounting tools, CSRD reporting software, Scope 3 data collectors, and ESG analytics platforms.

Why self-hosted n8n for ESG SaaS? Your platform processes emissions data, financial climate risk disclosures, and supply chain GHG inventories. Routing that data through Zapier or Make.com means it flows through external cloud infrastructure — a data governance finding under EU CSRD Art.6 ESRS 1 due diligence, a GHG Protocol third-party data custody question, and a red flag on any CDP supply chain questionnaire. Self-hosted n8n keeps the entire automation stack inside your compliance boundary.


Customer tier segmentation

Before the workflows: all 5 are tier-aware. Configure the tier field in your trigger data to one of:

Tier Customer Type
ENTERPRISE_ESG_PLATFORM_SAAS Full ESG/sustainability SaaS platforms (e.g., CSRD reporting, Scope 1/2/3 management)
CARBON_ACCOUNTING_SAAS Carbon accounting and footprint calculation tools
SUPPLY_CHAIN_SUSTAINABILITY_SAAS Scope 3 supply chain emissions trackers
GHG_EMISSIONS_SAAS GHG Protocol-aligned emissions measurement platforms
ESG_REPORTING_SAAS ESG/TCFD/GRI/SASB reporting and disclosure platforms
IMPACT_INVESTING_SAAS Impact measurement platforms (UNPRI, SFDR, GIIN IRIS+)
SUSTAINABILITY_STARTUP_SAAS Early-stage sustainability tech startups

Workflow 1: Climate Disclosure Deadline Tracker

Tracks your customers' regulatory filing deadlines across 14 frameworks: SEC Rule 33-11275, EU CSRD ESRS E1 double materiality, California SB 253/261, EU ETS annual surrender (April 30), CDP October window, TCFD, ISSB IFRS S1/S2, SBTi 24-month submission, and GRI material topics.

Why this matters: EU CSRD Art.29a requires the double materiality assessment before a single metric is reported — companies that miss this pre-step have defective disclosures. California SB 253 carries $500K/year civil penalties for non-compliance. Your platform's job is to make sure no deadline gets missed.

{
  "name": "Carbon/ESG Disclosure Deadline Tracker",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weekdays",
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "a1",
      "name": "8AM Weekdays",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "climate_disclosure_deadlines",
        "dataLocationOnSheet": {
          "rangeDefinition": "detectAutomatically"
        }
      },
      "id": "a2",
      "name": "Get Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const today = new Date();\nconst rows = $input.all();\nconst alerts = [];\nfor (const row of rows) {\n  const d = row.json;\n  const deadline = new Date(d.deadline_date);\n  const diffDays = Math.ceil((deadline - today) / 86400000);\n  const tiers = {\n    ENTERPRISE_ESG_PLATFORM_SAAS: 'enterprise',\n    CARBON_ACCOUNTING_SAAS: 'carbon',\n    SUPPLY_CHAIN_SUSTAINABILITY_SAAS: 'supplychain',\n    GHG_EMISSIONS_SAAS: 'ghg',\n    ESG_REPORTING_SAAS: 'esgreport',\n    IMPACT_INVESTING_SAAS: 'impact',\n    SUSTAINABILITY_STARTUP_SAAS: 'startup'\n  };\n  const fwTypes = {\n    SEC_CLIMATE_10K: 'SEC Rule 33-11275 10-K climate risk disclosure',\n    EU_CSRD_ESRS_E1: 'EU CSRD Art.29a ESRS E1 climate change \u2014 double materiality assessment',\n    CA_SB253_SCOPE1_2: 'California SB 253 CCDAA Scope 1+2 GHG inventory (>$1B revenue)',\n    CA_SB253_SCOPE3: 'California SB 253 CCDAA Scope 3 value chain (>$1B revenue, 1yr after S1/S2)',\n    CA_SB261_CLIMATE_RISK: 'California SB 261 biennial climate-related financial risk report (>$500M)',\n    EU_ETS_ANNUAL: 'EU ETS Directive 2003/87/EC verified emissions report \u2014 March 31 deadline',\n    CDP_ANNUAL: 'CDP annual climate questionnaire \u2014 October response window',\n    TCFD_REPORT: 'TCFD 11 recommendations governance/strategy/risk/metrics disclosure',\n    ISSB_IFRS_S1: 'ISSB IFRS S1 general sustainability-related financial disclosures',\n    ISSB_IFRS_S2: 'ISSB IFRS S2 climate-related disclosures \u2014 physical + transition risk',\n    SBTI_SUBMISSION: 'SBTi near-term target submission \u2014 24-month validation window',\n    GRI_MATERIAL: 'GRI Universal Standards 2021 material topics disclosure'\n  };\n  let severity;\n  if (diffDays < 0) severity = 'OVERDUE';\n  else if (diffDays <= 14) severity = 'CRITICAL';\n  else if (diffDays <= 45) severity = 'URGENT';\n  else if (diffDays <= 90) severity = 'WARNING';\n  else if (diffDays <= 120) severity = 'NOTICE';\n  else continue;\n  const fwLabel = fwTypes[d.disclosure_type] || d.disclosure_type;\n  alerts.push({\n    company_id: d.company_id,\n    company_name: d.company_name,\n    tier: tiers[d.tier] || d.tier,\n    disclosure_type: d.disclosure_type,\n    framework_label: fwLabel,\n    severity,\n    days_remaining: diffDays,\n    deadline_date: d.deadline_date,\n    assigned_owner: d.assigned_owner,\n    alert_key: `${d.company_id}-${d.disclosure_type}-${d.deadline_date}`\n  });\n}\nreturn alerts.map(a => ({json: a}));"
      },
      "id": "a3",
      "name": "Classify Severity",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.severity }}",
              "rightValue": "OVERDUE",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "c2",
              "leftValue": "={{ $json.severity }}",
              "rightValue": "CRITICAL",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "or"
        }
      },
      "id": "a4",
      "name": "Critical or Overdue?",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.1,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "text": "=:rotating_light: *[{{ $json.severity }}] ESG Disclosure Deadline \u2014 {{ $json.company_name }}*\n\n*Framework:* {{ $json.framework_label }}\n*Tier:* {{ $json.tier }}\n*Deadline:* {{ $json.deadline_date }} ({{ $json.days_remaining }}d remaining)\n*Owner:* {{ $json.assigned_owner }}\n\n{{ $json.severity === 'OVERDUE' ? 'OVERDUE \u2014 IMMEDIATE regulatory risk. Escalate to Sustainability Officer.' : 'CRITICAL \u2014 14 days or less. Deadline imminent.' }}",
        "channelId": "#regulatory-esg-team"
      },
      "id": "a5",
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ]
    }
  ],
  "connections": {
    "8AM Weekdays": {
      "main": [
        [
          {
            "node": "Get Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Deadlines": {
      "main": [
        [
          {
            "node": "Classify Severity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Severity": {
      "main": [
        [
          {
            "node": "Critical or Overdue?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Critical or Overdue?": {
      "main": [
        [
          {
            "node": "Slack Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: CSRD Scope 3 Supplier Data Request & Follow-up

Automates the Scope 3 supplier outreach loop: sends data requests with EU CSRD Art.29a / ESRS E1-6 citations, logs responses to Sheets, and escalates non-responsive suppliers to Slack after a tier-appropriate window.

Why this matters: ESRS E1-6 (Scope 3 GHG emissions) requires all material upstream and downstream categories — a company that submits Scope 1+2 but not material Scope 3 categories fails the CSRD disclosure standard. Your SaaS platform needs this pipeline running continuously for the entire supply chain.

{
  "name": "CSRD Scope 3 Supplier Data Request & Follow-up",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "supplier-data-due",
        "responseMode": "responseNode"
      },
      "id": "b1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const d = $input.first().json;\nconst supplierTiers = {\n  TIER1_CRITICAL: { label: 'Tier 1 Critical', scope3Cat: 'Cat 1 Purchased Goods/Services', reminderDays: 3 },\n  TIER1_MATERIAL: { label: 'Tier 1 Material', scope3Cat: 'Cat 1/4 Upstream Transport', reminderDays: 5 },\n  TIER2_SIGNIFICANT: { label: 'Tier 2 Significant', scope3Cat: 'Cat 2/3 Capital Goods/Fuel Energy', reminderDays: 7 },\n  TIER2_STANDARD: { label: 'Tier 2 Standard', scope3Cat: 'Cat 1-15 Value Chain', reminderDays: 10 }\n};\nconst tier = supplierTiers[d.supplier_tier] || supplierTiers.TIER2_STANDARD;\nconst csrdRef = 'EU CSRD Art.29a / ESRS E1-6 Scope 3 value chain GHG inventory';\nconst ghgRef = 'GHG Protocol Corporate Value Chain (Scope 3) Standard \u2014 Category ' + tier.scope3Cat;\nreturn [{json:{\n  supplier_id: d.supplier_id,\n  supplier_name: d.supplier_name,\n  supplier_email: d.supplier_email,\n  customer_company: d.customer_company,\n  reporting_year: d.reporting_year,\n  due_date: d.due_date,\n  tier_config: tier,\n  csrd_ref: csrdRef,\n  ghg_ref: ghgRef,\n  request_id: `SCO3-${d.supplier_id}-${d.reporting_year}-${Date.now()}`\n}}]"
      },
      "id": "b2",
      "name": "Build Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "fromEmail": "sustainability@{{$json.customer_company}}.com",
        "toEmail": "={{ $json.supplier_email }}",
        "subject": "=Scope 3 GHG Data Request \u2014 {{ $json.customer_company }} FY{{ $json.reporting_year }} (Due {{ $json.due_date }})",
        "emailType": "html",
        "message": "=<p>Dear {{ $json.supplier_name }} team,</p><p>As part of our {{ $json.customer_company }} FY{{ $json.reporting_year }} climate disclosure obligations under <strong>{{ $json.csrd_ref }}</strong>, we are collecting Scope 3 emissions data from our {{ $json.tier_config.label }} suppliers.</p><p><strong>Data requested ({{ $json.tier_config.scope3Cat }}):</strong></p><ul><li>Total Scope 1 + Scope 2 (market-based) emissions (tCO\u2082e)</li><li>Measurement methodology (spend-based / activity-based / supplier-specific)</li><li>Reporting boundary and consolidation approach</li><li>Third-party verification status (if any)</li></ul><p><strong>Due date:</strong> {{ $json.due_date }}<br><strong>Reference:</strong> {{ $json.ghg_ref }}</p><p>Please reply to this email or submit via our supplier portal. Request ID: <code>{{ $json.request_id }}</code></p>"
      },
      "id": "b3",
      "name": "Request Email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "scope3_supplier_requests",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "supplier_id": "={{ $json.supplier_id }}",
            "supplier_name": "={{ $json.supplier_name }}",
            "tier": "={{ $json.tier_config.label }}",
            "request_id": "={{ $json.request_id }}",
            "requested_at": "={{ new Date().toISOString() }}",
            "due_date": "={{ $json.due_date }}",
            "status": "REQUESTED",
            "response_received": "FALSE"
          }
        },
        "options": {}
      },
      "id": "b4",
      "name": "Log to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.4,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "amount": "={{ $json.tier_config.reminderDays }}",
        "unit": "days"
      },
      "id": "b5",
      "name": "Wait for Reminder",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "scope3_supplier_requests",
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "request_id",
              "lookupValue": "={{ $json.request_id }}"
            }
          ]
        }
      },
      "id": "b6",
      "name": "Check Response",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.4,
      "position": [
        1100,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "leftValue": "={{ $json.response_received }}",
              "rightValue": "FALSE",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "id": "b7",
      "name": "No Response?",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.1,
      "position": [
        1320,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "text": "=:warning: *Scope 3 Data Missing \u2014 {{ $json.supplier_name }}*\n\nSupplier has not responded to Scope 3 emissions data request.\n*Tier:* {{ $json.tier_config.label }}\n*Due:* {{ $json.due_date }}\n*Request ID:* {{ $json.request_id }}\n*CSRD ref:* {{ $json.csrd_ref }}\n\nEscalate to sustainability team for manual follow-up.",
        "channelId": "#scope3-data-collection"
      },
      "id": "b8",
      "name": "Slack Follow-up",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1540,
        0
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Build Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Request": {
      "main": [
        [
          {
            "node": "Request Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Email": {
      "main": [
        [
          {
            "node": "Log to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to Sheets": {
      "main": [
        [
          {
            "node": "Wait for Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Reminder": {
      "main": [
        [
          {
            "node": "Check Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Response": {
      "main": [
        [
          {
            "node": "No Response?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Response?": {
      "main": [
        [
          {
            "node": "Slack Follow-up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: Scope 1/2/3 GHG Emissions Data Ingestion & Validation

Validates incoming emissions data against GHG Protocol requirements (Scope 2 dual reporting: location-based AND market-based), checks Scope 3 material category coverage for ESRS E1-6, and upserts validated records to Postgres. Failed validations are flagged to Slack immediately.

Why this matters: The most common CSRD data quality failure is submitting only Scope 2 location-based (ignoring market-based) — ESRS E1-4 explicitly requires both. Your ingestion pipeline should catch this before the data enters your platform, not after.

{
  "name": "Scope 1/2/3 GHG Emissions Data Ingestion & Validation",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "emissions-data",
        "responseMode": "responseNode"
      },
      "id": "c1",
      "name": "Emissions Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const d = $input.first().json;\nconst errors = [];\nconst warnings = [];\n// GHG Protocol validation\nif (!d.scope1_direct_mtco2e && d.scope1_direct_mtco2e !== 0) errors.push('scope1_direct_mtco2e required (GHG Protocol Scope 1 \u2014 direct emissions from owned sources)');\nif (!d.scope2_location_mtco2e && d.scope2_location_mtco2e !== 0) errors.push('scope2_location_mtco2e required (GHG Protocol Scope 2 location-based)');\nif (!d.scope2_market_mtco2e && d.scope2_market_mtco2e !== 0) errors.push('scope2_market_mtco2e required (GHG Protocol Scope 2 market-based \u2014 EU CSRD ESRS E1-4 requirement)');\nif (!d.reporting_year) errors.push('reporting_year required');\nif (!d.boundary_approach) errors.push('boundary_approach required (operational_control / financial_control / equity_share)');\nconst s3cats = d.scope3_categories || {};\nconst materialCats = ['cat1_purchased_goods', 'cat3_fuel_energy', 'cat11_use_of_sold_products'];\nconst s3Coverage = materialCats.filter(c => s3cats[c] !== undefined).length;\nif (s3Coverage < materialCats.length) warnings.push(`Scope 3 partial: missing material categories. EU CSRD ESRS E1-6 requires all material upstream/downstream categories.`);\n// SBTi FLAG-2: Scope 2 market-based required for SBTi near-term targets\nif (d.scope2_market_mtco2e > d.scope2_location_mtco2e * 1.5) warnings.push('Scope 2 market-based significantly higher than location-based \u2014 verify renewable energy certificates (RECs/GOs)');\nconst totalScope1_2 = (d.scope1_direct_mtco2e||0) + (d.scope2_market_mtco2e||0);\nconst scope3Total = Object.values(s3cats).reduce((a,b)=>a+(b||0), 0);\nconst scope3Pct = totalScope1_2 > 0 ? Math.round(scope3Total/(totalScope1_2+scope3Total)*100) : 0;\nreturn [{json:{\n  ...d,\n  validation_errors: errors,\n  validation_warnings: warnings,\n  is_valid: errors.length === 0,\n  total_scope1_2_mtco2e: totalScope1_2,\n  scope3_total_mtco2e: scope3Total,\n  scope3_coverage_pct: scope3Pct,\n  ghg_protocol_compliant: errors.length === 0 && warnings.length === 0,\n  csrd_esrs_e1_ready: errors.length === 0 && s3Coverage >= materialCats.length,\n  ingested_at: new Date().toISOString()\n}}]"
      },
      "id": "c2",
      "name": "Validate GHG Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "leftValue": "={{ $json.is_valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ]
        }
      },
      "id": "c3",
      "name": "Is Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.1,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO emissions_records (company_id, reporting_year, scope1_direct_mtco2e, scope2_location_mtco2e, scope2_market_mtco2e, scope3_total_mtco2e, scope3_coverage_pct, boundary_approach, ghg_protocol_compliant, csrd_esrs_e1_ready, ingested_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (company_id, reporting_year) DO UPDATE SET scope1_direct_mtco2e=EXCLUDED.scope1_direct_mtco2e, scope2_market_mtco2e=EXCLUDED.scope2_market_mtco2e, scope3_total_mtco2e=EXCLUDED.scope3_total_mtco2e, csrd_esrs_e1_ready=EXCLUDED.csrd_esrs_e1_ready, ingested_at=EXCLUDED.ingested_at",
        "options": {},
        "additionalFields": {
          "queryParams": "={{ [$json.company_id, $json.reporting_year, $json.scope1_direct_mtco2e, $json.scope2_location_mtco2e, $json.scope2_market_mtco2e, $json.scope3_total_mtco2e, $json.scope3_coverage_pct, $json.boundary_approach, $json.ghg_protocol_compliant, $json.csrd_esrs_e1_ready, $json.ingested_at] }}"
        }
      },
      "id": "c4",
      "name": "Upsert Postgres",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        660,
        -100
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "text": "=:x: *GHG Validation Failed \u2014 {{ $json.company_id }} FY{{ $json.reporting_year }}*\n\n*Errors:*\n{{ $json.validation_errors.join('\\n') }}\n\n{{ $json.validation_warnings.length > 0 ? '*Warnings:*\\n' + $json.validation_warnings.join('\\n') : '' }}",
        "channelId": "#ghg-data-quality"
      },
      "id": "c5",
      "name": "Slack Error",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        660,
        100
      ]
    }
  ],
  "connections": {
    "Emissions Webhook": {
      "main": [
        [
          {
            "node": "Validate GHG Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate GHG Data": {
      "main": [
        [
          {
            "node": "Is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Valid?": {
      "main": [
        [
          {
            "node": "Upsert Postgres",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: EU ETS Allowance Deficit & Carbon Credit Monitor

Polls your customers' EU ETS positions hourly, calculates allowance deficits, estimates the €100/tonne penalty under Directive 2003/87/EC Art.12, checks offset vintage and additionality for SBTi beyond-value-chain mitigation requirements, and alerts #carbon-desk when coverage falls below threshold.

Why this matters: The EU ETS surrender deadline is April 30 every year. A company that is short on allowances faces a €100/tonne fine — and that fine doesn't extinguish the obligation to surrender the allowances anyway. Your platform needs to surface deficits at least 30 days out, not on April 29.

{
  "name": "EU ETS Allowance Deficit & Carbon Credit Registry Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "minutesInterval": 1
            }
          ]
        }
      },
      "id": "d1",
      "name": "Hourly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "carbon_positions",
        "dataLocationOnSheet": {
          "rangeDefinition": "detectAutomatically"
        }
      },
      "id": "d2",
      "name": "Get Positions",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "url": "=https://api.carbonmonitor.org/api/v1/ets/allowances/{{ $json.account_id }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_EU_ETS_API_KEY"
            }
          ]
        },
        "options": {}
      },
      "id": "d3",
      "name": "EU ETS API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const position = $('Get Positions').first().json;\nconst etsData = $input.first().json;\nconst alerts = [];\nconst verifiedEmissions = etsData.verified_emissions_mtco2e || 0;\nconst allocatedAllowances = etsData.allocated_allowances_eur || 0;\nconst purchasedAllowances = etsData.purchased_allowances_eur || 0;\nconst retiredOffsets = etsData.retired_offsets_eur || 0;\nconst totalAllowances = allocatedAllowances + purchasedAllowances + retiredOffsets;\nconst deficit = verifiedEmissions - totalAllowances;\nconst coverageRatio = totalAllowances / Math.max(verifiedEmissions, 1);\n// EU ETS: surrender deadline April 30, fine = 100 EUR/tonne excess\nconst today = new Date();\nconst aprilDeadline = new Date(today.getFullYear(), 3, 30); // April 30\nconst daysToDeadline = Math.ceil((aprilDeadline - today) / 86400000);\nconst estimatedFine = Math.max(0, deficit) * 100; // EUR\nlet severity = 'OK';\nif (deficit > 0 && daysToDeadline <= 14) severity = 'CRITICAL';\nelse if (deficit > 0 && daysToDeadline <= 30) severity = 'URGENT';\nelse if (coverageRatio < 0.8) severity = 'WARNING';\n// Offset quality checks (Verra VCS / Gold Standard)\nconst offsetFlags = [];\nif (position.offset_vintage && new Date(position.offset_vintage).getFullYear() < 2020) offsetFlags.push('VINTAGE_WARNING: EU CSRD ESRS E1 recommends post-2020 vintage offsets');\nif (position.additionality_verified !== 'TRUE') offsetFlags.push('ADDITIONALITY_UNVERIFIED: SBTi requires beyond-value-chain mitigation additionality');\nreturn [{json:{\n  account_id: position.account_id,\n  company_name: position.company_name,\n  verified_emissions_mtco2e: verifiedEmissions,\n  total_allowances_eur: totalAllowances,\n  deficit_mtco2e: deficit,\n  coverage_ratio: Math.round(coverageRatio*100)/100,\n  estimated_fine_eur: estimatedFine,\n  days_to_april_deadline: daysToDeadline,\n  severity,\n  offset_flags: offsetFlags,\n  regulatory_ref: 'EU ETS Directive 2003/87/EC Art.12 \u2014 surrender EUAs by April 30 or 100 EUR/tonne penalty'\n}}]"
      },
      "id": "d4",
      "name": "Analyze Position",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "OK",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ]
        }
      },
      "id": "d5",
      "name": "Alert Needed?",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.1,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "text": "=:chart_with_downwards_trend: *[{{ $json.severity }}] EU ETS Allowance Alert \u2014 {{ $json.company_name }}*\n\n*Deficit:* {{ $json.deficit_mtco2e }} tCO\u2082e\n*Coverage:* {{ $json.coverage_ratio*100 }}%\n*Estimated fine:* \u20ac{{ $json.estimated_fine_eur.toLocaleString() }}\n*Days to April 30 surrender:* {{ $json.days_to_april_deadline }}\n\n{{ $json.offset_flags.length > 0 ? ':warning: Offset flags:\\n' + $json.offset_flags.join('\\n') : '' }}\n\n*Ref:* {{ $json.regulatory_ref }}",
        "channelId": "#carbon-desk"
      },
      "id": "d6",
      "name": "Slack Carbon Desk",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1100,
        0
      ]
    }
  ],
  "connections": {
    "Hourly": {
      "main": [
        [
          {
            "node": "Get Positions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Positions": {
      "main": [
        [
          {
            "node": "EU ETS API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "EU ETS API": {
      "main": [
        [
          {
            "node": "Analyze Position",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Position": {
      "main": [
        [
          {
            "node": "Alert Needed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Needed?": {
      "main": [
        [
          {
            "node": "Slack Carbon Desk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly ESG Platform KPI Dashboard

Pulls metrics from Postgres, computes WoW deltas, and emails a structured KPI table to the CEO every Monday — with the Sustainability Officer BCC'd as a governance trail requirement under ESRS 1 due diligence. Flags low CSRD readiness (<60%) and low Scope 3 coverage (<50%) automatically.

{
  "name": "Weekly ESG/Sustainability Platform KPI Dashboard",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "daysOfTheWeek": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "e1",
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(DISTINCT company_id) as active_companies, COUNT(*) as reports_submitted, SUM(CASE WHEN csrd_esrs_e1_ready THEN 1 ELSE 0 END) as csrd_ready_count, AVG(scope3_coverage_pct) as avg_scope3_coverage, SUM(CASE WHEN ghg_protocol_compliant THEN 1 ELSE 0 END) as ghg_compliant_count FROM emissions_records WHERE ingested_at >= NOW() - INTERVAL '7 days'",
        "options": {}
      },
      "id": "e2",
      "name": "This Week",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(DISTINCT company_id) as active_companies, COUNT(*) as reports_submitted, SUM(CASE WHEN csrd_esrs_e1_ready THEN 1 ELSE 0 END) as csrd_ready_count, AVG(scope3_coverage_pct) as avg_scope3_coverage FROM emissions_records WHERE ingested_at >= NOW() - INTERVAL '14 days' AND ingested_at < NOW() - INTERVAL '7 days'",
        "options": {}
      },
      "id": "e3",
      "name": "Last Week",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        220,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "const tw = $('This Week').first().json;\nconst lw = $('Last Week').first().json;\nconst pct = (a,b) => b > 0 ? Math.round((a-b)/b*100) : 0;\nconst delta = (a,b) => { const p=pct(a,b); return (p>=0?'+':'')+p+'%'; };\nconst csrdPct = tw.reports_submitted > 0 ? Math.round(tw.csrd_ready_count/tw.reports_submitted*100) : 0;\nconst ghgPct = tw.reports_submitted > 0 ? Math.round(tw.ghg_compliant_count/tw.reports_submitted*100) : 0;\nconst flags = [];\nif (csrdPct < 60) flags.push(`LOW CSRD READINESS: only ${csrdPct}% of submitted reports are ESRS E1-ready \u2014 customers at EU CSRD Art.29a risk`);\nif (parseFloat(tw.avg_scope3_coverage||0) < 50) flags.push(`SCOPE 3 COVERAGE LOW: avg ${parseFloat(tw.avg_scope3_coverage||0).toFixed(0)}% \u2014 ESRS E1-6 requires material Scope 3 categories`);\nreturn [{json:{\n  active_companies: tw.active_companies,\n  active_delta: delta(tw.active_companies, lw.active_companies),\n  reports_submitted: tw.reports_submitted,\n  reports_delta: delta(tw.reports_submitted, lw.reports_submitted),\n  csrd_ready_pct: csrdPct,\n  csrd_delta: delta(tw.csrd_ready_count, lw.csrd_ready_count),\n  avg_scope3_coverage: parseFloat(tw.avg_scope3_coverage||0).toFixed(1),\n  ghg_compliant_pct: ghgPct,\n  flags,\n  week_of: new Date().toISOString().split('T')[0]\n}}]"
      },
      "id": "e4",
      "name": "Compute KPIs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        100
      ]
    },
    {
      "parameters": {
        "fromEmail": "esg-platform@yourcompany.com",
        "toEmail": "ceo@yourcompany.com",
        "ccEmail": "sustainability-officer@yourcompany.com",
        "subject": "=ESG Platform Weekly KPI \u2014 Week of {{ $json.week_of }}",
        "emailType": "html",
        "message": "=<h2>ESG / Sustainability Platform \u2014 Weekly KPI Report</h2><p><em>Week of {{ $json.week_of }}</em></p><table border='1' cellpadding='6' style='border-collapse:collapse;font-family:sans-serif'><tr><th>Metric</th><th>This Week</th><th>WoW</th></tr><tr><td>Active Companies</td><td>{{ $json.active_companies }}</td><td>{{ $json.active_delta }}</td></tr><tr><td>Reports Submitted</td><td>{{ $json.reports_submitted }}</td><td>{{ $json.reports_delta }}</td></tr><tr><td>CSRD ESRS E1-Ready (%)</td><td>{{ $json.csrd_ready_pct }}%</td><td>{{ $json.csrd_delta }}</td></tr><tr><td>Avg Scope 3 Coverage</td><td>{{ $json.avg_scope3_coverage }}%</td><td>\u2014</td></tr><tr><td>GHG Protocol Compliant</td><td>{{ $json.ghg_compliant_pct }}%</td><td>\u2014</td></tr></table>{{ $json.flags.length > 0 ? '<h3 style=\"color:red\">\u26a0 Flags</h3><ul>' + $json.flags.map(f => '<li>' + f + '</li>').join('') + '</ul>' : '<p style=\"color:green\">\u2713 No compliance flags this week</p>' }}<p><em>Note: CSRD ESRS E1 readiness = Scope 1+2+3 material categories submitted. BCC: Sustainability Officer for governance trail (EU CSRD Art.6 ESRS 1 due diligence).</em></p>"
      },
      "id": "e5",
      "name": "KPI Email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        660,
        100
      ]
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "This Week",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Last Week",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "This Week": {
      "main": [
        [
          {
            "node": "Compute KPIs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Last Week": {
      "main": [
        [
          {
            "node": "Compute KPIs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute KPIs": {
      "main": [
        [
          {
            "node": "KPI Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why n8n vs. Zapier/Make for ESG SaaS

Feature n8n (self-hosted) Zapier/Make
Data stays in your VPC ✅ Yes ❌ No — flows through external cloud
EU CSRD Art.6 ESRS 1 due diligence ✅ Automation stack in boundary ❌ Third-party data processor
GHG Protocol third-party data custody ✅ Controlled ⚠ Documented data flow required
CDP supply chain questionnaire ✅ No external processor flag ⚠ Disclose as data processor
Workflow JSON = audit trail ✅ Git-versionable ❌ UI-only, hard to diff
SFDR/UNPRI PRI reporting boundary ✅ Stays in scope ❌ Expands scope

Get the templates

All 5 workflow JSONs above are import-ready. If you want the full bundle of 14+ pre-built n8n automation templates (including AI agent memory, invoice generator, and more), they're at FlowKit on Gumroad.

Questions or customization help? Drop a comment below.

Top comments (0)