AutomationFlowsEmail & Gmail › Monitor Ssl Certificate Expiry with Google Sheets, Slack, Gmail and Jira

Monitor Ssl Certificate Expiry with Google Sheets, Slack, Gmail and Jira

ByAvkash Kakdiya @itechnotion on n8n.io

This workflow runs daily to check SSL certificate expiry for domains listed in Google Sheets, using ssl-checker.io to fetch certificate details, then creating Jira issues and sending Slack and Gmail alerts for risky certificates while logging results and posting a daily Slack…

Cron / scheduled trigger★★★★☆ complexity17 nodesGoogle SheetsHTTP RequestJiraSlackGmail
Email & Gmail Trigger: Cron / scheduled Nodes: 17 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16196 — we link there as the canonical source.

This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "naBLzOgOaG1gG6k6",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "SSL Certificate Expiry Monitor & Alerts",
  "tags": [
    {
      "id": "2V3HXFbv2wqNGm6s",
      "name": "Dev",
      "createdAt": "2025-06-17T05:42:41.949Z",
      "updatedAt": "2025-06-17T05:42:41.949Z"
    }
  ],
  "nodes": [
    {
      "id": "6ed7c14c-f10c-427b-aed5-6af831934d54",
      "name": "Sticky Note Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        16
      ],
      "parameters": {
        "width": 496,
        "height": 752,
        "content": "## SSL Certificate Expiry Monitor\nThis workflow automatically checks the SSL certificates of all your domains on a daily schedule and warns your team before any certificate expires. It reads the domain list from Google Sheets, fetches the certificate details for each domain, calculates how many days remain until expiry, and routes each result by urgency. Expiring or expired certificates trigger immediate alerts and ticket creation, while healthy certificates are simply logged. This prevents site outages, browser security warnings, and lost customer trust caused by lapsed certificates.\n\n### How it Works\n\t- A daily schedule trigger starts the workflow every morning.\n\t- The domain list is read from a Google Sheet.\n\t- Disabled or blank domain rows are filtered out before checking.\n\t- Each domain is processed one at a time in a loop.\n\t- An API call fetches the live SSL certificate details for the domain.\n\t- A code step calculates the exact days remaining until expiry.\n\t- An IF node checks whether the certificate is expiring soon or already expired.\n\t- Expiring certificates create a ticket and send Slack plus email alerts.\n\t- Healthy certificates are logged back to the tracking sheet for the record.\n\t- A final summary digest is posted to Slack once every domain is checked.\n\n### Setup Steps\n\t1. Create a Google Sheet with a column named domain listing every domain to monitor.\n\t2. Connect your Google Sheets credential so the workflow can read and write rows.\n\t3. Add an SSL lookup API key for the HTTP Request node that fetches certificate data.\n\t4. Connect your Slack credential and set the alert channel ID.\n\t5. Connect your Gmail credential and set the recipient address for email alerts.\n\t6. Connect your Jira credential and set the project key for ticket creation.\n\t7. Adjust the warning threshold in days inside the code node if needed.\n\t8. Activate the workflow so it runs automatically every day."
      },
      "typeVersion": 1
    },
    {
      "id": "e5bb762b-fa26-46f5-952f-eb4b70f2612c",
      "name": "Sticky Note Section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 752,
        "content": "## Step 1: Fetch Domains and Loop\n\nA daily schedule trigger starts the run, then the full domain list is read from Google Sheets. A filter step drops blank or disabled rows, and each remaining domain is passed one at a time into the loop so every certificate is checked individually without overloading the SSL lookup service."
      },
      "typeVersion": 1
    },
    {
      "id": "412736e6-0ed8-4fee-b378-00700a11dacb",
      "name": "Sticky Note Section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1216,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 752,
        "content": "## Step 2: Inspect Certificate and Score Risk\n\nFor each domain the workflow calls an SSL lookup API to retrieve the live certificate, then a code node calculates the days remaining until expiry and assigns a status of Expired, Critical, Warning, or Healthy. An IF node then decides whether the certificate needs an alert or is safe."
      },
      "typeVersion": 1
    },
    {
      "id": "a21537f7-6381-469e-9673-ba33ff844aaf",
      "name": "Sticky Note Section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1952,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 752,
        "content": "## Step 3: Alert, Ticket, Log, and Summarize\n\nCertificates that are expiring or expired create a Jira ticket and fire both a Slack message and an email so the team is notified through multiple channels. Healthy certificates skip the alerts and are written back to the tracking sheet. Once every domain has been checked, the run is aggregated into a single Slack digest so the team gets one clear end-of-run report."
      },
      "typeVersion": 1
    },
    {
      "id": "6d9a2b3c-5976-479d-956f-ddbe57401ec7",
      "name": "Daily Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        544,
        576
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "12487007-bee4-4e33-9ef4-e4a01a4dd47e",
      "name": "Read Domain List",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        768,
        576
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Domains"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "4eb0a8e6-9ed3-40b2-b756-d2b4f6ea05a8",
      "name": "Filter Active Domains",
      "type": "n8n-nodes-base.filter",
      "position": [
        992,
        576
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "filter-has-domain",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.domain }}",
              "rightValue": ""
            },
            {
              "id": "filter-not-disabled",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ ($json.enabled || 'yes').toString().toLowerCase() }}",
              "rightValue": "no"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "7e2210ae-97f1-48be-9f0e-06c0eb455065",
      "name": "Loop Over Domains",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1264,
        624
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "5500ce18-d615-438e-800a-faf8c98a6d97",
      "name": "Fetch SSL Details",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1440,
        304
      ],
      "parameters": {
        "url": "=https://ssl-checker.io/api/v1/check/{{ $json.domain }}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        }
      },
      "typeVersion": 4.3,
      "alwaysOutputData": true
    },
    {
      "id": "b508cb70-3031-4413-917b-47a6f94a5f56",
      "name": "Calculate Days Left",
      "type": "n8n-nodes-base.code",
      "position": [
        1632,
        304
      ],
      "parameters": {
        "jsCode": "// ================================================\n// SSL EXPIRY CALCULATOR AND RISK SCORER\n// ================================================\n\nconst WARNING_DAYS = 30;\nconst CRITICAL_DAYS = 7;\n\nconst results = [];\n\nfor (const item of $input.all()) {\n  const data = item.json || {};\n  const result = data.result || data;\n\n  const domain = result.host || result.domain || item.json.domain || 'unknown';\n\n  // Try common field names for the certificate expiry date\n  const rawExpiry = result.valid_till || result.validTo || result.not_after || result.expires || null;\n\n  let daysRemaining = null;\n  let expiryISO = null;\n\n  if (rawExpiry) {\n    const expiryDate = new Date(rawExpiry);\n    expiryISO = expiryDate.toISOString();\n    const now = new Date();\n    daysRemaining = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));\n  }\n\n  let status = 'Healthy';\n  let needsAlert = false;\n\n  if (daysRemaining === null) {\n    status = 'Unknown';\n    needsAlert = true;\n  } else if (daysRemaining < 0) {\n    status = 'Expired';\n    needsAlert = true;\n  } else if (daysRemaining <= CRITICAL_DAYS) {\n    status = 'Critical';\n    needsAlert = true;\n  } else if (daysRemaining <= WARNING_DAYS) {\n    status = 'Warning';\n    needsAlert = true;\n  }\n\n  const priorityMap = { Expired: 1, Unknown: 1, Critical: 2, Warning: 3, Healthy: 4 };\n\n  results.push({\n    json: {\n      domain,\n      issuer: result.issuer_o || result.issuer || 'Unknown',\n      validFrom: result.valid_from || result.validFrom || '',\n      expiryDate: expiryISO,\n      daysRemaining,\n      status,\n      needsAlert,\n      priority: priorityMap[status] || 4,\n      checkedAt: new Date().toISOString()\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "bd1bc4d8-2997-43e7-be4f-a96d0563a687",
      "name": "Needs Alert",
      "type": "n8n-nodes-base.if",
      "position": [
        1824,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "check-needs-alert",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.needsAlert }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "55c7a44d-409b-4e64-8621-7e1236888248",
      "name": "Create Jira Ticket",
      "type": "n8n-nodes-base.jira",
      "position": [
        2112,
        208
      ],
      "parameters": {
        "project": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_JIRA_PROJECT_KEY"
        },
        "summary": "=[SSL {{ $json.status }}] {{ $json.domain }} expires in {{ $json.daysRemaining }} days",
        "issueType": {
          "__rl": true,
          "mode": "name",
          "value": "Task"
        },
        "additionalFields": {
          "description": "=SSL Certificate Alert\n\nDomain: {{ $json.domain }}\nStatus: {{ $json.status }}\nDays Remaining: {{ $json.daysRemaining }}\nExpiry Date: {{ $json.expiryDate }}\nIssuer: {{ $json.issuer }}\nChecked At: {{ $json.checkedAt }}\n\nAction required: renew or reissue this certificate before it lapses to avoid downtime and browser security warnings."
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0714a001-f2d3-4792-b9a2-8d942971afb7",
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        2336,
        208
      ],
      "parameters": {
        "text": "=SSL CERTIFICATE ALERT\n\nStatus: {{ $json.status }}\nDomain: {{ $json.domain }}\nDays Remaining: {{ $json.daysRemaining }}\nExpiry Date: {{ $json.expiryDate }}\nIssuer: {{ $json.issuer }}\n\nPlease renew this certificate before it expires to prevent site outages and security warnings.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SLACK_CHANNEL_ID"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8c01bfd3-89b9-4099-ba5e-0de550ff4484",
      "name": "Email Alert",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2560,
        480
      ],
      "parameters": {
        "sendTo": "YOUR_ALERT_EMAIL",
        "message": "=An SSL certificate requires attention.\n\nDomain: {{ $json.domain }}\nStatus: {{ $json.status }}\nDays Remaining: {{ $json.daysRemaining }}\nExpiry Date: {{ $json.expiryDate }}\nIssuer: {{ $json.issuer }}\nChecked At: {{ $json.checkedAt }}\n\nRenew or reissue this certificate as soon as possible to avoid downtime and browser security warnings for your users.",
        "options": {},
        "subject": "=[SSL {{ $json.status }}] {{ $json.domain }} expires in {{ $json.daysRemaining }} days"
      },
      "typeVersion": 2.1
    },
    {
      "id": "36429ee5-022c-43e0-b82e-a28da6eef328",
      "name": "Log Healthy Cert",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2112,
        400
      ],
      "parameters": {
        "columns": {
          "value": {
            "domain": "={{ $json.domain }}",
            "status": "={{ $json.status }}",
            "checkedAt": "={{ $json.checkedAt }}",
            "expiryDate": "={{ $json.expiryDate }}",
            "daysRemaining": "={{ $json.daysRemaining }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "domain"
          ]
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "4a9b376b-b945-4751-9de3-7aa905ac6ee1",
      "name": "Aggregate Daily Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        1440,
        480
      ],
      "parameters": {
        "jsCode": "// ================================================\n// DAILY RUN AGGREGATOR\n// Collects every checked certificate from this run\n// and builds a single summary digest line.\n// ================================================\n\nconst items = $input.all();\n\nconst counts = { Expired: 0, Critical: 0, Warning: 0, Healthy: 0, Unknown: 0 };\nconst attention = [];\n\nfor (const item of items) {\n  const j = item.json || {};\n  const status = j.status || 'Unknown';\n  if (counts[status] === undefined) {\n    counts[status] = 0;\n  }\n  counts[status] += 1;\n\n  if (status !== 'Healthy') {\n    attention.push(j.domain + ' (' + status + ', ' + j.daysRemaining + 'd)');\n  }\n}\n\nconst total = items.length;\n\nconst summaryText = 'SSL Daily Summary\\n\\n'\n  + 'Total checked: ' + total + '\\n'\n  + 'Expired: ' + counts.Expired + '\\n'\n  + 'Critical: ' + counts.Critical + '\\n'\n  + 'Warning: ' + counts.Warning + '\\n'\n  + 'Healthy: ' + counts.Healthy + '\\n'\n  + 'Unknown: ' + counts.Unknown + '\\n\\n'\n  + 'Needs attention: ' + (attention.length ? attention.join(', ') : 'None');\n\nreturn [{\n  json: {\n    total,\n    counts,\n    attention,\n    summaryText,\n    generatedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b18236a6-de3f-43d0-9ebd-8f9ba4966aa1",
      "name": "Slack Daily Digest",
      "type": "n8n-nodes-base.slack",
      "position": [
        1632,
        480
      ],
      "parameters": {
        "text": "={{ $json.summaryText }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SLACK_CHANNEL_ID"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5e6e0319-3749-4c0a-bdb7-e8601884a65c",
  "connections": {
    "Email Alert": {
      "main": [
        [
          {
            "node": "Loop Over Domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Alert": {
      "main": [
        [
          {
            "node": "Create Jira Ticket",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Healthy Cert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Alert": {
      "main": [
        [
          {
            "node": "Email Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Schedule": {
      "main": [
        [
          {
            "node": "Read Domain List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Healthy Cert": {
      "main": [
        [
          {
            "node": "Loop Over Domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Domain List": {
      "main": [
        [
          {
            "node": "Filter Active Domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch SSL Details": {
      "main": [
        [
          {
            "node": "Calculate Days Left",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Domains": {
      "main": [
        [
          {
            "node": "Aggregate Daily Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch SSL Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Jira Ticket": {
      "main": [
        [
          {
            "node": "Slack Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Days Left": {
      "main": [
        [
          {
            "node": "Needs Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Active Domains": {
      "main": [
        [
          {
            "node": "Loop Over Domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Daily Summary": {
      "main": [
        [
          {
            "node": "Slack Daily Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs daily to check SSL certificate expiry for domains listed in Google Sheets, using ssl-checker.io to fetch certificate details, then creating Jira issues and sending Slack and Gmail alerts for risky certificates while logging results and posting a daily Slack…

Source: https://n8n.io/workflows/16196/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

Streamline IT and operations change management by automating approval routing, Jira issue creation, audit logging, and real-time Slack alerts. This workflow ensures faster reviews, traceable approvals

Monday.com, Slack, Jira +2
Email & Gmail

Streamline IT and operations change management by automating approval routing, Jira issue creation, audit logging, and real-time Slack alerts. This workflow ensures faster reviews, traceable approvals

Monday.com, Slack, Jira +2
Email & Gmail

E-commerce store owners, product managers, marketplace sellers, and pricing analysts who want to automatically track competitor pricing and get actionable alerts when their products are overpriced or

Google Sheets, HTTP Request, Slack +1
Email & Gmail

This template is ideal for developers, agencies, hosting providers, and website owners who need real-time alerts when a website goes down. It helps teams react quickly to downtime by sending multi-cha

Slack, HTTP Request, Gmail +1
Email & Gmail

Schedule Slack. Uses scheduleTrigger, googleSheets, slack, gmail. Scheduled trigger; 15 nodes.

Google Sheets, Slack, Gmail +1