{
  "id": "PrzFwmv3zS8HvwWo",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "TaskAssistant",
  "tags": [],
  "nodes": [
    {
      "id": "4edc3cc0-e794-473a-bbd8-bf2f15640c15",
      "name": "Sticky Note \u2014 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        352,
        736
      ],
      "parameters": {
        "color": 5,
        "width": 704,
        "height": 840,
        "content": "## Overview\nThis is a sub-workflow that converts a free-form DevOps request posted in Mattermost into a properly formatted Jira task \n\n## Requirements\n* OpenRouter/OpenAI/Anthropic API key\n* Google Gemini API key \u2014 for embeddings\n* Jira API credentials \u2014 Cloud or Server. \n* Mattermost API credentials \u2014 to post the reply back to the channel\n* Qdrant instance\n* Remote MCP servers  (see MCP section)\n* A sub-workflow that analyses attachments \n* A parent classifier workflow that triggers this one via \"Execute Workflow\" with a properly shaped payload\n\n## How it works\n1. The workflow is triggered by `Execute Workflow Trigger`\n1. `ReadIncidentContext` logs the request and forwards the payload\n1. Call '`attachmentsAnalyzer` invokes a vision sub-workflow with the file_ids\n1. SetVars sets workflow-level constants\n1.  `AI Agent` builds the Jira payload. The system prompt\n1. `Parse Agent Output` safely parses the agent's output enriches the Jira description.\n1. `Create an issue` creates a Task in the configured project with the chosen label and component.\n1. `Post a message` replies in the original thread with a confirmation containing a clickable Jira link.\n\n## How to use\n* Index your infrastructure docs into Qdrant using\n* Deploy the Mattermost MCP server and put its URL into the Mattermost MCP Client node\n* Deploy / import the `attachmentsAnalyzer` sub-workflow \n* Configure credentials in the workflow\n* Edit `SetVars`\n* Edit `Create an issue` \u2014 pick your own Jira project, issue type and component.\n* Tune the system prompt in `AI Agent` \u2014 adjust the direction labels and the few-shot examples to match your team's taxonomy.\n* Wire this workflow into your classifier via an Execute Workflow node"
      },
      "typeVersion": 1
    },
    {
      "id": "8eac83c2-6b95-4ed9-aae0-39d10d47c2c7",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -48,
        416
      ],
      "parameters": {
        "inputSource": "jsonExample",
        "jsonExample": "{\n  \"message\": \"...\",\n  \"post_id\": \"...\",\n  \"channel_id\": \"...\",\n  \"user_name\": \"...\",\n  \"is_thread\": false,\n  \"thread_root_id\": \"...\",\n  \"category\": \"create_resource\",\n  \"confidence\": 0.95,\n  \"summary\": \"...\",\n  \"on_call_user\": \"...\",\n  \"channel_name\": \"...\",\n  \"file_ids\": [\"x4t1k...\", \"9bzqm...\"]}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "85a3602f-4211-42cd-b0c0-7952e8ec3d1d",
      "name": "SetVars",
      "type": "n8n-nodes-base.set",
      "position": [
        944,
        416
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d89e8819-2d33-4548-84f7-bd00854b70e8",
              "name": "MATTERMOST_URL",
              "type": "string",
              "value": "https://<your-mattermost-host>/<your-team-url-path>/"
            },
            {
              "id": "16e9f447-4d98-4173-ba3e-95c047762068",
              "name": "reply_root_id",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.is_thread ? $('ReadIncidentContext').item.json.thread_root_id : $('ReadIncidentContext').item.json.post_id}}"
            },
            {
              "id": "e54f7764-afd8-487c-909a-68598092367b",
              "name": "JIRA_URL",
              "type": "string",
              "value": "https://<your-org>.atlassian.net"
            },
            {
              "id": "ddd796fc-2aa2-4bbd-aee3-e55dd9009276",
              "name": "message",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.message }}"
            },
            {
              "id": "66b+1234567890aa-81ee-585254f44f2d",
              "name": "post_id",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.post_id }}"
            },
            {
              "id": "804efcde-19f0-4854-8bdd-5b30faa15ac0",
              "name": "channel_id",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.channel_id }}"
            },
            {
              "id": "86dfdd6b-a138-49a9-a926-75869b10427b",
              "name": "on_call_user",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.on_call_user }}"
            },
            {
              "id": "ffd9f052-a36d-4aef-b97d-dda4281af9b3",
              "name": "channel_name",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.channel_name }}"
            },
            {
              "id": "fbebb9d0-4ea3-424c-b839-5098cbfcd129",
              "name": "is_thread",
              "type": "string",
              "value": "={{ $('ReadIncidentContext').item.json.is_thread }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "a20b8b36-a5fe-454c-9df9-a08faf10dba8",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1200,
        416
      ],
      "parameters": {
        "text": "=Prepare a task as requested by {{ $json.user_name }} in channel {{ $json.channel_name }} {{ $json.is_thread ? ' (message is 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": 15,
          "systemMessage": "=You're a professional task manager. You're proficient in setting tasks for DevOps, SRE engineers.\nAt the user's request, you need to prepare the data to create a task in Jira. You don't need to create the task yourself.\nDetermine the `subject area`; what does the task relate to? Use the Qdrant Vector Store tool to learn more about our infrastructure when needed.\nDefine the action that needs to be performed: create something, delete something, change something.\nThis could be a database, a cluster, a logging system, or a monitoring system.\n\nDetermine the `direction` and choose exactly one:\n- HighAvailability \u2014 availability of our services\n- Performance \u2014 performance of our services\n- Security \u2014 information security\n- CostManagement \u2014 cost management\n- Operations \u2014 routine operational activities\n- Observability \u2014 monitoring, logs, traces, alerts\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\nYou can use the HTTP Request tool to probe endpoints when the request is about availability or connectivity.\n\n## Response format\nReturn a single JSON object with EXACTLY these fields \u2014 no markdown, no code fences, no extra text:\n{\"summary\":\"<subject area>: <what needs to be done>\",\"description\":\"<1-3 sentences>\",\"label\":\"<one of the directions above>\"}\n\n## Example 1\nRequest: create a small Postgres database for the checkout API\nOutput:\n{\"summary\":\"Postgres: create database for checkout API\",\"description\":\"We need to create small Postgres instances in the stage and prod environments for the checkout backend service.\",\"label\":\"Operations\"}\n\n## Example 2\nRequest: please enable gRPC on the realtime gateway and add the URL to Vault\nOutput:\n{\"summary\":\"Realtime gateway: enable gRPC\",\"description\":\"Need to enable gRPC on the realtime gateway and add the service URL to HashiCorp Vault.\",\"label\":\"Operations\"}\n\n## Example 3\nRequest: Need to change logs collector promtail to alloy. Create a task\nOutput:\n{\"summary\":\"Logging: replace promtail with alloy\",\"description\":\"Need to change logs collector promtail to alloy.\",\"label\":\"Observability\"}\n\n\n## Rules\n* Describe the problem in formal language, even if it is not so in the initial message"
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "271b834e-0881-4016-9002-6529df935d6f",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        1136,
        576
      ],
      "parameters": {
        "model": "openai/gpt-5.3-codex",
        "options": {
          "temperature": 0.2,
          "responseFormat": "json_object"
        }
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "e9d25b17-61f6-4be2-968b-7a5607026855",
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequestTool",
      "position": [
        1344,
        800
      ],
      "parameters": {
        "url": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('URL', ``, 'string') }}",
        "options": {},
        "toolDescription": "Makes an HTTP request and returns the response data. For checking network issues or probing endpoints."
      },
      "typeVersion": 4.4
    },
    {
      "id": "473df4d2-a9ba-4bb0-81ff-1da25d3e46e1",
      "name": "Qdrant Vector Store",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreQdrant",
      "position": [
        1472,
        800
      ],
      "parameters": {
        "mode": "retrieve-as-tool",
        "options": {},
        "toolDescription": "Use it to learn more about our infrastructure, alert rules and runbooks.",
        "qdrantCollection": {
          "__rl": true,
          "mode": "list",
          "value": "<your-infrastructure-knowledge-collection>",
          "cachedResultName": "<your-infrastructure-knowledge-collection>"
        }
      },
      "credentials": {
        "qdrantApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "db631503-b383-4299-a149-28d75642f91f",
      "name": "Embeddings Google Gemini",
      "type": "@n8n/n8n-nodes-langchain.embeddingsGoogleGemini",
      "position": [
        1472,
        976
      ],
      "parameters": {
        "modelName": "models/gemini-embedding-2-preview"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1b7f0641-f611-4321-ba25-d93c6f46b844",
      "name": "Parse Agent Output",
      "type": "n8n-nodes-base.code",
      "position": [
        1648,
        416
      ],
      "parameters": {
        "jsCode": "// Parse AI Agent output (raw JSON or inside a markdown code fence),\n// merge with context from Read Incident Context and SetVars,\n// and build a Jira payload including a Mattermost permalink to the source message.\n\nconst raw = $input.first().json.output ?? '';\nconst ctx = $('ReadIncidentContext').first().json;\nconst vars = $('SetVars').first().json;\n\n// Strip surrounding markdown code fences if present\nconst cleaned = String(raw)\n  .replace(/^\\s*```(?:json)?\\s*/i, '')\n  .replace(/\\s*```\\s*$/i, '')\n  .trim();\n\nlet parsed;\ntry {\n  parsed = JSON.parse(cleaned);\n} catch (err) {\n  throw new Error(`Failed to parse AI Agent output as JSON: ${err.message}. Raw output was: ${raw}`);\n}\n\n// Build a Mattermost permalink to the thread where the request was posted\nconst mattermostBase = (vars.MATTERMOST_URL || '').replace(/\\/+$/, '');\nconst threadLink = ctx.post_id ? `${mattermostBase}/${ctx.post_id}` : null;\n\n// Compose source block appended to the Jira description\nconst sourceLines = [\n  '',\n  '---',\n  `*Source:* @${ctx.user_name || 'unknown'} in channel #${ctx.channel_name || 'unknown'}`,\n];\nif (threadLink) sourceLines.push(`*Message link:* ${threadLink}`);\nif (ctx.summary) sourceLines.push(`*Classification:* ${ctx.summary} (confidence: ${ctx.confidence ?? 'n/a'})`);\n\nconst description = `${parsed.description || ''}\\n${sourceLines.join('\\n')}`;\n\nreturn [{\n  json: {\n    summary: parsed.summary || '(no summary)',\n    description,\n    label: parsed.label || 'Operations',\n    user_name: ctx.user_name,\n    on_call_user: ctx.on_call_user,\n    channel_id: ctx.channel_id,\n    post_id: ctx.post_id,\n    thread_link: threadLink,\n    agent_raw_output: raw,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "da526997-7792-4d84-a0b2-b4cb8d9b2ef9",
      "name": "Create an issue",
      "type": "n8n-nodes-base.jira",
      "position": [
        1824,
        416
      ],
      "parameters": {
        "project": {
          "__rl": true,
          "mode": "list",
          "value": "<your-jira-project-id>",
          "cachedResultName": "<your-jira-project-name>"
        },
        "summary": "={{ $json.summary }}",
        "issueType": {
          "__rl": true,
          "mode": "list",
          "value": "<your-jira-issue-type-id>",
          "cachedResultName": "<your-jira-issue-type-name>"
        },
        "additionalFields": {
          "labels": "={{ [$json.label] }}",
          "description": "={{ $json.description }}",
          "componentIds": [
            "<your-jira-component-id>"
          ]
        }
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "62d40c47-6da6-440e-9621-1e9e853e058d",
      "name": "Post a message",
      "type": "n8n-nodes-base.mattermost",
      "position": [
        2032,
        416
      ],
      "parameters": {
        "message": "=\u2705 Created issue [{{ $('Create an issue').first().json.key }}]({{ $('SetVars').first().json.JIRA_URL }}/browse/{{ $('Create an issue').first().json.key }}): {{ $('Parse Agent Output').first().json.summary }}",
        "channelId": "={{ $('Parse Agent Output').first().json.channel_id }}",
        "attachments": [],
        "otherOptions": {
          "root_id": "={{ $('SetVars').first().json.reply_root_id }}"
        }
      },
      "credentials": {
        "mattermostApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ca58e161-67a4-4c9c-a60f-228d6da4698c",
      "name": "Mattermost",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "position": [
        1216,
        800
      ],
      "parameters": {
        "include": "selected",
        "options": {},
        "endpointUrl": "<your-mattermost-mcp-base-url>/mattermost/mcp",
        "includeTools": [
          "get_channel_by_name",
          "list_public_channels",
          "get_file_info",
          "get_channel_messages",
          "search_messages",
          "get_thread",
          "get_me",
          "get_user",
          "get_user_by_username"
        ]
      },
      "typeVersion": 1.2
    },
    {
      "id": "a2406fd5-fb89-46e1-a7f2-db40396945a7",
      "name": "Edit Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        608,
        560
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "632595c9-f4cd-43f7-916c-5ed9bdf51db4",
              "name": "attachments_context",
              "type": "string",
              "value": ""
            },
            {
              "id": "7ad957b3-1d49-47ae-b2d6-1abf5ced6b08",
              "name": "attachments_count",
              "type": "string",
              "value": "0"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "93828410-1aa3-4d06-9b45-e7a4537b5f3b",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        768,
        416
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "780ddba7-3663-425a-9b2a-bd066aa0a5a3",
      "name": "ReadIncidentContext",
      "type": "n8n-nodes-base.code",
      "position": [
        144,
        416
      ],
      "parameters": {
        "jsCode": "// Read the classification object passed from the parent classifier workflow.\n// Available fields: message, post_id, channel_id, user_name,\n// channel_name, category, confidence, summary, on_call_user.\n\nconst data = $input.first().json;\n\nconsole.log(`Task 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": "89a2f950-8050-4817-9821-a3d54382e252",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        272
      ],
      "parameters": {
        "width": 432,
        "height": 704,
        "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]"
      },
      "typeVersion": 1
    },
    {
      "id": "27baffb2-a220-40eb-a5fd-2c7948cddc1a",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        352,
        272
      ],
      "parameters": {
        "color": 2,
        "width": 704,
        "height": 432,
        "content": "## Check attachment"
      },
      "typeVersion": 1
    },
    {
      "id": "b819e627-8971-42d4-8e64-580303995b58",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        272
      ],
      "parameters": {
        "width": 576,
        "height": 432,
        "content": "## Output chain"
      },
      "typeVersion": 1
    },
    {
      "id": "1b4b81da-d49e-4940-b4a4-e40bf991aa89",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1104,
        272
      ],
      "parameters": {
        "color": 6,
        "width": 448,
        "height": 432,
        "content": "## Request analysis"
      },
      "typeVersion": 1
    },
    {
      "id": "ff618f46-2949-4f0f-a9da-b34cdd090bd4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1104,
        736
      ],
      "parameters": {
        "color": 4,
        "width": 656,
        "height": 416,
        "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* [mattermost-mcp](https://github.com/cloud-ru-tech/mcp-server-mattermost)"
      },
      "typeVersion": 1
    },
    {
      "id": "4b349210-7f35-4dc0-905f-081d945bb8a8",
      "name": "Call 'attachmentsAnalyzer'",
      "type": "n8n-nodes-base.executeWorkflow",
      "onError": "continueErrorOutput",
      "position": [
        416,
        416
      ],
      "parameters": {
        "options": {},
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "<your-attachments-analyzer-subworkflow-id>",
          "cachedResultUrl": "/workflow/<your-attachments-analyzer-subworkflow-id>",
          "cachedResultName": "<your-attachments-analyzer-workflow-name>"
        },
        "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",
    "errorWorkflow": "",
    "timeSavedMode": "fixed",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "c54e1570-ebe1-40e2-986d-9d2cfe3f3d58",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "SetVars",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SetVars": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Parse Agent Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mattermost": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "HTTP Request": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Create an issue": {
      "main": [
        [
          {
            "node": "Post a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Agent Output": {
      "main": [
        [
          {
            "node": "Create an issue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Qdrant Vector Store": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "ReadIncidentContext": {
      "main": [
        [
          {
            "node": "Call 'attachmentsAnalyzer'",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings Google Gemini": {
      "ai_embedding": [
        [
          {
            "node": "Qdrant Vector Store",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Call 'attachmentsAnalyzer'": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "ReadIncidentContext",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}