AutomationFlowsAI & RAG › Analyze Support Screenshots with Uploadtourl, Gpt-4o Vision, Zendesk, and Jira

Analyze Support Screenshots with Uploadtourl, Gpt-4o Vision, Zendesk, and Jira

ByJitesh Dugar @jiteshdugar on n8n.io

Eliminate the manual chaos of HR and legal document management. This workflow automates the transition from a raw document upload to a structured, audit-ready archive by combining UploadToURL for instant CDN hosting, Google Drive for long-term storage, and Airtable for status…

Webhook trigger★★★★☆ complexityAI-powered22 nodesN8N Nodes UploadtourlOpenAIZendeskJiraSlack
AI & RAG Trigger: Webhook Nodes: 22 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Jira → Slack 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
{
  "nodes": [
    {
      "id": "213a081f-853e-4f1a-a72c-cef27817a89b",
      "name": "\ud83d\udccb Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -272
      ],
      "parameters": {
        "width": 650,
        "height": 488,
        "content": "Analyze support screenshots with UploadToURL, OpenAI Vision, and Zendesk/Jira\nThe Problem: Large image attachments clog support inboxes and often reach developers without technical context, leading to slow resolution times.\nThe Solution: A support-to-dev pipeline that hosts visual proof via UploadToURL, uses AI Vision to transcribe errors, and syncs the data to Zendesk or Jira.\n\n\u2699\ufe0f How it Works\nWebhook: Receives a screenshot/video (Binary or URL) and Ticket ID.\n\nUploadToURL: Hosts the file instantly and returns a public CDN link.\n\nGPT-4o Vision: Analyzes the image to identify error messages and UI states.\n\nTicket Update: Attaches the link and AI analysis to the relevant Zendesk or Jira ticket.\n\n\ud83d\udd10 Credentials & Setup\nNode: Install n8n-nodes-uploadtourl via Community Nodes.\n\nAPIs: UploadToURL, OpenAI (Vision), and Zendesk/Jira.\n\nVariables: Set ZENDESK_SUBDOMAIN or JIRA_BASE_URL."
      },
      "typeVersion": 1
    },
    {
      "id": "6591ef98-d557-430e-9c61-e5ee20217487",
      "name": "Entry & Upload",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 984,
        "height": 727,
        "content": "## \ud83d\udeaa Entry, Validation & Upload\n**Nodes:** Webhook \u2192 Validate & Enrich \u2192 Has Remote URL? \u2192 Upload to URL (\u00d72) \u2192 Extract CDN URL\n\n- Accepts `POST` with screenshot/video + ticket metadata. Supports both `fileUrl` (remote) and binary multipart upload\n- Validates ticket ID format per platform (Zendesk: numeric, Jira: `PROJECT-123` pattern), sanitises all string inputs\n- Detects file type from extension \u2014 allowlist: `png`, `jpg`, `jpeg`, `gif`, `webp`, `mp4`, `mov`; rejects all others with `400`\n- Native **Upload to URL** node handles both paths \u2014 no custom HTTP node needed\n- `Extract CDN URL` normalises the response shape and force-upgrades to HTTPS"
      },
      "typeVersion": 1
    },
    {
      "id": "f74f9f4a-bf0f-4c8d-ace7-e0ad44cd3cdb",
      "name": "AI Vision Analysis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1712,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 456,
        "height": 551,
        "content": "## \ud83e\udd16 GPT-4o Vision Analysis\n**Nodes:** GPT-4o Vision \u2192 Parse AI Analysis \u2192 Determine Severity\n\n- Sends the hosted CDN image URL directly to GPT-4o Vision \u2014 no base64 encoding, no file passing\n- Prompt instructs the model to return structured JSON: `errorSummary`, `visibleErrorMessage`, `affectedComponent`, `browserOrOS`, `reproducibilityHint`, `developerNotes`, `suggestedPriority`, `detectedKeywords[]`, and `confidenceScore`\n- `Parse AI Analysis` validates JSON, falls back gracefully if vision confidence is low\n- `Determine Severity` maps AI keywords (`crash`, `500`, `null`, `payment`, `data loss`) to `critical | high | medium | low` \u2014 overrides AI suggestion if hard keywords are detected"
      },
      "typeVersion": 1
    },
    {
      "id": "2dc97cc4-91d7-4af2-8f00-e0cc03aef66c",
      "name": "Platform Routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2192,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 840,
        "height": 695,
        "content": "## \ud83c\udfab Platform Routing \u2014 Zendesk & Jira\n**Nodes:** Route by Platform \u2192 Zendesk Add Comment \u2192 Jira Add Comment \u2192 Jira Attach File\n\n- Switch node routes on `platform` field (`zendesk` or `jira`) \u2014 add more outputs for Linear, GitHub Issues, Freshdesk, etc.\n- **Zendesk:** Posts a rich internal note with inline image embed (`![screenshot](url)`), full AI analysis table, severity badge, and a collapsible developer notes section. Also updates ticket tags with `visual-proof` and the severity label\n- **Jira:** Adds a formatted comment in Jira wiki markup with the CDN image URL, AI breakdown, and affected component. A second API call attaches the raw file to the issue for download"
      },
      "typeVersion": 1
    },
    {
      "id": "e65a544e-9cc2-4938-b420-8f28a7cd5e70",
      "name": "Escalation & Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3072,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 680,
        "height": 744,
        "content": "## \ud83d\udce3 Severity Escalation & Response\n**Nodes:** IF Critical/High? \u2192 Slack Alert \u2192 Build Final Response \u2192 Respond to Webhook\n\n- IF node checks severity \u2014 only `critical` and `high` tickets trigger the Slack alert\n- Slack message includes: ticket ID, customer name, product area, AI error summary, severity badge, CDN image link, and a direct link to the ticket\n- `Build Final Response` assembles the full enriched summary: ticket URL, CDN URL, AI analysis, severity, detected keywords, and agent metadata\n- Returns `200 OK` with the complete ticket enrichment record \u2014 ready to log or forward to a dashboard"
      },
      "typeVersion": 1
    },
    {
      "id": "4e572be6-268a-483e-990d-99f91d7afb1b",
      "name": "Webhook - Receive Screenshot",
      "type": "n8n-nodes-base.webhook",
      "position": [
        704,
        272
      ],
      "parameters": {
        "path": "visual-proof-ticket",
        "options": {
          "allowedOrigins": "*"
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "be6cd236-30b0-4564-84e0-a294a9da87fe",
      "name": "Validate & Enrich Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        928,
        272
      ],
      "parameters": {
        "jsCode": "const body = $input.first().json.body || $input.first().json;\n\n// \u2500\u2500 Platform validation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst allowedPlatforms = ['zendesk', 'jira'];\nconst platform = (body.platform || 'zendesk').toLowerCase();\nif (!allowedPlatforms.includes(platform)) {\n  throw new Error(`Invalid platform \"${platform}\". Must be: zendesk | jira`);\n}\n\n// \u2500\u2500 File source check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (!body.fileUrl && !body.filename) {\n  throw new Error('Provide either fileUrl (remote screenshot) or filename (for binary upload).');\n}\n\n// \u2500\u2500 Ticket ID validation per platform \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst rawTicketId = String(body.ticketId || '').trim();\nif (!rawTicketId) throw new Error('ticketId is required.');\n\nlet ticketId = rawTicketId;\nif (platform === 'zendesk') {\n  if (!/^\\d+$/.test(rawTicketId.replace(/^ZD-/i, ''))) {\n    throw new Error('Zendesk ticket ID must be numeric (e.g. 10482 or ZD-10482).');\n  }\n  ticketId = rawTicketId.replace(/^ZD-/i, '');\n}\nif (platform === 'jira') {\n  if (!/^[A-Z]+-\\d+$/.test(rawTicketId.toUpperCase())) {\n    throw new Error('Jira issue key must match pattern PROJECT-123 (e.g. SUP-482).');\n  }\n  ticketId = rawTicketId.toUpperCase();\n}\n\n// \u2500\u2500 Filename & extension allowlist \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst filename = body.filename || body.fileUrl?.split('?')[0].split('/').pop() || 'screenshot.png';\nconst ext = filename.split('.').pop()?.toLowerCase() || 'png';\nconst allowedExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'mp4', 'mov'];\nif (!allowedExts.includes(ext)) {\n  throw new Error(`File type .${ext} not allowed. Accepted: ${allowedExts.join(', ')}`);\n}\n\n// \u2500\u2500 MIME map \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst mimeMap = {\n  png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',\n  gif: 'image/gif', webp: 'image/webp',\n  mp4: 'video/mp4', mov: 'video/quicktime'\n};\nconst mimeType = mimeMap[ext] || 'application/octet-stream';\nconst isVideo = ['mp4', 'mov'].includes(ext);\n\n// \u2500\u2500 Sanitise string fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sanitise = (s) => String(s || '').trim().slice(0, 500);\n\n// \u2500\u2500 Structured filename for attachment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst ts = new Date().toISOString().split('T')[0];\nconst structuredFilename = `${ticketId}_screenshot_${ts}.${ext}`;\n\nreturn [{\n  json: {\n    // Source\n    fileUrl: body.fileUrl || null,\n    filename,\n    structuredFilename,\n    mimeType,\n    isVideo,\n    ext,\n    // Routing\n    platform,\n    ticketId,\n    // People\n    customerName: sanitise(body.customerName),\n    customerEmail: sanitise(body.customerEmail),\n    agentName: sanitise(body.agentName),\n    // Context\n    productArea: sanitise(body.productArea) || 'Unknown',\n    userDescription: sanitise(body.description),\n    // Config\n    notifyDev: body.notifyDev !== false,\n    submittedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2dc42ca7-3eb9-41ca-bcad-bf0f89fadb66",
      "name": "Has Remote URL?",
      "type": "n8n-nodes-base.if",
      "position": [
        1152,
        272
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-fileurl",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.fileUrl }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "dcfa2c80-9351-4a88-b3e5-dbd43addbf1f",
      "name": "Upload to URL - Remote",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        1360,
        144
      ],
      "parameters": {
        "operation": "uploadFile"
      },
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "691eb389-788e-470c-8a69-2df6efb7bcf7",
      "name": "Upload to URL - Binary",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        1360,
        384
      ],
      "parameters": {
        "operation": "uploadFile"
      },
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f4e80de1-dbdb-4eb8-8732-12dc81e6c3a5",
      "name": "Extract CDN URL",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        272
      ],
      "parameters": {
        "jsCode": "const uploadResp = $input.first().json;\nconst meta = $('Validate & Enrich Payload').first().json;\n\nconst cdnUrl =\n  uploadResp.url ||\n  uploadResp.link ||\n  uploadResp.data?.url ||\n  uploadResp.file?.url ||\n  uploadResp.shortUrl;\n\nif (!cdnUrl) {\n  throw new Error('Upload to URL returned no public URL. Raw: ' + JSON.stringify(uploadResp).slice(0, 400));\n}\n\nreturn [{\n  json: {\n    ...meta,\n    cdnUrl: cdnUrl.replace(/^http:\\/\\//, 'https://'),\n    uploadId: uploadResp.id || uploadResp.data?.id || null,\n    fileSizeBytes: uploadResp.size || uploadResp.data?.size || null\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "360a2173-f68d-441c-b7ed-cb7a0a031a32",
      "name": "GPT-4o Vision - Analyse Screenshot",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "notes": "Passes the CDN URL to GPT-4o Vision. Returns structured error analysis: visible error message, affected component, browser/OS, developer notes, suggested priority, and keywords.",
      "position": [
        1760,
        272
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "ft:gpt-3.5-turbo-0125:bar-juice::91x6k9Fc",
          "cachedResultName": "FT:GPT-3.5-TURBO-0125:BAR-JUICE::91X6K9FC"
        },
        "options": {
          "maxTokens": 1000,
          "temperature": 0.3
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You are a senior software QA engineer and technical support specialist. Analyse the provided screenshot or screen recording URL and produce a structured developer-ready bug report. Return ONLY valid JSON \u2014 no markdown, no preamble."
            },
            {
              "content": "=Analyse this customer support screenshot.\n\nImage/Video URL: {{ $json.cdnUrl }}\nProduct Area: {{ $json.productArea }}\nCustomer Description: {{ $json.userDescription || 'No description provided' }}\nPlatform: {{ $json.platform }}\nTicket ID: {{ $json.ticketId }}\n\nReturn ONLY this JSON:\n{\n  \"errorSummary\": \"One-sentence plain English summary of what the customer is experiencing\",\n  \"visibleErrorMessage\": \"Exact error text visible in the screenshot, or null if none\",\n  \"errorCode\": \"HTTP status code or app error code if visible, or null\",\n  \"affectedComponent\": \"Specific UI component, page, or feature affected\",\n  \"affectedUrl\": \"URL visible in browser address bar if present, or null\",\n  \"browserOrOS\": \"Browser name/version or OS if identifiable from screenshot, or null\",\n  \"reproducibilityHint\": \"What state the UI appears to be in that might help reproduce the bug\",\n  \"developerNotes\": \"Technical observations: console errors visible, network state, loading indicators, broken elements\",\n  \"suggestedPriority\": \"critical|high|medium|low\",\n  \"detectedKeywords\": [\"array\", \"of\", \"technical\", \"keywords\", \"found\"],\n  \"suggestedLabels\": [\"frontend\", \"checkout\", \"etc\"],\n  \"isScreenRecording\": false,\n  \"confidenceScore\": 0.92\n}"
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "c962207d-40fd-4def-a363-a7bf6e9873fa",
      "name": "Parse AI Analysis & Compute Severity",
      "type": "n8n-nodes-base.code",
      "position": [
        2032,
        272
      ],
      "parameters": {
        "jsCode": "const aiRaw = $input.first().json;\nconst meta = $('Extract CDN URL').first().json;\n\n// \u2500\u2500 Parse AI JSON \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet ai;\ntry {\n  const raw =\n    aiRaw.message?.content ||\n    aiRaw.choices?.[0]?.message?.content ||\n    aiRaw.content ||\n    aiRaw.text;\n  ai = typeof raw === 'string' ? JSON.parse(raw) : raw;\n} catch (e) {\n  throw new Error('Failed to parse GPT-4o Vision JSON: ' + e.message);\n}\n\n// \u2500\u2500 Severity override logic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Hard keywords always escalate to critical regardless of AI suggestion\nconst criticalKeywords = ['crash', 'data loss', 'payment failed', 'stripe', 'checkout broken', '500', '503', 'null pointer', 'undefined', 'white screen', 'blank page'];\nconst highKeywords = ['404', 'timeout', 'login failed', 'auth', 'permission denied', 'infinite loop', 'spinner'];\n\nconst allText = [\n  ai.errorSummary || '',\n  ai.visibleErrorMessage || '',\n  ai.developerNotes || '',\n  ...(ai.detectedKeywords || [])\n].join(' ').toLowerCase();\n\nlet severity = ai.suggestedPriority || 'medium';\nif (criticalKeywords.some(kw => allText.includes(kw))) severity = 'critical';\nelse if (highKeywords.some(kw => allText.includes(kw)) && severity !== 'critical') severity = 'high';\n\n// \u2500\u2500 Emoji badge for ticket comments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst severityBadge = { critical: '\ud83d\udd34 CRITICAL', high: '\ud83d\udfe0 HIGH', medium: '\ud83d\udfe1 MEDIUM', low: '\ud83d\udfe2 LOW' };\n\nreturn [{\n  json: {\n    ...meta,\n    // AI analysis\n    errorSummary: ai.errorSummary || 'Unable to determine error from screenshot.',\n    visibleErrorMessage: ai.visibleErrorMessage || null,\n    errorCode: ai.errorCode || null,\n    affectedComponent: ai.affectedComponent || meta.productArea,\n    affectedUrl: ai.affectedUrl || null,\n    browserOrOS: ai.browserOrOS || null,\n    reproducibilityHint: ai.reproducibilityHint || null,\n    developerNotes: ai.developerNotes || null,\n    detectedKeywords: ai.detectedKeywords || [],\n    suggestedLabels: ai.suggestedLabels || [],\n    isScreenRecording: ai.isScreenRecording || false,\n    confidenceScore: ai.confidenceScore || null,\n    // Computed severity\n    severity,\n    severityBadge: severityBadge[severity],\n    // Ticket comment body (pre-built for both platforms)\n    richComment: `## \ud83d\udcf8 Visual Proof Attached\\n\\n**${severityBadge[severity]}** | Ticket: ${meta.ticketId} | Product Area: ${meta.productArea}\\n\\n### \ud83d\udd17 Screenshot\\n![Customer Screenshot](${meta.cdnUrl})\\n[Direct Link](${meta.cdnUrl})\\n\\n### \ud83e\udd16 AI Error Analysis\\n| Field | Value |\\n|---|---|\\n| **Error Summary** | ${ai.errorSummary || 'N/A'} |\\n| **Visible Error** | \\`${ai.visibleErrorMessage || 'None detected'}\\` |\\n| **Error Code** | ${ai.errorCode || 'N/A'} |\\n| **Affected Component** | ${ai.affectedComponent || 'N/A'} |\\n| **Affected URL** | ${ai.affectedUrl || 'N/A'} |\\n| **Browser / OS** | ${ai.browserOrOS || 'Not identified'} |\\n\\n### \ud83d\udee0 Developer Notes\\n${ai.developerNotes || 'No additional technical observations.'}\\n\\n### \ud83d\udd01 Reproducibility\\n${ai.reproducibilityHint || 'Unknown \u2014 see screenshot for UI state.'}\\n\\n**Labels:** ${(ai.suggestedLabels || []).join(', ')} | **Keywords:** ${(ai.detectedKeywords || []).join(', ')}\\n**Filed by:** ${meta.agentName || 'Support System'} | **Customer:** ${meta.customerName} (${meta.customerEmail})`,\n    jiraComment: `h2. \ud83d\udcf8 Visual Proof Attached\\n\\n*${severityBadge[severity]}* | Ticket: ${meta.ticketId}\\n\\n*Screenshot:* [View Image|${meta.cdnUrl}]\\n!${meta.cdnUrl}|thumbnail!\\n\\nh3. AI Error Analysis\\n||Field||Value||\\n|Error Summary|${ai.errorSummary || 'N/A'}|\\n|Visible Error|{{${ai.visibleErrorMessage || 'None'}}}|\\n|Error Code|${ai.errorCode || 'N/A'}|\\n|Affected Component|${ai.affectedComponent || 'N/A'}|\\n|Browser/OS|${ai.browserOrOS || 'N/A'}|\\n\\nh3. Developer Notes\\n${ai.developerNotes || 'No additional observations.'}\\n\\nh3. Reproducibility\\n${ai.reproducibilityHint || 'Unknown.'}\\n\\n*Filed by:* ${meta.agentName || 'Support System'} | *Customer:* ${meta.customerName}`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f351e33b-f202-4a95-9914-a822c39f31cb",
      "name": "Route by Platform",
      "type": "n8n-nodes-base.switch",
      "position": [
        2240,
        272
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Zendesk",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.platform }}",
                    "rightValue": "zendesk"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Jira",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.platform }}",
                    "rightValue": "jira"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "6d214aa5-6e58-40f2-9b88-32a7ff367aa2",
      "name": "Zendesk - Add Internal Note",
      "type": "n8n-nodes-base.zendesk",
      "notes": "Posts a rich internal note with inline image embed, AI analysis table, developer notes, and reproducibility hints. Tags ticket with severity and visual-proof labels.",
      "position": [
        2464,
        128
      ],
      "parameters": {
        "id": "={{ $json.ticketId }}",
        "operation": "update",
        "updateFields": {
          "tags": "visual-proof,ai-analysed,{{ $json.severity }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5cf40aa1-8d93-4afe-971a-0f4e2e253c2b",
      "name": "Jira - Update Issue Labels",
      "type": "n8n-nodes-base.jira",
      "notes": "Updates Jira issue with AI-generated summary and labels before adding the comment.",
      "position": [
        2464,
        304
      ],
      "parameters": {
        "issueKey": "={{ $json.ticketId }}",
        "operation": "update",
        "updateFields": {
          "labels": "={{ [...$json.suggestedLabels, 'visual-proof', $json.severity].join(',') }}",
          "summary": "={{ $json.errorSummary }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "33056cb7-c70b-40c3-803e-e559e46bb6ff",
      "name": "Jira - Add Comment",
      "type": "n8n-nodes-base.jira",
      "notes": "Adds a Jira wiki-markup formatted comment with image thumbnail, AI analysis table, developer notes, and reproducibility hints.",
      "position": [
        2688,
        304
      ],
      "parameters": {
        "resource": "issueComment",
        "operation": "create"
      },
      "typeVersion": 1
    },
    {
      "id": "5e6b35e0-36a3-4c1e-8575-7b052141d34f",
      "name": "Merge Platform Response",
      "type": "n8n-nodes-base.code",
      "position": [
        2912,
        240
      ],
      "parameters": {
        "jsCode": "// Normalise response from both Zendesk and Jira branches\nconst platformResp = $input.first().json;\nconst data = $('Parse AI Analysis & Compute Severity').first().json;\n\nconst isZendesk = data.platform === 'zendesk';\n\n// Build ticket URL\nconst ticketUrl = isZendesk\n  ? `https://${$vars.ZENDESK_SUBDOMAIN || 'your-domain'}.zendesk.com/agent/tickets/${data.ticketId}`\n  : `${$vars.JIRA_BASE_URL || 'https://your-domain.atlassian.net'}/browse/${data.ticketId}`;\n\nreturn [{\n  json: {\n    ...data,\n    ticketUrl,\n    platformCommentId:\n      platformResp.comment?.id ||\n      platformResp.audit?.events?.[0]?.id ||\n      platformResp.id ||\n      null\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "aa43cb6b-3c03-4db2-ae4d-fa32a14ca178",
      "name": "IF Critical or High?",
      "type": "n8n-nodes-base.if",
      "position": [
        3120,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-severity",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.severity }}",
              "rightValue": "medium"
            },
            {
              "id": "cond-notify",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.notifyDev }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ab65d1aa-8fc7-45c5-941d-cd056a3aff2d",
      "name": "Slack - Escalate to Dev Channel",
      "type": "n8n-nodes-base.slack",
      "notes": "Only fires for critical and high severity tickets when notifyDev is true. Posts to the configured dev channel with full error context, CDN image link, and direct ticket URL.",
      "position": [
        3344,
        128
      ],
      "parameters": {
        "text": "={{ $json.severityBadge }} *New Visual Proof Ticket Escalated*\n\n*Ticket:* <{{ $json.ticketUrl }}|{{ $json.ticketId }}> | *Product Area:* {{ $json.productArea }}\n*Customer:* {{ $json.customerName }} ({{ $json.customerEmail }})\n*Agent:* {{ $json.agentName }}\n\n*Error Summary:* {{ $json.errorSummary }}\n*Visible Error:* `{{ $json.visibleErrorMessage || 'None detected' }}`\n*Affected Component:* {{ $json.affectedComponent }}\n*Browser/OS:* {{ $json.browserOrOS || 'Unknown' }}\n\n:frame_with_picture: <{{ $json.cdnUrl }}|View Screenshot>\n:jira: <{{ $json.ticketUrl }}|Open Ticket>\n\n_Keywords: {{ $json.detectedKeywords.join(', ') }} | AI confidence: {{ Math.round(($json.confidenceScore || 0) * 100) }}%_",
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9569c065-e78b-4dd7-bb55-62a4cc8d860c",
      "name": "Build Final Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3344,
        352
      ],
      "parameters": {
        "jsCode": "const slackResp = $input.first();\nconst data = $('Merge Platform Response').first().json;\n\nreturn [{\n  json: {\n    success: true,\n    message: `Visual proof attached to ${data.platform} ticket ${data.ticketId} with severity ${data.severity}.`,\n    // Ticket\n    platform: data.platform,\n    ticketId: data.ticketId,\n    ticketUrl: data.ticketUrl,\n    platformCommentId: data.platformCommentId,\n    // Asset\n    cdnUrl: data.cdnUrl,\n    structuredFilename: data.structuredFilename,\n    fileSizeBytes: data.fileSizeBytes,\n    isScreenRecording: data.isScreenRecording,\n    // AI Analysis\n    severity: data.severity,\n    severityBadge: data.severityBadge,\n    errorSummary: data.errorSummary,\n    visibleErrorMessage: data.visibleErrorMessage,\n    errorCode: data.errorCode,\n    affectedComponent: data.affectedComponent,\n    affectedUrl: data.affectedUrl,\n    browserOrOS: data.browserOrOS,\n    developerNotes: data.developerNotes,\n    detectedKeywords: data.detectedKeywords,\n    suggestedLabels: data.suggestedLabels,\n    confidenceScore: data.confidenceScore,\n    // Meta\n    agentName: data.agentName,\n    customerName: data.customerName,\n    customerEmail: data.customerEmail,\n    devNotified: data.notifyDev && ['critical', 'high'].includes(data.severity),\n    submittedAt: data.submittedAt,\n    processedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cb02125b-2d56-4916-ad75-b85ed5b67a80",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3568,
        352
      ],
      "parameters": {
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.1
    }
  ],
  "connections": {
    "Extract CDN URL": {
      "main": [
        [
          {
            "node": "GPT-4o Vision - Analyse Screenshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Remote URL?": {
      "main": [
        [
          {
            "node": "Upload to URL - Remote",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upload to URL - Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Platform": {
      "main": [
        [
          {
            "node": "Zendesk - Add Internal Note",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Jira - Update Issue Labels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Jira - Add Comment": {
      "main": [
        [
          {
            "node": "Merge Platform Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Final Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Critical or High?": {
      "main": [
        [
          {
            "node": "Slack - Escalate to Dev Channel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to URL - Binary": {
      "main": [
        [
          {
            "node": "Extract CDN URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to URL - Remote": {
      "main": [
        [
          {
            "node": "Extract CDN URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Platform Response": {
      "main": [
        [
          {
            "node": "IF Critical or High?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate & Enrich Payload": {
      "main": [
        [
          {
            "node": "Has Remote URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Jira - Update Issue Labels": {
      "main": [
        [
          {
            "node": "Jira - Add Comment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zendesk - Add Internal Note": {
      "main": [
        [
          {
            "node": "Merge Platform Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Screenshot": {
      "main": [
        [
          {
            "node": "Validate & Enrich Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - Escalate to Dev Channel": {
      "main": [
        [
          {
            "node": "Build Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4o Vision - Analyse Screenshot": {
      "main": [
        [
          {
            "node": "Parse AI Analysis & Compute Severity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Analysis & Compute Severity": {
      "main": [
        [
          {
            "node": "Route by Platform",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

Eliminate the manual chaos of HR and legal document management. This workflow automates the transition from a raw document upload to a structured, audit-ready archive by combining UploadToURL for instant CDN hosting, Google Drive for long-term storage, and Airtable for status…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Complete incident workflow from detection through resolution to post-mortem, with full organizational context from Port's catalog. This template handles both incident triggered and resolved events fro

Custom, OpenAI, Slack +1
AI & RAG

Transform raw product images into fully-optimized e-commerce listings in seconds. This workflow automates the bridge between a photo upload and a live product page by combining UploadToURL for hosting

N8N Nodes Uploadtourl, OpenAI, Shopify +2
AI & RAG

Automatically detect and escalate Product UAT critical bugs using AI, create Jira issues, notify engineering teams, and close the feedback loop with testers.

Jira, Slack, Gmail +1
AI & RAG

Customer support teams, SaaS companies, and service businesses that need to quickly identify and respond to urgent customer issues. Perfect for organizations handling high ticket volumes where manual

OpenAI, Zendesk, Slack
AI & RAG

Complete security workflow from vulnerability detection to automated remediation, with severity-based routing and full organizational context from Port's catalog. This template provides end-to-end lif

Custom, OpenAI, Jira +2