{
  "name": "QA Platform \u2014 Jira Story to Test Workflow",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "jira-story",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "n1-jira-webhook",
      "name": "Jira Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyX",
              "value": 5,
              "unit": "minutes"
            }
          ]
        },
        "jql": "project={{ $env.JIRA_PROJECT_KEY }} AND updated >= -5m AND issuetype = Story",
        "simplifyOutput": true
      },
      "id": "n2-jira-poll",
      "name": "Jira Poller (Fallback)",
      "type": "n8n-nodes-base.jiraTrigger",
      "typeVersion": 1,
      "position": [
        240,
        460
      ],
      "disabled": true,
      "credentials": {
        "jiraApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT p.id, p.name, p.slug, p.locale, p.qdrant_collection FROM projects p WHERE p.jira_project_key = '{{ $json.issue?.fields?.project?.key || $json.project_key }}' LIMIT 1",
        "options": {}
      },
      "id": "n3-resolve-project",
      "name": "Resolve Project by Jira Key",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        480,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const project = $input.first().json;\nif (!project || !project.id) {\n  // Stop execution \u2014 project not configured\n  return [];\n}\n\nconst webhook = $('Jira Webhook').first().json;\nconst issue = webhook.issue || webhook;\n\nconst story = {\n  project_id: project.id,\n  project_slug: project.slug,\n  project_locale: project.locale || ['en'],\n  qdrant_collection: project.qdrant_collection || `spa_${project.slug}`,\n  jira_key: issue.key,\n  title: issue.fields?.summary || issue.title,\n  description: issue.fields?.description || issue.description || '',\n  acceptance_criteria: issue.fields?.customfield_10016 || '',\n  story_type: issue.fields?.issuetype?.name || 'Story',\n  updated_at: issue.fields?.updated || new Date().toISOString()\n};\n\nreturn [{ json: story }];"
      },
      "id": "n4-extract-story",
      "name": "Extract Story Fields",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT id FROM jira_stories WHERE jira_key = '{{ $json.jira_key }}' AND updated_at >= '{{ $json.updated_at }}'::timestamptz LIMIT 1",
        "options": {}
      },
      "id": "n5-check-duplicate",
      "name": "Check for Duplicate",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        960,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.id }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "empty"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "n5b-if-new",
      "name": "Is New Story?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1200,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://python-agents:8001/agent/translate",
        "sendBody": true,
        "bodyContentType": "json",
        "jsonBody": "={{ JSON.stringify({ title: $node['Extract Story Fields'].json.title, description: $node['Extract Story Fields'].json.description, acceptance_criteria: $node['Extract Story Fields'].json.acceptance_criteria }) }}",
        "options": {
          "timeout": 60000,
          "retry": {
            "enabled": true,
            "maxRetries": 2,
            "waitBetweenRetries": 5000
          }
        }
      },
      "id": "n6-translate",
      "name": "Detect Language & Translate",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1440,
        200
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO jira_stories (id, project_id, jira_key, title, description, acceptance_criteria, story_type, detected_lang, translated_title, translated_description, status)\nVALUES (gen_random_uuid(), '{{ $node['Extract Story Fields'].json.project_id }}', '{{ $node['Extract Story Fields'].json.jira_key }}', '{{ $node['Extract Story Fields'].json.title.replace(\"'\", \"''\") }}', '{{ $node['Extract Story Fields'].json.description.replace(\"'\", \"''\") }}', '{{ $node['Extract Story Fields'].json.acceptance_criteria.replace(\"'\", \"''\") }}', '{{ $node['Extract Story Fields'].json.story_type }}', '{{ $json.detected_lang }}', '{{ $json.translated_title?.replace(\"'\", \"''\") || '' }}', '{{ $json.translated_description?.replace(\"'\", \"''\") || '' }}', 'analyzing')\nON CONFLICT (jira_key) DO UPDATE SET title = EXCLUDED.title, description = EXCLUDED.description, detected_lang = EXCLUDED.detected_lang, translated_title = EXCLUDED.translated_title, status = 'analyzing', updated_at = NOW()\nRETURNING id",
        "options": {}
      },
      "id": "n7-upsert-story",
      "name": "Upsert Story to DB",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        1680,
        200
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://python-agents:8001/agent/analyze-story",
        "sendBody": true,
        "bodyContentType": "json",
        "jsonBody": "={{ JSON.stringify({ jira_key: $node['Extract Story Fields'].json.jira_key, title: $node['Extract Story Fields'].json.title, description: $node['Extract Story Fields'].json.description, acceptance_criteria: $node['Extract Story Fields'].json.acceptance_criteria, story_type: $node['Extract Story Fields'].json.story_type, project_id: $node['Extract Story Fields'].json.project_id, detected_lang: $node['Detect Language & Translate'].json.detected_lang, translated_title: $node['Detect Language & Translate'].json.translated_title }) }}",
        "options": {
          "timeout": 120000,
          "retry": {
            "enabled": true,
            "maxRetries": 2,
            "waitBetweenRetries": 10000
          }
        }
      },
      "id": "n8-analyze-story",
      "name": "Analyze Story (Story Analyst Agent)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1920,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "const storyAnalysis = $input.first().json;\nconst projectSlug = $node['Extract Story Fields'].json.project_slug;\n\n// For each step, call match endpoint\nconst matchPromises = storyAnalysis.steps.map(async (step) => {\n  const query = `${step.description_en} ${step.target_hint || ''}`;\n  const response = await $helpers.httpRequest({\n    method: 'POST',\n    url: 'http://go-gateway:8080/api/v1/search/match',\n    body: JSON.stringify({ query, project_slug: projectSlug }),\n    headers: { 'Content-Type': 'application/json' }\n  });\n  return { ...step, ...response };\n});\n\nconst stepsWithMatches = await Promise.all(matchPromises);\nreturn [{ json: { steps_with_matches: stepsWithMatches, api_endpoints: storyAnalysis.api_endpoints, project_locale: $node['Resolve Project by Jira Key'].json.locale } }];"
      },
      "id": "n9-match-elements",
      "name": "Match Elements for Each Step",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2160,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://python-agents:8001/agent/route",
        "sendBody": true,
        "bodyContentType": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "n10-route",
      "name": "Router Agent",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2400,
        200
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE jira_stories SET workflow_type = '{{ $json.overall_workflow_type }}', matched_elements = ARRAY[{{ $json.routing_per_step.filter(s => s.matched_test_id).map(s => `'${s.matched_test_id}'`).join(',') }}], status = 'matched', updated_at = NOW() WHERE jira_key = '{{ $node['Extract Story Fields'].json.jira_key }}'",
        "options": {}
      },
      "id": "n11-save-routing",
      "name": "Save Routing Decision",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        2640,
        200
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.overall_workflow_type }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "playwright"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "playwright"
            },
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.overall_workflow_type }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "api"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "api"
            },
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.overall_workflow_type }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "manual"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "manual"
            },
            {
              "conditions": {
                "conditions": [
                  {
                    "leftValue": "={{ $json.overall_workflow_type }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "mixed"
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "mixed"
            }
          ]
        }
      },
      "id": "n12-switch-type",
      "name": "Branch by Workflow Type",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        2880,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://python-agents:8001/agent/generate/playwright",
        "sendBody": true,
        "bodyContentType": "json",
        "jsonBody": "={{ JSON.stringify({ jira_key: $node['Extract Story Fields'].json.jira_key, steps_with_matches: $node['Router Agent'].json.routing_per_step, auth_required: $node['Router Agent'].json.auth_required, project_locale: $node['Extract Story Fields'].json.project_locale, playwright_steps: $node['Router Agent'].json.playwright_steps }) }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "n13a-gen-playwright",
      "name": "Generate Playwright Test",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3120,
        100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://python-agents:8001/agent/generate/api",
        "sendBody": true,
        "bodyContentType": "json",
        "jsonBody": "={{ JSON.stringify({ jira_key: $node['Extract Story Fields'].json.jira_key, api_steps: $node['Router Agent'].json.api_steps, api_endpoints: $node['Analyze Story (Story Analyst Agent)'].json.api_endpoints }) }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "n13b-gen-api",
      "name": "Generate API Test",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3120,
        260
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://python-agents:8001/agent/generate/manual",
        "sendBody": true,
        "bodyContentType": "json",
        "jsonBody": "={{ JSON.stringify({ jira_key: $node['Extract Story Fields'].json.jira_key, steps: $node['Analyze Story (Story Analyst Agent)'].json.steps, detected_lang: $node['Detect Language & Translate'].json.detected_lang }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "n13c-gen-manual",
      "name": "Generate Manual Checklist",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3120,
        420
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO test_workflows (id, story_id, project_id, workflow_type, generated_script, api_test_spec, manual_steps, match_confidence, status)\nSELECT gen_random_uuid(), js.id, js.project_id, js.workflow_type,\n  CASE WHEN js.workflow_type IN ('playwright','mixed') THEN '{{ $json.script }}' END,\n  CASE WHEN js.workflow_type IN ('api','mixed') THEN '{{ JSON.stringify($json.api_spec) }}'::jsonb END,\n  CASE WHEN js.workflow_type = 'manual' THEN '{{ JSON.stringify($json.manual_steps) }}'::jsonb END,\n  {{ $node['Router Agent'].json.confidence }},\n  'ready'\nFROM jira_stories js WHERE js.jira_key = '{{ $node['Extract Story Fields'].json.jira_key }}'",
        "options": {}
      },
      "id": "n14-save-workflow",
      "name": "Save Test Workflow to DB",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        3360,
        260
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "webhookUri": "={{ $env.SLACK_WEBHOOK_URL }}",
        "text": "\u26a1 *Test Generated* \u2014 {{ $node['Extract Story Fields'].json.jira_key }}\n\ud83d\udccb Workflow: `{{ $node['Router Agent'].json.overall_workflow_type.toUpperCase() }}`\n\ud83c\udfaf Confidence: {{ Math.round($node['Router Agent'].json.confidence * 100) }}%\n\u2705 Matched {{ $node['Router Agent'].json.routing_per_step.filter(s => s.matched_test_id).length }}/{{ $node['Router Agent'].json.routing_per_step.length }} elements\n\ud83d\udd17 <http://localhost:3002/stories/{{ $node['Extract Story Fields'].json.jira_key }}|View in Stories UI>"
      },
      "id": "n15-slack-notify",
      "name": "Slack Workflow Notification",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.1,
      "position": [
        3600,
        260
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "leftValue": "={{ $node['Router Agent'].json.needs_review_steps.length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        }
      },
      "id": "n16-needs-review",
      "name": "Has Review Needed Steps?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3840,
        260
      ]
    },
    {
      "parameters": {
        "webhookUri": "={{ $env.SLACK_WEBHOOK_URL }}",
        "text": "\u26a0\ufe0f *Manual Review Required* \u2014 {{ $node['Extract Story Fields'].json.jira_key }}\n{{ $node['Router Agent'].json.needs_review_steps.length }} steps need human verification:\n{{ $node['Router Agent'].json.needs_review_steps.map(s => `\u2022 Step ${s.step_number}: ${s.description_en} (confidence: ${Math.round(s.confidence*100)}%)`).join('\\n') }}"
      },
      "id": "n17-slack-review",
      "name": "Slack Review Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.1,
      "position": [
        4080,
        200
      ]
    }
  ],
  "connections": {
    "Jira Webhook": {
      "main": [
        [
          {
            "node": "Resolve Project by Jira Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Jira Poller (Fallback)": {
      "main": [
        [
          {
            "node": "Resolve Project by Jira Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Project by Jira Key": {
      "main": [
        [
          {
            "node": "Extract Story Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Story Fields": {
      "main": [
        [
          {
            "node": "Check for Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Duplicate": {
      "main": [
        [
          {
            "node": "Is New Story?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is New Story?": {
      "main": [
        [
          {
            "node": "Detect Language & Translate",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Detect Language & Translate": {
      "main": [
        [
          {
            "node": "Upsert Story to DB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Story to DB": {
      "main": [
        [
          {
            "node": "Analyze Story (Story Analyst Agent)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Story (Story Analyst Agent)": {
      "main": [
        [
          {
            "node": "Match Elements for Each Step",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Elements for Each Step": {
      "main": [
        [
          {
            "node": "Router Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Router Agent": {
      "main": [
        [
          {
            "node": "Save Routing Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Routing Decision": {
      "main": [
        [
          {
            "node": "Branch by Workflow Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Branch by Workflow Type": {
      "main": [
        [
          {
            "node": "Generate Playwright Test",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate API Test",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Manual Checklist",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Playwright Test",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate API Test",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Playwright Test": {
      "main": [
        [
          {
            "node": "Save Test Workflow to DB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate API Test": {
      "main": [
        [
          {
            "node": "Save Test Workflow to DB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Manual Checklist": {
      "main": [
        [
          {
            "node": "Save Test Workflow to DB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Test Workflow to DB": {
      "main": [
        [
          {
            "node": "Slack Workflow Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Workflow Notification": {
      "main": [
        [
          {
            "node": "Has Review Needed Steps?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Review Needed Steps?": {
      "main": [
        [
          {
            "node": "Slack Review Alert",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "executionTimeout": 600,
    "timezone": "UTC"
  },
  "meta": {
    "templateId": "qa-jira-pipeline-v1",
    "description": "Full Jira story pipeline: webhook \u2192 detect lang \u2192 analyze \u2192 match elements \u2192 route \u2192 generate tests \u2192 notify"
  }
}