{
  "name": "FilePost — Triage support screenshots with OpenAI and Jira",
  "nodes": [
    {
      "parameters": {
        "content": "## Screenshot-to-Jira triage\n\nA support webhook provides a screenshot URL and Jira issue key. The workflow creates a seven-day FilePost link, analyzes the actual image through OpenAI's Responses API, and adds a structured diagnostic comment to Jira.\n\n### Privacy boundary\nFilePost URLs are public-by-link. Do not send screenshots containing passwords, access tokens, health data, or other secrets. The template expires the hosted screenshot after seven days.\n\n### Setup\n- Install and connect `n8n-nodes-filepost`.\n- Add OpenAI Header Auth to **Analyze Screenshot** (`Authorization: Bearer ...`).\n- Add Jira Basic Auth to **Add Jira Diagnostic Comment** (Atlassian email + API token).\n- Set `JIRA_BASE_URL`, for example `https://acme.atlassian.net`.",
        "height": 590,
        "width": 640,
        "color": 5
      },
      "id": "23f95e22-7f1a-49ca-bdd4-4c84736f66f4",
      "name": "Setup and privacy boundary",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-840, -420]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "filepost-support-screenshot-triage",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "79285ba0-38bb-43b1-89a7-2bf31d08741e",
      "name": "Receive Support Screenshot",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-800, 100],
      "webhookId": "filepost-support-triage-template"
    },
    {
      "parameters": {
        "jsCode": "const body = $input.first().json.body || $input.first().json;\nif (!body.screenshot_url) throw new Error('screenshot_url is required');\nconst issueKey = String(body.issue_key || '').trim().toUpperCase();\nif (!/^[A-Z][A-Z0-9_]+-\\d+$/.test(issueKey)) throw new Error('issue_key must look like SUPPORT-123');\nconst screenshotUrl = new URL(body.screenshot_url);\nif (!['http:', 'https:'].includes(screenshotUrl.protocol)) throw new Error('screenshot_url must use http or https');\nconst clean = (value, fallback = '') => String(value || fallback).trim().replace(/[\\r\\n]+/g, ' ').slice(0, 1500);\nreturn [{ json: {\n  screenshotUrl: screenshotUrl.toString(),\n  issueKey,\n  userReport: clean(body.user_report, 'No written report supplied'),\n  productArea: clean(body.product_area, 'Unknown'),\n  browser: clean(body.browser, 'Unknown'),\n  reporter: clean(body.reporter, 'Support workflow'),\n  receivedAt: new Date().toISOString()\n} }];"
      },
      "id": "9c849f59-12de-4942-9747-b3948e9f1ba5",
      "name": "Validate Support Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-560, 100]
    },
    {
      "parameters": {
        "url": "={{ $json.screenshotUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "id": "b1091f90-d8a8-4edb-a796-f3537d31599b",
      "name": "Download Screenshot",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-320, 100]
    },
    {
      "parameters": {
        "operation": "upload",
        "binaryPropertyName": "data",
        "expiresIn": "7d",
        "filenameMode": "download"
      },
      "id": "4d2c42cc-450c-43b4-99dd-625a61ee0372",
      "name": "Create Seven-Day FilePost Link",
      "type": "n8n-nodes-filepost.filePost",
      "typeVersion": 1,
      "position": [-80, 100]
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Validate Support Request').first().json;\nconst hosted = $input.first().json;\nconst prompt = `Act as a senior support engineer. Analyze the screenshot and the user's report. Return JSON with summary, visible_error, affected_component, likely_cause, reproduction_hint, severity (critical, high, medium, or low), and next_steps (array). Do not invent text that is not visible; use null when unknown. User report: ${meta.userReport}. Product area: ${meta.productArea}. Browser: ${meta.browser}.`;\nreturn [{ json: { requestBody: {\n  model: 'gpt-5.6-luna',\n  input: [{ role: 'user', content: [\n    { type: 'input_text', text: prompt },\n    { type: 'input_image', image_url: hosted.url, detail: 'high' }\n  ] }],\n  text: { format: { type: 'json_object' } }\n}, screenshotUrl: hosted.url, filepostFileId: hosted.file_id, expiresAt: hosted.expires_at } }];"
      },
      "id": "a9052714-4eb0-48a6-a257-fde295721216",
      "name": "Build Screenshot Analysis Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [160, 100]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/responses",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.requestBody) }}",
        "options": {}
      },
      "id": "518f6578-f4c9-47a3-986c-94e3c986894f",
      "name": "Analyze Screenshot",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [400, 100]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst outputText = response.output_text || (response.output || []).flatMap((item) => item.content || []).find((part) => part.type === 'output_text')?.text;\nif (!outputText) throw new Error('OpenAI returned no output_text');\nlet analysis;\ntry { analysis = JSON.parse(outputText); } catch (error) { throw new Error(`OpenAI returned invalid JSON: ${error.message}`); }\nconst meta = $('Validate Support Request').first().json;\nconst hosted = $('Build Screenshot Analysis Request').first().json;\nconst severity = ['critical', 'high', 'medium', 'low'].includes(String(analysis.severity).toLowerCase()) ? String(analysis.severity).toLowerCase() : 'medium';\nconst nextSteps = Array.isArray(analysis.next_steps) ? analysis.next_steps.map(String).slice(0, 8) : [];\nconst comment = [\n  `Automated screenshot triage — ${severity.toUpperCase()}`,\n  `Summary: ${analysis.summary || 'No summary returned'}`,\n  `Visible error: ${analysis.visible_error || 'None detected'}`,\n  `Affected component: ${analysis.affected_component || meta.productArea}`,\n  `Likely cause: ${analysis.likely_cause || 'Unknown'}`,\n  `Reproduction hint: ${analysis.reproduction_hint || 'Unknown'}`,\n  `Suggested next steps: ${nextSteps.length ? nextSteps.join('; ') : 'Review manually'}`,\n  `Screenshot (expires in seven days): ${hosted.screenshotUrl}`,\n  `Original report: ${meta.userReport}`,\n  `Submitted by: ${meta.reporter} at ${meta.receivedAt}`\n].join('\\n\\n');\nreturn [{ json: { ...meta, ...analysis, severity, nextSteps, comment, screenshotUrl: hosted.screenshotUrl, filepostFileId: hosted.filepostFileId, expiresAt: hosted.expiresAt } }];"
      },
      "id": "7d8a8e47-a879-42b7-8a85-1a993d9829ca",
      "name": "Build Jira Comment",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [640, 100]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $vars.JIRA_BASE_URL }}/rest/api/3/issue/{{ $json.issueKey }}/comment",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ body: { type: 'doc', version: 1, content: $json.comment.split('\\n\\n').map((text) => ({ type: 'paragraph', content: [{ type: 'text', text }] })) } }) }}",
        "options": {}
      },
      "id": "90e9051e-ab42-491b-84b3-a61c0764d29e",
      "name": "Add Jira Diagnostic Comment",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [880, 100]
    },
    {
      "parameters": {
        "jsCode": "const jira = $input.first().json;\nconst result = $('Build Jira Comment').first().json;\nreturn [{ json: {\n  success: true,\n  issue_key: result.issueKey,\n  jira_comment_id: jira.id || null,\n  severity: result.severity,\n  summary: result.summary || null,\n  screenshot_url: result.screenshotUrl,\n  screenshot_expires_at: result.expiresAt,\n  filepost_file_id: result.filepostFileId\n} }];"
      },
      "id": "d7247c7b-2d0b-4f4e-b8ca-9c96234372a5",
      "name": "Build Triage Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1120, 100]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {"responseCode": 200}
      },
      "id": "49a5f8a2-d78e-4e36-915f-78c178b59e9c",
      "name": "Return Triage Result",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [1360, 100]
    }
  ],
  "connections": {
    "Receive Support Screenshot": {"main": [[{"node": "Validate Support Request", "type": "main", "index": 0}]]},
    "Validate Support Request": {"main": [[{"node": "Download Screenshot", "type": "main", "index": 0}]]},
    "Download Screenshot": {"main": [[{"node": "Create Seven-Day FilePost Link", "type": "main", "index": 0}]]},
    "Create Seven-Day FilePost Link": {"main": [[{"node": "Build Screenshot Analysis Request", "type": "main", "index": 0}]]},
    "Build Screenshot Analysis Request": {"main": [[{"node": "Analyze Screenshot", "type": "main", "index": 0}]]},
    "Analyze Screenshot": {"main": [[{"node": "Build Jira Comment", "type": "main", "index": 0}]]},
    "Build Jira Comment": {"main": [[{"node": "Add Jira Diagnostic Comment", "type": "main", "index": 0}]]},
    "Add Jira Diagnostic Comment": {"main": [[{"node": "Build Triage Result", "type": "main", "index": 0}]]},
    "Build Triage Result": {"main": [[{"node": "Return Triage Result", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"},
  "versionId": "b54aca77-015f-49e9-ae9f-20bcf827e66d",
  "meta": {"templateCredsSetupCompleted": false},
  "tags": []
}
