{
  "id": "W3HQKZQvozOvtzpC",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "CICDAssistant",
  "tags": [],
  "nodes": [
    {
      "id": "66d0b9e0-2c2a-45b2-9456-a15e35808e68",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -16,
        -32
      ],
      "parameters": {
        "inputSource": "jsonExample",
        "jsonExample": "{\n  \"message\": \"...\",\n  \"post_id\": \"...\",\n  \"channel_id\": \"...\",\n  \"is_thread\": false,\n  \"thread_root_id\": \"...\",\n  \"user_name\": \"...\",\n  \"category\": \"ci_cd_error\",\n  \"confidence\": 0.95,\n  \"summary\": \"...\",\n  \"on_call_user\": \"...\",\n  \"channel_name\": \"...\",\n  \"file_ids\": [\"x4t1k...\", \"9bzqm...\"]}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "75587bf2-e8af-44ed-af04-0ec9270a47d4",
      "name": "SetVars",
      "type": "n8n-nodes-base.set",
      "position": [
        976,
        -32
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a016e5e9-6c20-463b-9d96-895e6a87e3f3",
              "name": "GITHUB_ORG",
              "type": "string",
              "value": "<your-github-organization>"
            },
            {
              "id": "d89e8819-2d33-4548-84f7-bd00854b70e8",
              "name": "LOKI_UID",
              "type": "string",
              "value": "<your-grafana-loki-datasource-uid>"
            },
            {
              "id": "4b9c2ad4-8e51-41e4-85a1-fae533e8f9b1",
              "name": "K8S_NAMESPACE",
              "type": "string",
              "value": "<your-kubernetes-namespace>"
            },
            {
              "id": "35be24e2-cb17-4e8e-9cb4-dda5b96db505",
              "name": "MATTERMOST_CHANNEL",
              "type": "string",
              "value": "<your-devops-mattermost-channel-slug>"
            },
            {
              "id": "9cf5b1e9-9a3e-4d07-b01d-3aee80949e4a",
              "name": "K8S_CLUSTERS",
              "type": "string",
              "value": "<your-dev-kube-context>,<your-prod-kube-context>"
            },
            {
              "id": "f1a0c6d2-7b9e-4d5e-9b1f-8a2c3d4e5f60",
              "name": "K8S_CONTEXT_DEFAULT",
              "type": "string",
              "value": "<your-default-kube-context>"
            },
            {
              "id": "d6f87e8b-203a-48a4-a9fe-4e778f4b44cc",
              "name": "reply_root_id",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.is_thread ? $('ReadIncidentContext').item.json.thread_root_id : $('ReadIncidentContext').item.json.post_id}}"
            },
            {
              "id": "3881c1fd-c6d7-499e-8ea8-a6f3bcb88f3f",
              "name": "message",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.message }}"
            },
            {
              "id": "01242697-dd1c-4515-ae1d-430e3b5c897e",
              "name": "is_thread",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.is_thread }}"
            },
            {
              "id": "ab2a516c-98d7-4d13-8e0a-2739ab05a0ac",
              "name": "channel_id",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.channel_id }}"
            },
            {
              "id": "5aa51df1-5887-496b-834c-29dd50bc3738",
              "name": "on_call_user",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.on_call_user }}"
            },
            {
              "id": "17001a2c-eb49-48dc-8157-00e1436588b9",
              "name": "user_name",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.user_name }}"
            },
            {
              "id": "df5580fd-0c41-43ca-bcdb-30f8416167de",
              "name": "channel_name",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.channel_name }}"
            },
            {
              "id": "4d2289a6-8f2c-4912-b999-4e645359c824",
              "name": "post_id",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.post_id }}"
            },
            {
              "id": "adf463d6-fa59-41a4-8106-71d865d55c77",
              "name": "thread_root_id",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.thread_root_id }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "d4807333-39ba-4e78-b868-e4fdb5b19ddf",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1248,
        -32
      ],
      "parameters": {
        "text": "=Investigate issue from {{ $json.user_name }} in channel {{ $json.channel_name }}{{ $json.is_thread ? ' (message in thread, thread_root_id=' + $json.thread_root_id + ' \u2014 read the story through Mattermost get_thread first)' : '' }}\n\n{{ $json.message }}{{ $json.attachments_context && $json.attachments_context.trim().length > 0 ? '\\n\\nAdditional information from attachments:\\n' + $json.attachments_context : '' }}",
        "options": {
          "maxIterations": 20,
          "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 github organization: {{ $json.GITHUB_ORG }}\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### Frontend team\n- \n\nTheir repositories:\n- \nFrontend repo support 3 platforms: web, android, iOS.\n\n### Backend team\n- \n\nTheir repos:\n- all repos starts with go-<name>. For example: go-room, go-user, etc\n- admin\n\n### QA team\n- \n\nTheir repos:\n- \n\nAlso they may ask about frontend repo\n\n### Devops team\n- \n\nTheir repos:\n- \n\nArea of \u200b\u200bresponsibility:\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.MATTERMOST_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\n## Environment defaults\n\nIf the user does NOT explicitly specify an environment, ALWAYS assume the issue is on PRODUCTION.\n\n- Default Kubernetes context: `{{ $json.K8S_CONTEXT_DEFAULT }}`\n- Available contexts: {{ $json.K8S_CLUSTERS }}\n\nWhen calling k8s tools, pass `context = {{ $json.K8S_CONTEXT_DEFAULT }}` unless the user explicitly mentions one of the following \u2014 only then switch to the dev cluster `dev-cluster`:\n- English: \"dev\", \"development\", \"staging\", \"stage\", \"test\", \"testing\"\n- Or the user names the dev cluster directly.\n\nDo NOT ask the user which environment they meant \u2014 silently default to prod and state your assumption in the final answer (e.g. \"Assuming production, I verified against `prod-cluster`\").\n\n## Message context\n\nThe request can come either from a main channel message or from a thread reply.\n\n- channel_id: {{ $json.channel_id }}\n- post_id: {{ $json.post_id }}\n- is_thread: {{ $json.is_thread }}\n- thread_root_id: {{ $json.thread_root_id }}\n\nIf `is_thread` is true, the user's question is part of an ongoing thread.\nBefore investigating, call Mattermost `get_thread` with `thread_root_id`\nexactly ONCE to load the full conversation. Earlier messages in the thread\noften contain critical context: links to failing jobs, error logs, repository\nnames, previous hypotheses, or clarifications from the user.\n\nRules for reading the thread:\n- Call `get_thread` only once, at the very start, before any other tool.\n- Do not call `get_channel_messages` for thread context \u2014 use `get_thread`.\n- Ignore posts authored by the bot itself (previous assistant replies) when\n  extracting the problem description. Focus on messages from human users.\n- Treat the user prompt (the message below) as the LATEST question \u2014\n  prior thread messages are background context, not the current ask.\n\nIf `is_thread` is false, skip this step. The message in the user prompt\nis the entire request; do not fetch thread history.\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 (default context: `{{ $json.K8S_CONTEXT_DEFAULT }}`).\n- If errors are related to missing github actions secrets or variables, check if they exist for the repository.\n\n## Querying Kubernetes events via Loki\n\nDo NOT try to read Kubernetes events through k8s tools \u2014 they don't expose `kubectl get events` reliably. Cluster events (FailedScheduling, OOMKilled, ImagePullBackOff, CrashLoopBackOff, BackOff, FailedMount, Unhealthy, etc.) are streamed into Loki by `kubernetes-event-exporter`.\n\nQuery them through Grafana `query_loki_logs` with:\n- datasource_uid: `{{ $json.LOKI_UID }}`\n- logql: `{job=\"integrations/kubernetes/eventhandler\", namespace=\"{{ $json.K8S_NAMESPACE }}\"}`\n\nNarrow the result with line filters when you know the resource:\n- by pod/deployment name: `{job=\"integrations/kubernetes/eventhandler\", namespace=\"{{ $json.K8S_NAMESPACE }}\"} |= \"<name>\"`\n- by reason: `{job=\"integrations/kubernetes/eventhandler\", namespace=\"{{ $json.K8S_NAMESPACE }}\"} |= \"OOMKilled\"`\n- combine: `{job=\"integrations/kubernetes/eventhandler\", namespace=\"{{ $json.K8S_NAMESPACE }}\"} |= \"<name>\" |= \"FailedScheduling\"`\n\nUse a tight time range (last 30m\u20131h) for active incidents to keep responses small.\n\nRequest Examples 1:\n```\nInvestigate the issue from j.doe, our web deployment is giving 400\n```\nExplanation: Web most likely means a build of the web version of the frontend, so you need to look for failed jobs in <repo name>\n\nRequest Examples 2:\n```\nInvestigate issue from a.smith: CI/CD does not work on GitHub.\n```\nExplanation: It's most likely a backend service issue. The problem is likely in one of the backend repositories that has had changes in the last few hours.\n\n# MCP tools\nYou have several tools\n\n## Github tools\n- Use for manipulation with actions, repositories, files, commits, pull requests\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- For Kubernetes events use the dedicated query described in \"Querying Kubernetes events via Loki\".\n\n## k8s tools\n- Default context: `{{ $json.K8S_CONTEXT_DEFAULT }}`. Override only when the user explicitly mentions dev/stage.\n- For cluster events, prefer Loki (see above) over k8s tools.\n\n## Mattermost tools\n- Use `get_thread` to read the full thread context when `is_thread` is true.\n- Use `get_user_by_username` / `get_user` to resolve author identities if needed.\n- Do not use Mattermost tools to post replies \u2014 replies are sent by the\n  workflow after your response.\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": "4c205a27-ea12-41f5-a5d5-03844fa4aeee",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        1152,
        128
      ],
      "parameters": {
        "model": "openai/gpt-5.3-codex",
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9fb50189-9238-44e2-b3fb-2f5bb4a182fa",
      "name": "Github",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1536,
        400
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "<your-github-mcp-base-url>/github/mcp",
        "includeTools": [
          "actions_get",
          "actions_list",
          "get_commit",
          "get_file_contents",
          "get_job_logs",
          "get_gist",
          "get_repository_tree",
          "list_branches",
          "list_commits",
          "list_pull_requests",
          "pull_request_read",
          "search_code",
          "search_repositories"
        ],
        "authentication": "bearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "9c02ee51-cbdd-413b-9103-7031b6c08e78",
      "name": "Grafana",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1152,
        400
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "<your-grafana-mcp-base-url>/grafana/mcp",
        "includeTools": [
          "get_datasource",
          "get_panel_image",
          "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": "e9fc66a2-6a15-4fa8-a9aa-8b7a877c7d27",
      "name": "probe_url",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        1648,
        400
      ],
      "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": "d76a64ba-8ad6-4ec5-adac-f62e2d8ba4b2",
      "name": "k8s",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1280,
        400
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "<your-kubernetes-mcp-url>",
        "includeTools": [
          "configuration_view",
          "namespaces_list",
          "nodes_log",
          "pods_get",
          "pods_list",
          "pods_list_in_namespace",
          "pods_log",
          "resources_get",
          "resources_list"
        ]
      },
      "typeVersion": 1.2
    },
    {
      "id": "e489ccdf-7c51-4a80-875d-bc09458538db",
      "name": "Mattermost",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1408,
        400
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "<your-mattermost-mcp-base-url>/mattermost/mcp",
        "includeTools": [
          "get_channel",
          "get_channel_by_name",
          "get_channel_members",
          "list_my_channels",
          "list_public_channels",
          "get_file_info",
          "get_channel_messages",
          "search_messages",
          "get_user_by_username",
          "get_user",
          "get_thread"
        ]
      },
      "typeVersion": 1.2
    },
    {
      "id": "4087c4e9-1db4-4942-af95-61cada61c969",
      "name": "Validate Output",
      "type": "n8n-nodes-base.code",
      "position": [
        1696,
        -32
      ],
      "parameters": {
        "jsCode": "// Validate AI Agent output before posting to Mattermost.\n// If the agent returned nothing, only whitespace, or a non-string value,\n// substitute a fallback message so the user always receives a reply.\n\nconst data = $input.first().json;\nconst rawOutput = data.output;\nconst output = (typeof rawOutput === 'string' ? rawOutput : '').trim();\n\nconst FALLBACK = 'Could not complete the investigation.';\nconst finalOutput = output.length > 0 ? output : FALLBACK;\n\nif (output.length === 0) {\n  console.warn('AI Agent returned empty output, using fallback message');\n}\n\nreturn [{ json: { ...data, output: finalOutput, is_fallback: output.length === 0 } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "4dcb45e0-10de-44e1-af73-dc3e96e3849e",
      "name": "Post a message",
      "type": "n8n-nodes-base.mattermost",
      "position": [
        1872,
        -32
      ],
      "parameters": {
        "message": "={{ $json.output }}",
        "channelId": "={{ $('SetVars').first().json.channel_id }}",
        "attachments": [],
        "otherOptions": {
          "root_id": "={{ $('SetVars').first().json.reply_root_id }}"
        }
      },
      "credentials": {
        "mattermostApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "835c72eb-bbfa-47be-9c11-e2c8db94e756",
      "name": "Set: empty attachments",
      "type": "n8n-nodes-base.set",
      "position": [
        624,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d18dd763-c02e-4966-b18c-1607d74a7c26",
              "name": "attachments_context",
              "type": "string",
              "value": ""
            },
            {
              "id": "fee92a25-bf1e-4d87-a74c-5e8bc586ee65",
              "name": "attachments_count",
              "type": "string",
              "value": "0"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "97f5f573-c835-4459-b1fc-d91db40311b1",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        816,
        -32
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "e3a490eb-3093-4d92-94c5-fd71605e07a2",
      "name": "ReadIncidentContext",
      "type": "n8n-nodes-base.code",
      "position": [
        192,
        -32
      ],
      "parameters": {
        "jsCode": "// Read the classification object passed from devopsChatAssistant\n// Available fields: message, post_id, channel_id, user_name, user_id,\n// team_domain, channel_name, timestamp, category, confidence, summary\n\nconst data = $input.first().json;\n\n// Log for debugging\nconsole.log(`Incident received: ${data.summary}`);\nconsole.log(`From: ${data.user_name} in #${data.channel_name}`);\nconsole.log(`Confidence: ${data.confidence}`);\n\nreturn [{ json: data }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77afc3d9-2029-487b-bcb5-fd632fb8a4d9",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -160
      ],
      "parameters": {
        "width": 416,
        "height": 736,
        "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_thread\":,\n    \"parent_post\": ,\n    \"thread_root_id\": ,\n    \"on_call_user\": \n  }\n]\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "17e6e297-90dd-4da4-bdfb-9e9362967485",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        -160
      ],
      "parameters": {
        "color": 6,
        "width": 512,
        "height": 432,
        "content": "## Alert analysis"
      },
      "typeVersion": 1
    },
    {
      "id": "5e5f88c8-4eaa-4b7d-b2a5-1931a2fe7045",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        -160
      ],
      "parameters": {
        "width": 512,
        "height": 432,
        "content": "## Output chain\nValidating the response and sending it to the Mattermost request thread"
      },
      "typeVersion": 1
    },
    {
      "id": "77e6c1fc-d121-4000-8bd3-a696df03f5a0",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        320
      ],
      "parameters": {
        "color": 4,
        "width": 656,
        "height": 432,
        "content": "\n\n\n\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* [github-mcp](https://github.com/github/github-mcp-server)\n* [kubernetes-mcp-server](https://github.com/containers/kubernetes-mcp-server)\n* [mattermost-mcp](https://github.com/cloud-ru-tech/mcp-server-mattermost)"
      },
      "typeVersion": 1
    },
    {
      "id": "0bd522c8-96b3-4ed3-8f4f-3ce58c6de0f2",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -160
      ],
      "parameters": {
        "color": 2,
        "width": 720,
        "height": 432,
        "content": "## Check attachments\nCalling a subworkflow to get context from attachments"
      },
      "typeVersion": 1
    },
    {
      "id": "77347e09-aed7-4ed4-9d0e-6d45ae47d988",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        320
      ],
      "parameters": {
        "color": 5,
        "width": 720,
        "height": 784,
        "content": "## Overview\nIt is a sub-workflow that investigates CI/CD failures reported by engineers in Mattermost. 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* Mattermost credentials (native n8n node) \u2014 used to post the final reply back into the thread.\n* `attachmentsAnalyzer` sub-workflow \u2014 must be present in the same n8n instance; analyzes files attached to the originating Mattermost 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](https://n8n.io/workflows/15051-classify-and-route-devops-chat-requests-with-mattermost-openrouter-and-google-calendar) \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 Mattermost 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 call `get_thread` so it can read the conversation history before pulling\nlogs from anywhere else\n* `Post a message` sends the final answer back into theoriginal channel\n\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` - for `probe_url` node tool\n* Connect a [parent classifier](https://n8n.io/workflows/15051-classify-and-route-devops-chat-requests-with-mattermost-openrouter-and-google-calendar) that calls this workflow via `Execute Workflow`\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b14126b1-b64c-489a-9356-eccea7bf0178",
      "name": "attachmentsAnalyzer",
      "type": "n8n-nodes-base.executeWorkflow",
      "onError": "continueErrorOutput",
      "position": [
        432,
        -32
      ],
      "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
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "10e7cff8-7949-45ae-8b16-4c820270311b",
  "connections": {
    "k8s": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "SetVars",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Github": {
      "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": "Validate Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "probe_url": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Mattermost": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Validate Output": {
      "main": [
        [
          {
            "node": "Post a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ReadIncidentContext": {
      "main": [
        [
          {
            "node": "attachmentsAnalyzer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "attachmentsAnalyzer": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set: empty attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Set: empty attachments": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "ReadIncidentContext",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}