{
  "id": "mfj3y9MNPLaxkcwU",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "attachmentsAnalyzer",
  "tags": [],
  "nodes": [
    {
      "id": "e342262d-24d0-4d50-bf5c-4b6df19561e2",
      "name": "About",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        -192
      ],
      "parameters": {
        "color": 5,
        "width": 1336,
        "height": 544,
        "content": "## Overview\n\nSubworkflow for analyzing Mattermost attachments before passing them to the AI Agent.\n\n## Per-attachment behavior\n- **Image** (mime starts with `image/`, size \u2264 `MAX_IMAGE_SIZE_BYTES`): downloaded and analyzed by OpenAI vision model.\n- **Text small** (text-like by mime/extension, size \u2264 `MAX_TEXT_SIZE_BYTES`): downloaded as plain text and inlined into the context block (with line count check \u2264 `MAX_TEXT_LINES`).\n- **Text too large** (text-like, exceeds byte or line limit): replaced with a \"too big\" marker so the agent asks the user for the relevant excerpt.\n- **Other** (binaries, oversized images, unknown): replaced with a generic \"not analyzed\" marker.\n\n## Setup checklist\n1. Set `MATTERMOST_BASE_URL` in the Config node to your Mattermost origin (`https://hostname`; no trailing slash).\n2. Create an `HTTP Header Auth` credential called \"Mattermost PAT\" with:\n   - Name: `Authorization`\n   - Value: `Bearer <your_personal_access_token>`\n3. Attach this credential to all three HTTP Request nodes (Get file info, Download file, Download text file).\n4. Attach your OpenAI credential to the \"OpenAI: Analyze Image\" node.\n5. Tune in Config: `VISION_MODEL` (OpenAI vision model id; the Analyze Image node reads it from Config), `MAX_IMAGE_SIZE_BYTES`, `MAX_TEXT_SIZE_BYTES`, `MAX_TEXT_LINES`.\n\n## Notes\n- The IF guard short-circuits when `file_ids` is empty so the parent workflow can call this subworkflow unconditionally.\n- Classification by mime + extension catches `.log` files served as `application/octet-stream`.\n- Final output is always a single item with the same shape \u2014 safe to consume in the parent."
      },
      "typeVersion": 1
    },
    {
      "id": "bede9898-e490-496a-bf78-2703c31292e5",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        704,
        656
      ],
      "parameters": {
        "inputSource": "jsonExample",
        "jsonExample": "{\n  \"file_ids\": [\"x4t1k7example1\", \"9bzqmexample2\"]\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "2cbd0079-0953-4cfd-ba09-89b130e3e065",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "position": [
        896,
        656
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "c1f+1234567890-+1234567890",
              "name": "MATTERMOST_BASE_URL",
              "type": "string",
              "value": "https://<your-mattermost-host>"
            },
            {
              "id": "c1f+1234567890-+1234567890",
              "name": "VISION_MODEL",
              "type": "string",
              "value": "gpt-4o-mini"
            },
            {
              "id": "c1f+1234567890-+1234567890",
              "name": "MAX_IMAGE_SIZE_BYTES",
              "type": "number",
              "value": 20971520
            },
            {
              "id": "c1f+1234567890-+1234567890",
              "name": "MAX_TEXT_SIZE_BYTES",
              "type": "number",
              "value": 65536
            },
            {
              "id": "c1f+1234567890-+1234567890",
              "name": "MAX_TEXT_LINES",
              "type": "number",
              "value": 300
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "50298b55-4fbf-4052-8859-3ae9f1d73c23",
      "name": "Has attachments?",
      "type": "n8n-nodes-base.if",
      "position": [
        1088,
        656
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "if+1234567890-+1234567890",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ ($json.file_ids || []).length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "7369a49e-7f79-4082-a07b-1ef09b87f6f4",
      "name": "Empty result",
      "type": "n8n-nodes-base.set",
      "position": [
        1296,
        912
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "se+1234567890-+1234567890",
              "name": "attachments_context",
              "type": "string",
              "value": ""
            },
            {
              "id": "se+1234567890-+1234567890",
              "name": "attachments_count",
              "type": "number",
              "value": 0
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "08c9ba1a-6413-4a5c-a129-6fa456d5e907",
      "name": "Split file_ids",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        464
      ],
      "parameters": {
        "jsCode": "// Split incoming file_ids array into one item per file_id.\n// The IF guard upstream guarantees at least one id; defensive check kept.\n\nconst data = $input.first().json;\nconst ids = Array.isArray(data.file_ids) ? data.file_ids : [];\n\nreturn ids.map((file_id, idx) => ({\n  json: {\n    file_id,\n    file_index: idx\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "1e0aae90-4e03-4f2d-9639-0c15bbd7aff8",
      "name": "Get file info",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1552,
        464
      ],
      "parameters": {
        "url": "={{ $('Config').first().json.MATTERMOST_BASE_URL }}/api/v4/files/{{ $json.file_id }}/info",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "1b35f66f-fef6-4e36-9afe-4bd2408cda3c",
      "name": "Classify file",
      "type": "n8n-nodes-base.code",
      "position": [
        1760,
        464
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Classify the attachment by mime type, file extension, and size.\n// Adds a `category` field used by the downstream Switch node:\n//   - 'image'           image, fits MAX_IMAGE_SIZE_BYTES \u2192 vision pipeline\n//   - 'text_small'      text-like, fits MAX_TEXT_SIZE_BYTES \u2192 inline content\n//   - 'text_too_large'  text-like but exceeds MAX_TEXT_SIZE_BYTES \u2192 \"too big\" marker\n//   - 'unsupported'     binaries, oversized images, unknown formats\n//\n// Detecting text by extension is intentional: Mattermost often serves `.log`\n// (and other dev-related files) as `application/octet-stream`.\n\nconst fileInfo = $input.item.json;\nconst config = $('Config').first().json;\n\nconst mimeType = (fileInfo.mime_type || '').toLowerCase();\nconst fileName = fileInfo.name || '';\nconst size = Number(fileInfo.size) || 0;\n\nconst dotIdx = fileName.lastIndexOf('.');\nconst ext = dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : '';\n\nconst TEXT_EXTENSIONS = new Set([\n  // Logs & plain text\n  'log', 'txt', 'out', 'err',\n  // Structured data\n  'json', 'ndjson', 'jsonl', 'yaml', 'yml', 'xml', 'csv', 'tsv', 'toml',\n  // Docs & configs\n  'md', 'markdown', 'rst', 'conf', 'cfg', 'ini', 'env', 'properties',\n  // Shell & scripts\n  'sh', 'bash', 'zsh', 'fish', 'ps1',\n  // Programming languages\n  'py', 'js', 'mjs', 'cjs', 'ts', 'jsx', 'tsx',\n  'go', 'rs', 'java', 'kt', 'rb', 'php', 'pl', 'lua',\n  'c', 'h', 'cpp', 'hpp', 'cc', 'cs', 'm', 'mm',\n  // Infra & DevOps\n  'tf', 'hcl', 'tfvars', 'sql', 'graphql', 'gql', 'dockerfile',\n  // Web\n  'html', 'htm', 'css', 'scss', 'sass', 'less', 'vue', 'svelte',\n  // Patches\n  'patch', 'diff'\n]);\n\nconst isImage = mimeType.startsWith('image/');\nconst isTextMime =\n  mimeType.startsWith('text/') ||\n  mimeType === 'application/json' ||\n  mimeType === 'application/ld+json' ||\n  mimeType === 'application/xml' ||\n  mimeType === 'application/x-yaml' ||\n  mimeType === 'application/yaml' ||\n  mimeType === 'application/x-sh' ||\n  mimeType === 'application/javascript' ||\n  mimeType === 'application/x-ndjson' ||\n  mimeType === 'application/toml';\nconst isTextLike = isTextMime || TEXT_EXTENSIONS.has(ext);\n\nlet category;\nif (isImage && size <= config.MAX_IMAGE_SIZE_BYTES) {\n  category = 'image';\n} else if (isTextLike && size <= config.MAX_TEXT_SIZE_BYTES) {\n  category = 'text_small';\n} else if (isTextLike) {\n  category = 'text_too_large';\n} else {\n  category = 'unsupported';\n}\n\nreturn {\n  json: {\n    ...fileInfo,\n    file_extension: ext,\n    is_text_like: isTextLike,\n    is_image: isImage,\n    category\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "d7abba2a-4351-4bae-8ea5-cc1c45335d9c",
      "name": "Route by category",
      "type": "n8n-nodes-base.switch",
      "position": [
        1968,
        432
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "image",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw+1234567890-+1234567890",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "image"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "text_small",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw+1234567890-+1234567890",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "text_small"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "text_too_large",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw+1234567890-+1234567890",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "text_too_large"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "unsupported"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "51e2994b-61c6-4797-b521-616f6005f53d",
      "name": "Download file",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2192,
        160
      ],
      "parameters": {
        "url": "={{ $('Config').first().json.MATTERMOST_BASE_URL }}/api/v4/files/{{ $json.id }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "720bd88c-5091-4b38-a58f-488106b3b067",
      "name": "OpenAI: Analyze Image",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2416,
        160
      ],
      "parameters": {
        "text": "You are a DevOps screenshot analyst. Produce a concise structured description in English of the attached image.\n\nOutput sections (skip a section if not applicable):\n1. source_system \u2014 which tool/system the screenshot is from. Choose from: GitHub Actions, GitLab CI, Jenkins, TeamCity, Grafana, Sentry, Loki, Prometheus, Mattermost, Slack, Jira, Kubernetes Dashboard, terminal/shell, IDE (VSCode/JetBrains), browser DevTools, generic web page, unknown.\n2. visible_text \u2014 verbatim transcript of error messages, stack traces, command output, status codes, job names. Preserve exact wording and casing. Use code fences for multi-line blocks.\n3. key_signals \u2014 bullet list of what matters for an SRE: failed step name, exit code, service/pod/namespace, timestamp, severity, status indicator color.\n4. summary \u2014 1-2 sentences in plain English on what the screenshot shows.\n\nBe terse. No filler, no greetings, no markdown headings beyond what is asked. If the image is not technical (meme, photo of a person, decorative), say so in summary and skip other sections.",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Config').first().json.VISION_MODEL }}"
        },
        "options": {
          "detail": "auto"
        },
        "resource": "image",
        "inputType": "=base64",
        "operation": "analyze"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "563c042b-8359-4eed-b5fa-32ea62da3a19",
      "name": "Format image description",
      "type": "n8n-nodes-base.code",
      "position": [
        2640,
        160
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// runOnceForEachItem \u2014 pairedItem propagation lets us reach back to\n// \"Classify file\" by item index and recover the original file metadata.\n\nconst llmOut = $input.item.json;\nconst fileInfo = $('Classify file').item.json;\n\n// OpenAI Analyze Image returns the model text under different fields\n// depending on n8n version. Try common shapes in order.\nlet description =\n  (typeof llmOut.content === 'string' && llmOut.content) ||\n  (typeof llmOut.message?.content === 'string' && llmOut.message.content) ||\n  (Array.isArray(llmOut.content) && llmOut.content.map(c => c.text || '').join('\\n')) ||\n  llmOut.text ||\n  '[vision model returned no content]';\n\nif (typeof description !== 'string') {\n  description = JSON.stringify(description);\n}\n\nreturn {\n  json: {\n    file_id: fileInfo.id,\n    file_name: fileInfo.name,\n    mime_type: fileInfo.mime_type,\n    size: fileInfo.size,\n    is_image: true,\n    is_text: false,\n    description\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "9b8f65b0-012a-4d17-a53c-65931b5e7cf2",
      "name": "Download text file",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2192,
        352
      ],
      "parameters": {
        "url": "={{ $('Config').first().json.MATTERMOST_BASE_URL }}/api/v4/files/{{ $json.id }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "eb150e5f-ddb7-4387-b8c8-55ffd741f22d",
      "name": "Format text content",
      "type": "n8n-nodes-base.code",
      "position": [
        2416,
        352
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// runOnceForEachItem. HTTP Request (responseFormat=text) returned the body\n// as a string in $json.data. File metadata is recovered via pairedItem from\n// \"Classify file\".\n//\n// Two-tier size check:\n//   tier 1 (bytes): already enforced by \"Classify file\" + Switch routing.\n//   tier 2 (lines): re-checked here for files that fit byte budget but\n//                   contain unusually long lines (e.g. minified JSON).\n\nconst fileInfo = $('Classify file').item.json;\nconst config = $('Config').first().json;\nconst text = typeof $json.data === 'string' ? $json.data : '';\n\nconst lineCount = text.length === 0 ? 0 : text.split(/\\r\\n|\\r|\\n/).length;\nconst maxLines = Number(config.MAX_TEXT_LINES) || 300;\n\nif (lineCount > maxLines) {\n  return {\n    json: {\n      file_id: fileInfo.id,\n      file_name: fileInfo.name,\n      mime_type: fileInfo.mime_type,\n      size: fileInfo.size,\n      is_image: false,\n      is_text: true,\n      description: `Attachment ${fileInfo.name} is too large to inline: ${fileInfo.size} bytes / ${lineCount} lines (limit: ${maxLines} lines). Ask the user to share the relevant excerpt.`\n    }\n  };\n}\n\nreturn {\n  json: {\n    file_id: fileInfo.id,\n    file_name: fileInfo.name,\n    mime_type: fileInfo.mime_type,\n    size: fileInfo.size,\n    is_image: false,\n    is_text: true,\n    description: `Text content of ${fileInfo.name} (${fileInfo.size} bytes, ${lineCount} lines):\\n\\n\\`\\`\\`\\n${text}\\n\\`\\`\\``\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "bf4dfecb-8585-4252-871f-dfee7970dc04",
      "name": "Text too large marker",
      "type": "n8n-nodes-base.set",
      "position": [
        2192,
        576
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "tl+1234567890-+1234567890",
              "name": "file_id",
              "type": "string",
              "value": "={{ $json.id }}"
            },
            {
              "id": "tl+1234567890-+1234567890",
              "name": "file_name",
              "type": "string",
              "value": "={{ $json.name }}"
            },
            {
              "id": "tl+1234567890-+1234567890",
              "name": "mime_type",
              "type": "string",
              "value": "={{ $json.mime_type }}"
            },
            {
              "id": "tl+1234567890-+1234567890",
              "name": "size",
              "type": "number",
              "value": "={{ $json.size }}"
            },
            {
              "id": "tl+1234567890-+1234567890",
              "name": "is_image",
              "type": "boolean",
              "value": false
            },
            {
              "id": "tl+1234567890-+1234567890",
              "name": "is_text",
              "type": "boolean",
              "value": true
            },
            {
              "id": "tl+1234567890-+1234567890",
              "name": "description",
              "type": "string",
              "value": "=Attachment {{ $json.name }} is a text file but too large to inline: {{ $json.size }} bytes (limit: {{ $('Config').first().json.MAX_TEXT_SIZE_BYTES }} bytes). Ask the user to share the relevant excerpt."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "63ec86c3-eb4b-4da3-bbc7-c80293618a44",
      "name": "Non-image marker",
      "type": "n8n-nodes-base.set",
      "position": [
        2192,
        784
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ni+1234567890-+1234567890",
              "name": "file_id",
              "type": "string",
              "value": "={{ $json.id }}"
            },
            {
              "id": "ni+1234567890-+1234567890",
              "name": "file_name",
              "type": "string",
              "value": "={{ $json.name }}"
            },
            {
              "id": "ni+1234567890-+1234567890",
              "name": "mime_type",
              "type": "string",
              "value": "={{ $json.mime_type }}"
            },
            {
              "id": "ni+1234567890-+1234567890",
              "name": "size",
              "type": "number",
              "value": "={{ $json.size }}"
            },
            {
              "id": "ni+1234567890-+1234567890",
              "name": "is_image",
              "type": "boolean",
              "value": "={{ ($json.mime_type || '').startsWith('image/') }}"
            },
            {
              "id": "ni+1234567890-+1234567890",
              "name": "is_text",
              "type": "boolean",
              "value": false
            },
            {
              "id": "ni+1234567890-+1234567890",
              "name": "description",
              "type": "string",
              "value": "=Attachment {{ $json.name }} not analyzed (mime: {{ $json.mime_type }}, size: {{ $json.size }} bytes). Either unsupported binary format or exceeds size limit."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f716620e-b4d8-45fb-b080-aa311dc79d43",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        2864,
        528
      ],
      "parameters": {
        "numberInputs": 4
      },
      "typeVersion": 3.2
    },
    {
      "id": "5cbc7aa3-6eae-4e39-8363-e7fa7ca8efb9",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        3072,
        560
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "attachments"
      },
      "typeVersion": 1
    },
    {
      "id": "3de99b38-8fec-432f-9f9c-2fcfcff19d5b",
      "name": "Build attachments_context",
      "type": "n8n-nodes-base.code",
      "position": [
        3264,
        560
      ],
      "parameters": {
        "jsCode": "// Aggregate produced a single item with `attachments: [...]`.\n// Build a human-readable block for the AI Agent system prompt.\n// Output shape mirrors the \"Empty result\" Set node so the parent workflow\n// can consume the output uniformly.\n\nconst attachments = $input.first().json.attachments || [];\nconst valid = attachments.filter(a => a && a.file_id);\n\nif (valid.length === 0) {\n  return [{ json: { attachments_context: '', attachments_count: 0 } }];\n}\n\nconst blocks = valid.map((a, i) => {\n  let tag;\n  if (a.is_image) tag = 'image';\n  else if (a.is_text) tag = 'text';\n  else tag = a.mime_type || 'file';\n\n  const header = `Attachment ${i + 1}: ${a.file_name} [${tag}]`;\n  return `${header}\\n${a.description}`;\n});\n\nreturn [{\n  json: {\n    attachments_context: blocks.join('\\n\\n---\\n\\n'),\n    attachments_count: valid.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "70b6af74-e8da-464a-b47c-ac1426a65865",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        576
      ],
      "parameters": {
        "width": 576,
        "height": 480,
        "content": "# Input chain\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n```json\n{ \"file_ids\": [\"id1\", \"id2\", ...] }\n```\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c830d6bb-75fb-4004-8a15-2b81efc5f44b",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3008,
        480
      ],
      "parameters": {
        "width": 528,
        "height": 512,
        "content": "## Output chain\n\n\n\n\n\n\n\n\n\n\n\n\n```json\n{\n  \"attachments_context\": \"...human-readable block...\",\n  \"attachments_count\": 2\n}\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "91fa12c8-0d2f-4880-88c8-b011a67a40fe",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2160,
        48
      ],
      "parameters": {
        "color": 6,
        "width": 624,
        "height": 944,
        "content": "## Attachment analyzer\nAccepts images and text files"
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "8ce4e88e-800c-4a8d-b9a1-bdb69f3d86bf",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Has attachments?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Build attachments_context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify file": {
      "main": [
        [
          {
            "node": "Route by category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download file": {
      "main": [
        [
          {
            "node": "OpenAI: Analyze Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get file info": {
      "main": [
        [
          {
            "node": "Classify file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split file_ids": {
      "main": [
        [
          {
            "node": "Get file info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has attachments?": {
      "main": [
        [
          {
            "node": "Split file_ids",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Empty result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Non-image marker": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Route by category": {
      "main": [
        [
          {
            "node": "Download file",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download text file",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Text too large marker",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Non-image marker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download text file": {
      "main": [
        [
          {
            "node": "Format text content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format text content": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "OpenAI: Analyze Image": {
      "main": [
        [
          {
            "node": "Format image description",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text too large marker": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Format image description": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}