{"meta":{"instanceId":"277842713620d9f5554de3b1518b865a152c8c4db680008bd8aec536fc18b4a8"},"nodes":[{"id":"213a081f-853e-4f1a-a72c-cef27817a89b","name":"📋 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⚙️ 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🔐 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":"## 🚪 Entry, Validation & Upload\n**Nodes:** Webhook → Validate & Enrich → Has Remote URL? → Upload to URL (×2) → 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 — allowlist: `png`, `jpg`, `jpeg`, `gif`, `webp`, `mp4`, `mov`; rejects all others with `400`\n- Native **Upload to URL** node handles both paths — 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":"## 🤖 GPT-4o Vision Analysis\n**Nodes:** GPT-4o Vision → Parse AI Analysis → Determine Severity\n\n- Sends the hosted CDN image URL directly to GPT-4o Vision — 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` — 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":"## 🎫 Platform Routing — Zendesk & Jira\n**Nodes:** Route by Platform → Zendesk Add Comment → Jira Add Comment → Jira Attach File\n\n- Switch node routes on `platform` field (`zendesk` or `jira`) — 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":"## 📣 Severity Escalation & Response\n**Nodes:** IF Critical/High? → Slack Alert → Build Final Response → Respond to Webhook\n\n- IF node checks severity — 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 — 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],"webhookId":"d25fa786-d4c4-4942-956d-3912f22a2977","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// ── Platform validation ───────────────────────────────────────\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// ── File source check ─────────────────────────────────────────\nif (!body.fileUrl && !body.filename) {\n  throw new Error('Provide either fileUrl (remote screenshot) or filename (for binary upload).');\n}\n\n// ── Ticket ID validation per platform ────────────────────────\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// ── Filename & extension allowlist ───────────────────────────\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// ── MIME map ──────────────────────────────────────────────────\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// ── Sanitise string fields ────────────────────────────────────\nconst sanitise = (s) => String(s || '').trim().slice(0, 500);\n\n// ── Structured filename for attachment ───────────────────────\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":{"id":"aTtpEWKPdBd8vRfH","name":"Upload to URL account 3"}},"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":{"id":"aTtpEWKPdBd8vRfH","name":"Upload to URL account 3"}},"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 — 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":{"id":"8IkhtT3EbXygnvcr","name":"Mediajade"}},"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// ── Parse AI JSON ─────────────────────────────────────────────\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// ── Severity override logic ───────────────────────────────────\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// ── Emoji badge for ticket comments ──────────────────────────\nconst severityBadge = { critical: '🔴 CRITICAL', high: '🟠 HIGH', medium: '🟡 MEDIUM', low: '🟢 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: `## 📸 Visual Proof Attached\\n\\n**${severityBadge[severity]}** | Ticket: ${meta.ticketId} | Product Area: ${meta.productArea}\\n\\n### 🔗 Screenshot\\n![Customer Screenshot](${meta.cdnUrl})\\n[Direct Link](${meta.cdnUrl})\\n\\n### 🤖 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### 🛠 Developer Notes\\n${ai.developerNotes || 'No additional technical observations.'}\\n\\n### 🔁 Reproducibility\\n${ai.reproducibilityHint || 'Unknown — 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. 📸 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],"webhookId":"a663e398-cbe9-4ad3-a87d-88945ab75896","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":{"id":"RYkcspsGW073XNK0","name":"Mediajade Slack"}},"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}],"pinData":{},"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}]]}}}