{
  "id": "Qqtv43Or6stmBsmt",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "JIRA Ticket Quality Checker",
  "tags": [],
  "nodes": [
    {
      "id": "3b59846b-b1cd-4816-a405-a7786a110797",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 320,
        "content": "## Parse and route score\n\nExtracts the numeric quality score from the AI output, then checks whether it passes or fails the configured threshold."
      },
      "typeVersion": 1
    },
    {
      "id": "fe7d4f6f-f24a-43cc-b796-214ec325646c",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1104,
        400
      ],
      "parameters": {
        "color": 7,
        "height": 576,
        "content": "## Comment or ignore\n\nHandles the final branch: low-quality tickets receive a Jira comment, while acceptable tickets end with no further action."
      },
      "typeVersion": 1
    },
    {
      "id": "c955ddef-8743-4acd-915e-1ce2aca310f1",
      "name": "When JIRA Ticket Created",
      "type": "n8n-nodes-base.jiraTrigger",
      "position": [
        -240,
        704
      ],
      "parameters": {
        "events": [
          "jira:issue_created"
        ],
        "additionalFields": {
          "filter": "issuetype in (Story, Bug, Task)"
        }
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "652534d6-e1ed-48b0-9d43-175800d561d2",
      "name": "Quality Analysis Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        336,
        704
      ],
      "parameters": {
        "text": "=Evaluate this Jira ticket:\n\n<ticket>\nType: {{ $('When JIRA Ticket Created').first().json.issue.fields.issuetype.name }}\nTitle: {{ $('When JIRA Ticket Created').first().json.issue.fields.summary }}\nDescription: {{ $('When JIRA Ticket Created').first().json.issue.fields.description ?? \"No description provided.\" }}\nReporter: {{ $('When JIRA Ticket Created').first().json.issue.fields.reporter.displayName }}\n</ticket>\n\nReturn the evaluation JSON.",
        "options": {
          "systemMessage": "You are a Jira ticket quality reviewer. Your role is to evaluate whether \na ticket contains enough information for a developer to start working on it \nwithout asking clarifying questions.\n\nEvaluate the ticket on these 5 criteria:\n\n1. **Title clarity** \u2014 Is the title specific and action-oriented? (not \"Fix bug\" or \"Update page\")\n2. **Description completeness** \u2014 Does it explain the context, the problem, and the expected behavior?\n3. **Acceptance criteria** \u2014 Are there clear, testable conditions for \"done\"?\n4. **Reproduction steps** \u2014 For bugs: are steps to reproduce present? For stories: is there a user scenario?\n5. **Scope & dependencies** \u2014 Are unknowns, blockers, or related tickets mentioned?\n\nFor each criterion, assign a score from 1 to 3:\n- 1 = Missing or too vague to be useful\n- 2 = Present but incomplete\n- 3 = Clear and sufficient\n\nOutput a JSON object with this exact structure:\n{\n  \"scores\": {\n    \"title_clarity\": 1-3,\n    \"description_completeness\": 1-3,\n    \"acceptance_criteria\": 1-3,\n    \"reproduction_steps\": 1-3,\n    \"scope_dependencies\": 1-3\n  },\n  \"total\": 5-15,\n  \"normalized_score\": 1-10,\n  \"missing\": [\"list of the criteria scored 1\"],\n  \"comment\": \"2-3 sentence coaching comment addressed directly to the ticket author. Friendly tone. Name exactly what is missing and why it matters. Do not mention the score.\"\n}\n\nReturn only the JSON object. No preamble, no markdown fences."
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "e7b66d3d-fc8c-4c30-bc92-46b97191b22e",
      "name": "Add Comment to JIRA Ticket",
      "type": "n8n-nodes-base.jira",
      "position": [
        1152,
        608
      ],
      "parameters": {
        "comment": "={{ $json.comment }}\n\n**Quality score: {{ $json.normalized_score }}/10**\nMissing: {{ $json.missing.join(', ') }}\n\n_This comment was generated automatically to help keep our backlog clean._",
        "options": {},
        "issueKey": "={{ $('When JIRA Ticket Created').item.json.issue.key }}",
        "resource": "issueComment"
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "64a21e7e-36c9-4b70-9a44-4a5adf05395d",
      "name": "Set Score Threshold",
      "type": "n8n-nodes-base.set",
      "position": [
        16,
        704
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f1719701-bb91-4fcf-b024-18e45ef3f41c",
              "name": "SCORE_THRESHOLD",
              "type": "number",
              "value": 7
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "203d07c6-9f28-4682-a6f7-e1b036c8f58c",
      "name": "OpenAI Quality Evaluator",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        320,
        960
      ],
      "parameters": {
        "model": "openai/gpt-oss-20b:free",
        "options": {
          "maxTokens": -1,
          "temperature": 0.9
        }
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1f7c24d1-257a-429d-a920-b119a5d37d56",
      "name": "Parse Normalized Score",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        704
      ],
      "parameters": {
        "jsCode": "const response = $input.first().json.output ?? $input.first().json.text ?? \"\";\n\nconst cleaned = response\n  .replace(/^```json\\s*/i, \"\")\n  .replace(/^```\\s*/i, \"\")\n  .replace(/\\s*```$/i, \"\")\n  .trim();\n\nlet scores;\n\ntry {\n  scores = JSON.parse(cleaned);\n} catch (error) {\n  throw new Error(`Invalid JSON returned by Quality Checker Agent.\\n${error.message}`);\n}\n\nreturn scores;"
      },
      "typeVersion": 2
    },
    {
      "id": "6b9ec50c-d7b7-46ba-bf74-fefbdd5d83d0",
      "name": "Check Score Threshold",
      "type": "n8n-nodes-base.if",
      "position": [
        928,
        704
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "d7d49325-a7a0-4030-a53f-af4f2842798f",
              "operator": {
                "type": "number",
                "operation": "lt"
              },
              "leftValue": "={{ $json.normalized_score }}",
              "rightValue": "={{ $('Set Score Threshold').item.json.SCORE_THRESHOLD }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "b5f709f5-4465-40c9-baf9-4251e8d6d1ae",
      "name": "No Action Needed",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1152,
        816
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "69250667-dd1f-4cff-89ca-69f0ff6d301b",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        400
      ],
      "parameters": {
        "width": 480,
        "height": 752,
        "content": "## JIRA Ticket Quality Checker\n\n### How it works\n\nThis workflow listens for Jira ticket events, sets a quality score threshold, and sends the ticket details to an AI quality-checking agent. It parses the AI response into a score, compares it with the configured threshold, and either adds a Jira comment with feedback or exits without action.\n\n### Setup steps\n\n- Configure Jira credentials for the Jira Trigger and Add a comment nodes, including the target Jira site/project events to monitor.\n- Configure the OpenRouter/OpenAI chat model credentials used by the Open AI Quality Checker sub-node.\n- Set the SCORE_THRESHOLD value in the Config node to define the minimum acceptable ticket quality score.\n- Verify the AI agent prompt and Score Parser Code output format agree on how the quality score is returned and parsed.\n\n### Customization\n\nAdjust the SCORE_THRESHOLD, AI evaluation prompt, and Jira comment text to match your team\u2019s definition of ticket quality and desired feedback style."
      },
      "typeVersion": 1
    },
    {
      "id": "4061410e-7795-40a8-b3d8-9ed6ca2346d1",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Receive Jira ticket\n\nStarts the workflow when a Jira event occurs and prepares the configurable quality threshold for downstream checks."
      },
      "typeVersion": 1
    },
    {
      "id": "64de4da4-82a0-4733-985c-c3523f66c50e",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 560,
        "content": "## Evaluate ticket quality\n\nUses an AI agent and its connected OpenRouter chat model to assess the Jira ticket\u2019s quality against the workflow\u2019s criteria."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "74af3881-4b5f-432f-b30c-b4a5fa48ebe6",
  "connections": {
    "Set Score Threshold": {
      "main": [
        [
          {
            "node": "Quality Analysis Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Score Threshold": {
      "main": [
        [
          {
            "node": "Add Comment to JIRA Ticket",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Action Needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Normalized Score": {
      "main": [
        [
          {
            "node": "Check Score Threshold",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quality Analysis Agent": {
      "main": [
        [
          {
            "node": "Parse Normalized Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Quality Evaluator": {
      "ai_languageModel": [
        [
          {
            "node": "Quality Analysis Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "When JIRA Ticket Created": {
      "main": [
        [
          {
            "node": "Set Score Threshold",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}