{
  "id": "pYW3RTVMaIluKEGt",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "CICDAssistant",
  "tags": [],
  "nodes": [
    {
      "id": "96f7b704-5422-4199-9f7b-4cfc249cee04",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -64,
        0
      ],
      "parameters": {
        "inputSource": "jsonExample",
        "jsonExample": "{\n  \"message\": \"...\",\n  \"post_id\": \"...\",\n  \"channel_id\": \"...\",\n  \"thread_root_id\": \"...\",\n  \"user_id\": \"...\",\n  \"category\": \"incident\",\n  \"is_in_thread\": false,\n  \"confidence\": 0.95,\n  \"summary\": \"...\",\n  \"channel_name\": \"...\",\n  \"user_name\": \"...\",\n  \"file_ids\": [\"x4t1k...\", \"9bzqm...\"]}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6eafbd3c-95cc-4a2e-abd5-c365f75080c1",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1056,
        0
      ],
      "parameters": {
        "text": "=Investigate issue from {{ $json.user_name }} in a channel {{ $json.channel_id }}\n{{ $json.message }}{{ $json.attachments_context && $json.attachments_context.trim().length > 0 ? '\\n\\nAdditional information from attachments:\\n' + $json.attachments_context : '' }}",
        "options": {
          "maxIterations": 25,
          "systemMessage": "=You are an experienced DevOps engineer. \nYou have a good understanding of CI/CD systems such as Gitlab CI, Github Actions, Jenkins, Teamcity.\nYour main task is to analyze unsuccessful tasks, jobs, workflows. You only look and analyze, without changing anything. You need to provide 1 to 3 probable causes and their solutions.\n\nThere're all repositories in gitlab group: {{ $json.GITLAB_GROUP }}\nIf you haven't received a link to the repository or the specific job that broke, try finding it yourself. Check who sent the request. Knowing which team it refers to, you can narrow your search to the repositories of that team. Below is a description of the teams and their repositories.\n\n## Slack thread context\n- The Slack message metadata is provided as variables: is_in_thread = {{ $json.is_in_thread }}, thread_root_id = {{ $json.thread_root_id }}, channel_id = {{ $json.channel_id }}\n- If is_in_thread is true, the user's question is a reply in an existing Slack thread. BEFORE doing any other investigation step, fetch the full thread using the Slack tool conversations_replies with channel = {{ $json.channel_id }} and ts = {{ $json.thread_root_id }}. Read every reply in chronological order \u2014 they often contain prior investigation steps, shared logs, links to failing jobs / repositories / dashboards, decisions, and findings from teammates\n- Use the thread context to scope the investigation. Do NOT repeat work that was already done in the thread; build on it. If the thread already points to a specific repo, branch, job, service or root cause hypothesis, focus there first\n- Treat the latest message in the thread as the most up-to-date statement of the problem; earlier messages provide context but may be outdated\n- If is_in_thread is false, do NOT fetch thread history\n\n### Frontend team\n-\n\nTheir repositories:\n-\n\n### Backend team\n-\n\nTheir repos:\n-\n\n### QA team\n-\n\n### Devops team\n-\n\nTheir repos:\n-\n\nArea of responsibility:\n- Workflow Descriptions\n- Variables and Secrets\n- Runners where jobs are launched\n- Databases, load balancers, and other infrastructure\n\n## Important rules\n- Always answer in the same language in which the question was asked.\n- Don't ask anything, analyze it yourself\n- If you can't do something, don't get hung up on it, look for other options.\n- Don't use emoji\n- If the message was received in the channel {{ $json.SLACK_CHANNEL }} AND the most likely cause lies within the area of \u200b\u200bresponsibility Devops team mention the duty officer at the end of the message like @{{ $json.on_call_user }}\n- You are an autonomous investigator. DO NOT announce what you are going to do. DO NOT write phrases like \"I will check\", \"Let me investigate\", \"I'll look at the logs\". Just CALL THE TOOLS and produce the final answer with findings.\n- Your first message to the user MUST be the final report with concrete findings from tool calls. Never produce an interim plan message.\n\n## Investigation suggestion\n- Investigate failed job logs\n- Check last commit and changes\n- If there is a problem with the availability of an external resource, try making a test request to it.\n- If you're having trouble deploying to a Kubernetes cluster, check the service logs in Grafana Loki. Datasource uid: {{ $json.LOKI_UID }}\n- If the errors are related to deployment kubernetes cluster determine which cluster the problem occurred with, it could be: {{ $json.K8S_CLUSTERS }}. Use k8s tools for investigation issue inside cluster.\n- If errors are related to missing github actions secrets or variables, check if they exist for the repository.\n\n- \n\nRequest Examples 1:\n```\nInvestigate the issue from j.doe: our web deployment returns 400\n```\nExplanation: Web most likely means a build of the web frontend \u2014 look for failed jobs in <your-frontend-repo>.\n\nRequest Examples 2:\n```\nInvestigate issue from a.smith: GitLab CI/CD is not working\n```\nExplanation: Likely a backend service. Check backend repositories with recent changes in your GitLab backend group.\n\n# MCP tools\nYou have several tools\n\n## Gitlab tools\n- Use for manipulation with ci/cd, repositories, files, commits, pull requests\n- The GitLab repo is a file tree. A path with no file extension, or a path you know is a folder in the UI, may be a directory, not a file.\n- Never call get_file_contents for a path until you are sure it is a specific file (e.g. ansible/inventory/prod/hosts, not ansible/inventory/prod).\n- To see what is inside a path, first call get_repository_tree with path set to that directory\n\n## probe_url\n- Use for web request. Checking connectivity\n\n## Grafana tools\n- Use for searching logs. Use label namespace={{ $json.K8S_NAMESPACE }}\n\n## Handling External Dependencies\n\nWhen a CI job fails due to a download/fetch error (e.g., \"connection timed out\",\n\"could not resolve host\", \"404 Not Found\" on a dependency URL):\n\n1. Extract the failing URL from the job log\n2. Use the probe_url tool to verify its current reachability\n3. Interpret the result:\n   - timeout / dns_failure / connection_refused \u2192 the dependency host is\n     down or misconfigured. This IS the root cause. Report it clearly and\n     suggest: check mirror availability, contact vendor, pin to cached\n     version, or add a fallback mirror.\n   - HTTP 404 \u2192 the specific artifact path is gone. Check if version was\n     yanked or URL changed.\n   - HTTP 401/403 \u2192 credential / auth issue in CI variables.\n   - HTTP 2xx \u2192 the URL works now; failure was transient or a flaky network.\n\nNever treat an unreachable dependency as an investigation failure \u2014\nit's a valid and actionable root cause.\n"
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "e6eff919-8683-4206-98e5-270a1a9af4c7",
      "name": "Grafana",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        960,
        432
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "https://<your-mcp-host>/grafana/mcp",
        "includeTools": [
          "get_datasource",
          "list_datasources",
          "list_loki_label_names",
          "list_loki_label_values",
          "list_prometheus_label_names",
          "list_prometheus_label_values",
          "list_prometheus_metric_metadata",
          "list_prometheus_metric_names",
          "query_loki_logs",
          "query_loki_patterns",
          "query_loki_stats",
          "query_prometheus",
          "query_prometheus_histogram"
        ]
      },
      "typeVersion": 1.2
    },
    {
      "id": "6f2c3b31-8f82-4e42-8a9f-76a74b87822c",
      "name": "SetVars",
      "type": "n8n-nodes-base.set",
      "position": [
        704,
        0
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a016e5e9-6c20-463b-9d96-895e6a87e3f3",
              "name": "GITLAB_GROUP",
              "type": "string",
              "value": "<your-gitlab-group>"
            },
            {
              "id": "d89e8819-2d33-4548-84f7-bd00854b70e8",
              "name": "LOKI_UID",
              "type": "string",
              "value": "<your-loki-datasource-uid>"
            },
            {
              "id": "4b9c2ad4-8e51-41e4-85a1-fae533e8f9b1",
              "name": "K8S_NAMESPACE",
              "type": "string",
              "value": "<your-k8s-namespace>"
            },
            {
              "id": "35be24e2-cb17-4e8e-9cb4-dda5b96db505",
              "name": "SLACK_CHANNEL",
              "type": "string",
              "value": "<your-devops-slack-channel>"
            },
            {
              "id": "9cf5b1e9-9a3e-4d07-b01d-3aee80949e4a",
              "name": "K8S_CLUSTERS",
              "type": "string",
              "value": "<your-prod-kube-context>,<your-dev-kube-context>"
            },
            {
              "id": "1cd93ff0-8fd7-4b87-acbd-971ec34f3892",
              "name": "channel_id",
              "type": "string",
              "value": "={{ $('When Executed by Another Workflow').item.json.channel_id }}"
            },
            {
              "id": "5b71f524-6ee4-4894-aa5b-965ada8a0a42",
              "name": "message",
              "type": "string",
              "value": "={{ $('When Executed by Another Workflow').item.json.message }}"
            },
            {
              "id": "7b818b14-a5d8-49d2-8e5b-76cceec1b624",
              "name": "channel_name",
              "type": "string",
              "value": "={{ $('When Executed by Another Workflow').item.json.channel_name }}"
            },
            {
              "id": "4610c1f4-b7d0-4141-9e31-76e562df15d6",
              "name": "on_call_user",
              "type": "string",
              "value": "<your-on-call-slack-usergroup>"
            },
            {
              "id": "d7d66d05-90f8-4f38-a1b2-28f5b14f16f0",
              "name": "user_id",
              "type": "string",
              "value": "={{ $('When Executed by Another Workflow').item.json.user_id }}"
            },
            {
              "id": "5317f304-9b21-4f37-89d5-0c51b714a625",
              "name": "is_in_thread",
              "type": "string",
              "value": "={{ $('When Executed by Another Workflow').item.json.is_in_thread }}"
            },
            {
              "id": "d4d1cfa3-9fb2-4a05-b860-832629b5fd57",
              "name": "thread_root_id",
              "type": "string",
              "value": "={{ $('When Executed by Another Workflow').item.json.thread_root_id }}"
            },
            {
              "id": "ba79fb87-795e-4e52-aab6-3d28022c2a20",
              "name": "user_name",
              "type": "string",
              "value": "={{ $('When Executed by Another Workflow').item.json.user_name }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "2aae9b12-f1f4-4f2c-af5a-efc084ecafb0",
      "name": "probe_url",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        1408,
        432
      ],
      "parameters": {
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "<your-httpProbeTool-subworkflow-id>",
          "cachedResultUrl": "/workflow/<your-httpProbeTool-subworkflow-id>",
          "cachedResultName": "httpProbeTool"
        },
        "description": " Check if a URL is reachable. Use this to verify whether a dependency endpoint\n     (artifactory, registry, package index, vendor API) is accessible from the CI\n     network. Returns reachability status, HTTP code if reachable, or classified\n     network error (timeout, dns_failure, connection_refused, tls_error).\n     This tool NEVER throws \u2014 always returns a structured result, so you can\n     safely probe any URL extracted from a job log.",
        "workflowInputs": {
          "value": {
            "url": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('url', ``, 'string') }}",
            "method": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('method', ``, 'string') }}",
            "timeout_ms": 10000
          },
          "schema": [
            {
              "id": "url",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "method",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "method",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "timeout_ms",
              "type": "number",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "timeout_ms",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d19d85ed-b659-45b9-871f-4fdf853e5b57",
      "name": "k8s",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1072,
        432
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "http://<your-k8s-mcp-service>:8089/mcp",
        "includeTools": [
          "configuration_view",
          "events_list",
          "namespaces_list",
          "nodes_log",
          "pods_get",
          "pods_list",
          "pods_list_in_namespace",
          "pods_log",
          "resources_get",
          "resources_list"
        ]
      },
      "typeVersion": 1.2
    },
    {
      "id": "d1f851d7-2126-44d2-a390-7d3d959d88bd",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        928,
        176
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5.5",
          "cachedResultName": "gpt-5.5"
        },
        "options": {},
        "builtInTools": {
          "webSearch": {
            "searchContextSize": "medium"
          }
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "e3c8003c-e6e4-4f0c-834c-34ddffa8a04d",
      "name": "Send a message",
      "type": "n8n-nodes-base.slack",
      "position": [
        1632,
        0
      ],
      "parameters": {
        "text": "={{ $json.output }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('SetVars').item.json.channel_id }}"
        },
        "otherOptions": {
          "thread_ts": {
            "replyValues": {
              "thread_ts": "={{ $('SetVars').first().json.thread_root_id }}"
            }
          }
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "b24b49f6-7edb-4408-9dc7-fa9bf3bd2470",
      "name": "Slack",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1184,
        432
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "https://<your-mcp-host>/slack/mcp",
        "includeTools": [
          "conversations_history",
          "conversations_replies",
          "users_search",
          "channels_list"
        ],
        "authentication": "bearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "4d49fa90-b9f6-41e0-b6b1-74ed46914c3a",
      "name": "Gitlab",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1296,
        432
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "https://<your-mcp-host>/gitlab/mcp",
        "includeTools": [
          "get_merge_request_conflicts",
          "get_file_contents",
          "get_merge_request",
          "list_merge_request_changed_files",
          "list_merge_request_diffs",
          "get_merge_request_file_diff",
          "get_branch_diffs",
          "get_namespace",
          "verify_namespace",
          "get_project",
          "list_projects",
          "list_project_members",
          "get_users",
          "list_merge_requests",
          "get_repository_tree",
          "list_commits",
          "get_commit",
          "get_commit_diff",
          "get_pipeline_job",
          "get_pipeline_job_output",
          "list_pipeline_jobs",
          "get_pipeline",
          "list_pipelines"
        ],
        "authentication": "bearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "92b42f6c-29d8-4d21-9f63-44e3e0e4d948",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        560,
        0
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "7858e1a7-d1d4-410c-953a-b4ba25b9378b",
      "name": "Set: empty attachments",
      "type": "n8n-nodes-base.set",
      "position": [
        384,
        192
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "7a4423d6-d836-4ca2-a498-f554d3f116ee",
              "name": "attachments_context",
              "type": "string",
              "value": ""
            },
            {
              "id": "01032c8e-9a5e-46b7-8c84-ab15fe71d2c5",
              "name": "attachments_count",
              "type": "string",
              "value": "0"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "6480e4ef-ca7c-4a76-8570-d60afc474de7",
      "name": "Call 'attachmentsAnalyzer'",
      "type": "n8n-nodes-base.executeWorkflow",
      "onError": "continueErrorOutput",
      "position": [
        208,
        0
      ],
      "parameters": {
        "options": {},
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "<your-attachments-analyzer-subworkflow-id>",
          "cachedResultUrl": "/workflow/<your-attachments-analyzer-subworkflow-id>",
          "cachedResultName": "AttachmentsAnalyzer"
        },
        "workflowInputs": {
          "value": {
            "file_ids": "={{ $json.file_ids }}"
          },
          "schema": [
            {
              "id": "file_ids",
              "type": "array",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "file_ids",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "file_ids"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "d95bf174-638b-4fd9-b82a-18c1751d4fb8",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        -96
      ],
      "parameters": {
        "color": 2,
        "width": 272,
        "height": 688,
        "content": "## Input Chain\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n```\n[\n  {\n    \"message\": ,\n    \"post_id\": ,\n    \"channel_id\": ,\n    \"channel_name\": ,\n    \"user_name\": ,\n    \"user_id\": ,\n    \"file_ids\":,\n    \"category\": ,\n    \"confidence\": ,\n    \"summary\": ,\n    \"acknowledge\": ,\n    \"is_in_thread\":,\n    \"thread_root_id\": ,\n  }\n]\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "58ee48b8-8ea2-4380-a877-bbd5ced26141",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        -96
      ],
      "parameters": {
        "color": "#3F3E0D",
        "width": 656,
        "height": 448,
        "content": "## Check attachments\nCalling a subworkflow to get context from attachments"
      },
      "typeVersion": 1
    },
    {
      "id": "9f7bbed0-4466-4fb5-9b50-5272d5160453",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        -96
      ],
      "parameters": {
        "color": 6,
        "width": 640,
        "height": 448,
        "content": "## CI/CD analysis"
      },
      "typeVersion": 1
    },
    {
      "id": "ba09d04c-03c2-4d3c-9f43-01087407c9ff",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        384
      ],
      "parameters": {
        "color": 4,
        "width": 640,
        "height": 352,
        "content": "\n\n\n\n\n\n\n\n\n\n## MCP servers\nAdd correct URLs for remote MCPs\nUse following mcp:\n* [grafana-mcp](https://github.com/grafana/mcp-grafana)\n* [gitlab-mcp](https://github.com/zereight/gitlab-mcp)\n* [kubernetes-mcp-server](https://github.com/containers/kubernetes-mcp-server)\n* [slack-mcp](https://github.com/korotovsky/slack-mcp-server)"
      },
      "typeVersion": 1
    },
    {
      "id": "8b6030d6-9af0-40bb-9c78-1337b8099c35",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1552,
        -96
      ],
      "parameters": {
        "color": 2,
        "width": 256,
        "height": 448,
        "content": "## Output chain"
      },
      "typeVersion": 1
    },
    {
      "id": "d126c48a-5163-46ac-bfe0-e27864493665",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        384
      ],
      "parameters": {
        "color": 5,
        "width": 656,
        "height": 864,
        "content": "## Overview\nIt is a sub-workflow that investigates CI/CD failures reported by engineers in Slack. It is invoked by a parent classifier and it runs an autonomous AI Agent that diagnoses the failing pipeline, job, or\ndeployment without making any changes.\n\n## Requirements\n* OpenRouter/OpenAI/Anthropic API key \u2014 for the chat model .\n* MCP servers (see MCP section) .\n* Slack credentials .\n* `AttachmentsAnalyzer` sub-workflow \u2014 must be present in the same n8n instance; analyzes files attached to the originating Slack message and returns a textual context.\n* `httpProbeTool` sub-workflow \u2014 provides the probe_url tool that the agent uses to verify reachability of external dependencies; must return structured\n* [Parent workflow]() \u2014 a classifier  that calls this workflow via Execute Workflow \n\n## How it works\n* The workflow is invoked by another workflow via `Execute Workflow Trigger`\n* Call 'attachmentsAnalyzer' invokes a separate sub-workflow that fetches and analyzes any files the user attached to the originating Slack post\n* The successful branch returns enriched attachment context; the error branch goes through\n* `SetVars` materializes the runtime configuration\n* The agent is instructed to  read the conversation history before pulling\nlogs from anywhere else\n* `Post a message` sends the final answer back into theoriginal channel\n\n## How to use\n* Deploy MCP servers\n* Configure the `SetVars` node with your environment\n* Edit the `AI Agent` system prompt to match your organization\n* Import sibling sub-workflows:\n  * `AttachmentsAnalyzer`\n  * `httpProbeTool` \u2014 for the `probe_url` tool node\n* Wire **ErrorReporter** as Error Workflow in Settings (optional)\n* Connect a parent classifier (e.g. ChatAssistant) that calls this workflow via Execute Workflow\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": "",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "9d29dd72-c213-47c3-8a55-2b1ee31f40c9",
  "connections": {
    "k8s": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "SetVars",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Gitlab": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Grafana": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "SetVars": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "probe_url": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Set: empty attachments": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Call 'attachmentsAnalyzer'": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set: empty attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Call 'attachmentsAnalyzer'",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}