{
  "id": "QyAM8G44li7x8ZiA",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Work Permit Approval & Safety Compliance Management System",
  "tags": [],
  "nodes": [
    {
      "id": "6dabb347-510e-441f-a0bd-8f5026a4ca2d",
      "name": "Section: Error Handler",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2720,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 556,
        "height": 368,
        "content": "## \u26a0\ufe0f Error Handler\nCatches any failure in the workflow and posts a Slack alert with the error message, failing node name, and execution ID. Wire the error output of any critical node here to prevent silent failures going unnoticed."
      },
      "typeVersion": 1
    },
    {
      "id": "78df0844-4515-4598-b636-c4f0e06e124f",
      "name": "On Workflow Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -2624,
        672
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "1766f9b8-db9f-4eaf-b67e-2dad3d601c80",
      "name": "Slack \u2013 Send Error Alert1",
      "type": "n8n-nodes-base.slack",
      "position": [
        -2368,
        672
      ],
      "parameters": {
        "text": "=error in the workflow please check ",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0AN1UGL0RM",
          "cachedResultName": "all-n8n-automations"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "a77a8986-2f1f-4adf-a117-9bfeffb36601",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3344,
        -1376
      ],
      "parameters": {
        "width": 628,
        "height": 588,
        "content": "## \ud83c\udfd7\ufe0f Construction Site Work Permit Management System\n\n### How it works\nThis workflow automates the full lifecycle of site work permits \u2014 from submission through AI conflict screening, supervisor approval, permit issuance, and automatic expiry enforcement. A worker submits a permit request via webhook form. GPT-4o checks it against all active permits for location or time conflicts. If clear, the supervisor receives an email with one-click Approve/Reject buttons. On approval, a formal permit PDF is emailed to the worker and the permit register is updated. A parallel scheduler runs every 15 minutes to send 30-minute expiry reminders and expire overdue permits automatically.\n\n### Setup steps\n1. Connect **Google Sheets OAuth2** credentials and set your Sheet ID in all Google Sheets nodes\n2. Connect **Gmail OAuth2** credentials and replace `[SUPERVISOR-EMAIL]` / `[WORKER-EMAIL]` placeholders\n3. Connect **OpenAI API** credentials (GPT-4o-mini is used for conflict detection)\n4. Update `https://YOUR-N8N-DOMAIN/webhook/permit-decision` in the **Prepare Approval Data** node with your live n8n instance URL\n5. Set up two Google Sheet tabs: **log permit** (submission log) and **Permit Register** (active permits)\n6. Connect **Slack OAuth2** if you want error alerts, or remove the error handler\n7. Activate the workflow \u2014 test with the pinned sample data on the **Work Permit Form** node"
      },
      "typeVersion": 1
    },
    {
      "id": "15a306dd-1883-4673-b50e-6cf3ae6462e3",
      "name": "Section \u2013 Intake & Logging",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2416,
        -784
      ],
      "parameters": {
        "color": 7,
        "width": 756,
        "height": 420,
        "content": "## \ud83d\udce5 Intake & Logging\nReceives the permit form submission via webhook and immediately appends a row to the **log permit** sheet. Then fetches all currently active permits from the **Permit Register** tab so the AI can check for conflicts."
      },
      "typeVersion": 1
    },
    {
      "id": "fc8b84b5-ab9d-493b-b4df-3520aee58634",
      "name": "Section \u2013 AI Conflict Detection",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1616,
        -784
      ],
      "parameters": {
        "color": 7,
        "width": 996,
        "height": 440,
        "content": "## \ud83e\udd16 AI Conflict Detection\nAggregates all active permits and sends them to GPT-4o-mini with the new request. The model checks for overlapping location/time and dangerous work-type combinations (e.g. Hot Work + Confined Space). Blocked requests trigger an immediate rejection email to the worker."
      },
      "typeVersion": 1
    },
    {
      "id": "52a7b528-14db-4e69-90cb-ea1aa6c85d3b",
      "name": "Section \u2013 Supervisor Approval Gate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -576,
        -960
      ],
      "parameters": {
        "color": 7,
        "width": 516,
        "height": 724,
        "content": "## \u2705 Supervisor Approval Gate\nBuilds formatted approval/rejection URLs, then sends the supervisor a rich HTML email with permit details and one-click decision buttons. The workflow pauses (Send and Wait) until a decision is received via the decision webhook."
      },
      "typeVersion": 1
    },
    {
      "id": "3ad93cfd-83a7-458d-9d74-419c0862939c",
      "name": "Section \u2013 Permit Issuance",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        -608
      ],
      "parameters": {
        "color": 7,
        "width": 1172,
        "height": 708,
        "content": "## \ud83d\udcc4 Permit Issuance\nOn approval, updates the sheet status to **approved**, generates a full HTML permit document with worker/supervisor details, expiry time, and safety conditions, then emails it to the worker. Rejected permits update the sheet to **rejected**."
      },
      "typeVersion": 1
    },
    {
      "id": "f8d5009a-42ac-4022-86ac-aab610e4171d",
      "name": "Section \u2013 Expiry Monitoring",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2432,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 1364,
        "height": 692,
        "content": "## \u23f0 Expiry Monitoring\nRuns every 15 minutes. Fetches all **Approved** permits from the register and checks each against the current time. Permits expiring within 30 minutes get a warning email; permits past their expiry get a stop-work notification."
      },
      "typeVersion": 1
    },
    {
      "id": "1c33754a-13ac-40ad-8006-b53eba1d241b",
      "name": "Credentials & Security",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1200,
        -96
      ],
      "parameters": {
        "color": 3,
        "width": 316,
        "height": 192,
        "content": "## \ud83d\udd10 Credentials & Security\nUse OAuth2 for Gmail and Google Sheets. Use an API key credential for OpenAI. Replace all hardcoded emails with `[SUPERVISOR-EMAIL]` and `[WORKER-EMAIL]` placeholders before sharing. Never publish real Sheet IDs or domain URLs."
      },
      "typeVersion": 1
    },
    {
      "id": "ca46db2d-7eed-4051-8184-566947a264e6",
      "name": "Route on Supervisor Decision",
      "type": "n8n-nodes-base.if",
      "position": [
        32,
        -384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "action_check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.data.approved }}",
              "rightValue": "approve"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b60cfc70-cf37-4162-ad89-fe235d41731f",
      "name": "Update Permit Log \u2013 Approved",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        432,
        -400
      ],
      "parameters": {
        "columns": {
          "value": {
            "status": "approved",
            "Worker Full Name": "={{ $('Work Permit Form').item.json.body[\"Worker Full Name\"] }}"
          },
          "schema": [
            {
              "id": "Worker Full Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Worker Full Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Worker Email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Worker Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Work Type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Work Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Work Location / Zone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Work Location / Zone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Work Start Date & Time",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Work Start Date & Time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Supervisor Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Supervisor Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Supervisor Email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Supervisor Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Description of Work",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Description of Work",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "submittedAt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "submittedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Worker Full Name"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "log permit"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "46f41083-2bfc-4fed-b98c-0b912c9bfd5a",
      "name": "Generate Permit HTML Document",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        -400
      ],
      "parameters": {
        "jsCode": "const permitId = $input.first().json.permitId;\n\nconst html = `\n<!DOCTYPE html>\n<html>\n<head><meta charset='UTF-8'><title>Work Permit ${permitId}</title>\n<style>\n  body { font-family: Arial, sans-serif; margin: 0; padding: 20px; color: #333; }\n  .header { background: #1a5276; color: white; padding: 20px; text-align: center; }\n  .permit-id { font-size: 24px; font-weight: bold; }\n  .status-badge { background: #28a745; color: white; padding: 4px 16px; border-radius: 20px; font-size: 14px; }\n  table { width: 100%; border-collapse: collapse; margin: 16px 0; }\n  th { background: #eaf4fb; padding: 10px; text-align: left; border: 1px solid #ccc; width: 40%; }\n  td { padding: 10px; border: 1px solid #ccc; }\n  .warning { background: #fff3cd; border: 2px solid #ffc107; padding: 12px; margin: 16px 0; border-radius: 4px; }\n  .footer { margin-top: 30px; font-size: 11px; color: #888; text-align: center; border-top: 1px solid #ccc; padding-top: 10px; }\n  .signature-box { border: 1px solid #333; height: 60px; margin-top: 8px; }\n</style>\n</head>\n<body>\n<div class='header'>\n  <div class='permit-id'>WORK PERMIT TO PROCEED</div>\n  <div>${permitId}</div>\n  <span class='status-badge'>\u2705 APPROVED</span>\n</div>\n<table>\n  <tr><th>Worker Name</th><td>{{workerName}}</td></tr>\n  <tr><th>Work Type</th><td>{{workType}}</td></tr>\n  <tr><th>Location / Zone</th><td>{{location}}</td></tr>\n  <tr><th>Start Date & Time</th><td>{{startDateTime}}</td></tr>\n  <tr><th>Expiry Date & Time</th><td>{{expiryDateTime}}</td></tr>\n  <tr><th>Supervisor</th><td>{{supervisorName}}</td></tr>\n  <tr><th>Work Description</th><td>{{workDescription}}</td></tr>\n  <tr><th>Risk Level</th><td>{{riskLevel}}</td></tr>\n</table>\n<div class='warning'>\n  \u26a0\ufe0f <strong>CONDITIONS OF PERMIT:</strong> This permit is valid ONLY for the work described above, at the stated location, during the stated time. Work must stop immediately if conditions change. Supervisor must be notified on completion.\n</div>\n<table>\n  <tr>\n    <th>Supervisor Signature</th>\n    <th>Worker Signature</th>\n  </tr>\n  <tr>\n    <td><div class='signature-box'></div>Name: {{supervisorName}}</td>\n    <td><div class='signature-box'></div>Name: {{workerName}}</td>\n  </tr>\n</table>\n<div class='footer'>\n  Auto-generated by EHS Permit Management System &bull; ${new Date().toISOString()} &bull; This document is legally binding\n</div>\n</body>\n</html>\n`;\n\nreturn [{ json: { permitId, htmlContent: html } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "f1c625d7-75e4-43b8-bef6-04858ee4a8c9",
      "name": "Email Approved Permit to Worker",
      "type": "n8n-nodes-base.gmail",
      "position": [
        992,
        -400
      ],
      "parameters": {
        "sendTo": "={{ $('Work Permit Form').item.json.body['Worker Email'] }}",
        "message": "={{ $json.htmlContent }}",
        "options": {},
        "subject": "\u2705 Work Permit APPROVED - {{ $('Prepare Approval Data1').item.json.permitId }} - Please collect before starting work"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "44e32a29-5604-422a-b27f-e34366c8c238",
      "name": "Expiry Scheduler (Every 15 min)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2288,
        -48
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "7e099954-b3fa-4cac-946e-df439592551f",
      "name": "Fetch Approved Permits from Register",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -2048,
        -48
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Permit Register"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "3c1cbdbe-57b6-43d5-a3b8-60c9ef9a4fd8",
      "name": "Route on Expiry or 30-min Reminder",
      "type": "n8n-nodes-base.if",
      "position": [
        -1568,
        -48
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5a2b98eb-0c08-40b3-9f24-f24edcc92e49",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.action }}",
              "rightValue": "expire"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a9770efa-96b3-46d2-8790-a83f9124a91b",
      "name": "Send Permit Expiry Notification",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -1280,
        -176
      ],
      "parameters": {
        "sendTo": "={{ $json['Worker Email'] }}",
        "message": "=<html><body style='font-family:Arial,sans-serif;'><div style='background:#dc3545;color:white;padding:20px;'><h2>\ud83d\udd34 Work Permit Expired</h2><p>Permit {{ $json['Permit ID'] }} has EXPIRED. All work must stop immediately.</p><p>Location: {{ $json['Location'] }}</p><p>Contact supervisor {{ $json['Supervisor'] }} to request a new permit if work needs to continue.</p></div></body></html>",
        "options": {},
        "subject": "\ud83d\udd34 Permit EXPIRED - {{ $json['Permit ID'] }} - Stop Work Now"
      },
      "typeVersion": 2.1
    },
    {
      "id": "a29d3eda-bfe2-4d52-86d4-daeb8b9b07d0",
      "name": "Send 30-min Expiry Reminder",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -1296,
        192
      ],
      "parameters": {
        "sendTo": "={{ $json['Worker Email'] }}",
        "message": "=<html><body style='font-family:Arial,sans-serif;'><div style='background:#ffc107;padding:20px;'><h2>\u23f0 Permit Expiry Reminder</h2><p>Your work permit <strong>{{ $json['Permit ID'] }}</strong> will expire in approximately 30 minutes.</p><p><strong>Location:</strong> {{ $json['Location'] }}</p><p>Please begin wrapping up work and ensure the area is safe before the permit expires. Contact <strong>{{ $json['Supervisor'] }}</strong> if you need an extension.</p></div></body></html>",
        "options": {},
        "subject": "\u23f0 Reminder: Permit {{ $json['Permit ID'] }} expiring in 30 minutes"
      },
      "typeVersion": 2.1
    },
    {
      "id": "95dbc715-4275-4038-8862-fe517090247f",
      "name": "Update Permit Log \u2013 Rejected",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        288,
        -128
      ],
      "parameters": {
        "columns": {
          "value": {
            "status": "rejected",
            "Worker Full Name": "={{ $('Work Permit Form').item.json.body[\"Worker Full Name\"] }}"
          },
          "schema": [
            {
              "id": "Worker Full Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Worker Full Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Worker Full Name"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "log permit"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "7a89c9e6-4b17-43d6-a668-b9f907c8cf0c",
      "name": "Work Permit Form",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -2352,
        -576
      ],
      "parameters": {
        "path": "work-permit-request",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "lastNode"
      },
      "typeVersion": 2
    },
    {
      "id": "abb2af69-99c1-4374-b2c9-bf358d9c2e01",
      "name": "Log Permit Submission to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -2032,
        -576
      ],
      "parameters": {
        "columns": {
          "value": {
            "Work Type": "={{ $json.body[\"Work Type\"] }}",
            "submittedAt": "={{ $json.body.submittedAt }}",
            "Worker Email": "={{ $json.body[\"Worker Email\"] }}",
            "Supervisor Name": "={{ $json.body[\"Supervisor Name\"] }}",
            "Supervisor Email": "={{ $json.body[\"Supervisor Email\"] }}",
            "Worker Full Name": "={{ $json.body[\"Worker Full Name\"] }}",
            "Description of Work": "={{ $json.body[\"Description of Work\"] }}",
            "Work Location / Zone": "={{ $json.body[\"Work Location / Zone\"] }}",
            "Work Start Date & Time": "={{ $json.body[\"Work Start Date & Time\"] }}"
          },
          "schema": [
            {
              "id": "Worker Full Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Worker Full Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Worker Email",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Worker Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Work Type",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Work Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Work Location / Zone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Work Location / Zone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Work Start Date & Time",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Work Start Date & Time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Supervisor Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Supervisor Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Supervisor Email",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Supervisor Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Description of Work",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Description of Work",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "submittedAt",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "submittedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "log permit"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "4854e444-84de-49ca-8fed-ffa18720cd04",
      "name": "Fetch Active Permits for Conflict Check",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -1792,
        -576
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Permit Register"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "7efb228b-6668-4147-8aef-f964b062ed89",
      "name": "Aggregate Active Permits",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1552,
        -576
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "5a844f70-6679-4f44-bde3-34cff8bb81ac",
      "name": "GPT-4o Permit Conflict Check",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -1312,
        -576
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {
          "temperature": 0.1
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You are an EHS (Environmental Health & Safety) compliance engine for a construction/industrial site. Your job is to check permit conflicts strictly.\n\nA CONFLICT exists when:\n1. Same LOCATION and overlapping TIME window as another active permit\n2. Same WORK TYPE in same LOCATION on same DATE\n3. Mutually exclusive work (e.g. Hot Work and Confined Space Entry in same zone at same time is extremely dangerous)\n\nAlways respond in valid JSON only. No markdown, no prose.\n\nFormat:\n{\n  \"conflict_detected\": true/false,\n  \"conflict_reason\": \"string or null\",\n  \"conflicting_permit_ids\": [\"PTW-XXX\"] or [],\n  \"risk_level\": \"HIGH\" | \"MEDIUM\" | \"LOW\" | \"NONE\",\n  \"recommendation\": \"string\"\n}"
            },
            {
              "content": "=NEW PERMIT REQUEST:\nWorker: {{ $('Work Permit Form').item.json.body['Worker Full Name'] }}\nWork Type: {{ $('Work Permit Form').item.json.body['Work Type'] }}\nLocation: {{ $('Work Permit Form').item.json.body['Work Location / Zone'] }}\nStart: {{ $('Work Permit Form').item.json.body['Work Start Date & Time'] }}\nDuration: {{ $('Work Permit Form').item.json.body['Duration (Hours)'] }} hours\n\nACTIVE PERMITS IN SYSTEM:\n{{ JSON.stringify($json.data, null, 2) }}\n\nCheck for conflicts and return JSON response."
            }
          ]
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "eeb3bbb5-fdaf-4ffb-b91f-290a0d48c4b4",
      "name": "Parse GPT Conflict Response",
      "type": "n8n-nodes-base.code",
      "position": [
        -960,
        -576
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json.message.content;\nlet analysis;\ntry {\n  analysis = JSON.parse(raw);\n} catch(e) {\n  const match = raw.match(/\\{[\\s\\S]*\\}/);\n  analysis = match ? JSON.parse(match[0]) : { conflict_detected: false, risk_level: 'UNKNOWN', recommendation: 'Manual review required', conflicting_permit_ids: [], conflict_reason: null };\n}\n\nconst body = $('Work Permit Form').item.json.body;\nconst permitId = 'PTW-' + new Date().toISOString().slice(0,10).replace(/-/g,'') + '-' + Math.floor(Math.random()*900+100);\nconst startDt = new Date(body['Work Start Date & Time']);\nconst expiry = new Date(startDt.getTime() + body['Duration (Hours)'] * 3600000);\n\nreturn [{\n  json: {\n    permitId,\n    workerName: body['Worker Full Name'],\n    workerEmail: body['Worker Email'],\n    workType: body['Work Type'],\n    location: body['Work Location / Zone'],\n    startDateTime: startDt.toISOString(),\n    expiryDateTime: expiry.toISOString(),\n    durationHours: body['Duration (Hours)'],\n    supervisorName: body['Supervisor Name'],\n    supervisorEmail: body['Supervisor Email'],\n    workDescription: body['Description of Work'],\n    conflict_detected: analysis.conflict_detected,\n    conflict_reason: analysis.conflict_reason,\n    conflicting_permit_ids: analysis.conflicting_permit_ids,\n    risk_level: analysis.risk_level,\n    recommendation: analysis.recommendation,\n    submittedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cff1d5a6-d5b3-4769-8d39-5f306351bc85",
      "name": "Conflict Detected?",
      "type": "n8n-nodes-base.if",
      "position": [
        -736,
        -576
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "conflict_check",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.conflict_detected }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e634a268-e828-4fb7-a39d-09aa52e35a6f",
      "name": "Send Conflict Block Email to Worker",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -512,
        -768
      ],
      "parameters": {
        "sendTo": "={{ $json.workerEmail }}",
        "message": "=<html><body style='font-family:Arial,sans-serif;max-width:600px;margin:0 auto;'>\n<div style='background:#dc3545;color:white;padding:20px;border-radius:8px 8px 0 0;'>\n<h2>\ud83d\udeab Permit Request Blocked</h2>\n<p style='margin:0;font-size:14px;'>Reference: {{ $json.permitId }}</p>\n</div>\n<div style='background:#fff3f3;border:2px solid #dc3545;padding:20px;border-radius:0 0 8px 8px;'>\n<p>Dear <strong>{{ $json.workerName }}</strong>,</p>\n<p>Your permit request for <strong>{{ $json.workType }}</strong> at <strong>{{ $json.location }}</strong> has been <span style='color:#dc3545;font-weight:bold;'>BLOCKED</span> due to a conflict in the permit register.</p>\n<hr>\n<h3 style='color:#dc3545;'>\u26a0\ufe0f Conflict Details</h3>\n<p><strong>Reason:</strong> {{ $json.conflict_reason }}</p>\n<p><strong>Risk Level:</strong> <span style='background:#dc3545;color:white;padding:2px 8px;border-radius:4px;'>{{ $json.risk_level }}</span></p>\n<p><strong>Conflicting Permits:</strong> {{ $json.conflicting_permit_ids.join(', ') }}</p>\n<hr>\n<h3>\ud83d\udccb Recommendation</h3>\n<p>{{ $json.recommendation }}</p>\n<hr>\n<p>Please coordinate with your supervisor <strong>{{ $json.supervisorName }}</strong> and re-submit after the conflict is resolved.</p>\n<p style='color:#666;font-size:12px;'>This is an automated EHS notification. Do not reply to this email.</p>\n</div>\n</body></html>",
        "options": {},
        "subject": "\u26a0\ufe0f Permit Request BLOCKED - Conflict Detected [{{ $json.permitId }}]"
      },
      "typeVersion": 2.1
    },
    {
      "id": "fa7e1491-e708-4651-9228-4af1c0869b4b",
      "name": "Prepare Approval Data1",
      "type": "n8n-nodes-base.code",
      "position": [
        -464,
        -400
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nconst permitId = item.permitId;\nconst startDt = new Date(item.startDateTime);\nconst expiryDt = new Date(item.expiryDateTime);\n\nconst formatDate = (d) => d.toLocaleDateString('en-IN', {day:'2-digit', month:'short', year:'numeric'});\nconst formatTime = (d) => d.toLocaleTimeString('en-IN', {hour:'2-digit', minute:'2-digit', hour12:true});\n\nconst approvalToken = Buffer.from(JSON.stringify({\n  permitId: permitId,\n  action: 'approve',\n  ts: Date.now()\n})).toString('base64');\n\nconst rejectToken = Buffer.from(JSON.stringify({\n  permitId: permitId,\n  action: 'reject',\n  ts: Date.now()\n})).toString('base64');\n\nreturn [{\n  json: {\n    ...item,\n    startFormatted: formatDate(startDt) + ' ' + formatTime(startDt),\n    expiryFormatted: formatDate(expiryDt) + ' ' + formatTime(expiryDt),\n    approvalToken,\n    rejectToken,\n    approvalUrl: `https://YOUR-N8N-DOMAIN/webhook/permit-decision?token=${approvalToken}`,\n    rejectUrl: `https://YOUR-N8N-DOMAIN/webhook/permit-decision?token=${rejectToken}`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d4573bc2-d2cd-4826-bab3-a04be8903f2d",
      "name": "Send Approval Request to Supervisor",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -192,
        -400
      ],
      "parameters": {
        "sendTo": "=[SUPERVISOR-EMAIL]",
        "message": "=<html><body style='font-family:Arial,sans-serif;max-width:600px;margin:0 auto;'>\n<div style='background:#1a5276;color:white;padding:20px;border-radius:8px 8px 0 0;'>\n<h2>\ud83d\udd10 Work Permit Approval Required</h2>\n<p style='margin:0;font-size:14px;'>Permit ID: <strong>{{ $json.permitId }}</strong></p>\n</div>\n<div style='background:#f8f9fa;border:1px solid #dee2e6;padding:20px;border-radius:0 0 8px 8px;'>\n<p>Dear <strong>{{ $json.supervisorName }}</strong>,</p>\n<p>A work permit request requires your approval before work can commence.</p>\n\n<table style='width:100%;border-collapse:collapse;margin:16px 0;'>\n<tr style='background:#1a5276;color:white;'><th colspan='2' style='padding:10px;text-align:left;'>\ud83d\udccb Permit Details</th></tr>\n<tr style='background:#eaf4fb;'><td style='padding:8px 12px;font-weight:bold;width:40%;'>Permit ID</td><td style='padding:8px 12px;'>{{ $json.permitId }}</td></tr>\n<tr><td style='padding:8px 12px;font-weight:bold;'>Worker</td><td style='padding:8px 12px;'>{{ $json.workerName }}</td></tr>\n<tr style='background:#eaf4fb;'><td style='padding:8px 12px;font-weight:bold;'>Work Type</td><td style='padding:8px 12px;'>\u26a0\ufe0f {{ $json.workType }}</td></tr>\n<tr><td style='padding:8px 12px;font-weight:bold;'>Location</td><td style='padding:8px 12px;'>\ud83d\udccd {{ $json.location }}</td></tr>\n<tr style='background:#eaf4fb;'><td style='padding:8px 12px;font-weight:bold;'>Start Time</td><td style='padding:8px 12px;'>\ud83d\udd50 {{ $json.startFormatted }}</td></tr>\n<tr><td style='padding:8px 12px;font-weight:bold;'>Expiry</td><td style='padding:8px 12px;'>\u23f0 {{ $json.expiryFormatted }}</td></tr>\n<tr style='background:#eaf4fb;'><td style='padding:8px 12px;font-weight:bold;'>Risk Level</td><td style='padding:8px 12px;'>{{ $json.risk_level }}</td></tr>\n<tr><td style='padding:8px 12px;font-weight:bold;'>Work Description</td><td style='padding:8px 12px;'>{{ $json.workDescription }}</td></tr>\n</table>\n\n<div style='text-align:center;margin:24px 0;'>\n<a href='{{ $json.approvalUrl }}' style='background:#28a745;color:white;padding:14px 32px;text-decoration:none;border-radius:6px;font-size:16px;font-weight:bold;margin-right:12px;'>\u2705 APPROVE</a>\n<a href='{{ $json.rejectUrl }}' style='background:#dc3545;color:white;padding:14px 32px;text-decoration:none;border-radius:6px;font-size:16px;font-weight:bold;'>\u274c REJECT</a>\n</div>\n\n<p style='background:#fff3cd;border:1px solid #ffc107;padding:12px;border-radius:4px;font-size:13px;'>\n\u26a0\ufe0f <strong>Safety Reminder:</strong> By approving this permit, you confirm that the work area has been inspected, required PPE is available, and all safety precautions are in place.\n</p>\n<p style='color:#666;font-size:12px;'>This permit will auto-expire at {{ $json.expiryFormatted }}. You will receive a closure reminder.</p>\n</div>\n</body></html>",
        "options": {},
        "subject": "\ud83d\udd14 Permit Approval Required: {{ $json.workType }} - {{ $json.location }} [{{ $json.permitId }}]",
        "operation": "sendAndWait"
      },
      "typeVersion": 2.1
    },
    {
      "id": "27fed27b-358b-4430-8756-b4505b75e819",
      "name": "Evaluate Permit Expiry Status",
      "type": "n8n-nodes-base.code",
      "position": [
        -1808,
        -48
      ],
      "parameters": {
        "jsCode": "const now = new Date();\nconst items = $input.all();\nconst toExpire = [];\nconst toRemind = [];\n\n// Parse \"11-Jun-2026 06:34\" format safely\nfunction parseDate(str) {\n  if (!str) return null;\n  // Convert \"11-Jun-2026 06:34\" \u2192 \"11 Jun 2026 06:34\"\n  return new Date(str.replace('-', ' ').replace('-', ' '));\n}\n\nfor (const item of items) {\n  // Skip non-Approved permits\n  const status = item.json['Status'];\n  if (status !== 'Approved') continue;\n\n  const expiry = parseDate(item.json['Expiry Date & Time']);\n  if (!expiry || isNaN(expiry)) continue;\n\n  const diffMs = expiry - now;\n  const diffMin = diffMs / 60000;\n\n  if (diffMs <= 0) {\n    toExpire.push(item.json);\n  } else if (diffMin <= 30 && diffMin > 0) {\n    toRemind.push(item.json);\n  }\n}\n\nreturn [\n  ...toExpire.map(p => ({ json: { ...p, action: 'expire' } })),\n  ...toRemind.map(p => ({ json: { ...p, action: 'remind' } }))\n];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "9a53892d-0db9-407e-a42a-0f8c24e8e8aa",
  "connections": {
    "Work Permit Form": {
      "main": [
        [
          {
            "node": "Log Permit Submission to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On Workflow Error": {
      "main": [
        [
          {
            "node": "Slack \u2013 Send Error Alert1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Conflict Detected?": {
      "main": [
        [
          {
            "node": "Send Conflict Block Email to Worker",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Approval Data1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Approval Data1": {
      "main": [
        [
          {
            "node": "Send Approval Request to Supervisor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Active Permits": {
      "main": [
        [
          {
            "node": "GPT-4o Permit Conflict Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse GPT Conflict Response": {
      "main": [
        [
          {
            "node": "Conflict Detected?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4o Permit Conflict Check": {
      "main": [
        [
          {
            "node": "Parse GPT Conflict Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route on Supervisor Decision": {
      "main": [
        [
          {
            "node": "Update Permit Log \u2013 Approved",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Permit Log \u2013 Rejected",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Permit Expiry Status": {
      "main": [
        [
          {
            "node": "Route on Expiry or 30-min Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Permit HTML Document": {
      "main": [
        [
          {
            "node": "Email Approved Permit to Worker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Permit Submission to Sheet": {
      "main": [
        [
          {
            "node": "Fetch Active Permits for Conflict Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Permit Log \u2013 Approved": {
      "main": [
        [
          {
            "node": "Generate Permit HTML Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expiry Scheduler (Every 15 min)": {
      "main": [
        [
          {
            "node": "Fetch Approved Permits from Register",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route on Expiry or 30-min Reminder": {
      "main": [
        [
          {
            "node": "Send Permit Expiry Notification",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send 30-min Expiry Reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Approval Request to Supervisor": {
      "main": [
        [
          {
            "node": "Route on Supervisor Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Approved Permits from Register": {
      "main": [
        [
          {
            "node": "Evaluate Permit Expiry Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Active Permits for Conflict Check": {
      "main": [
        [
          {
            "node": "Aggregate Active Permits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}