This workflow corresponds to n8n.io template #qa-jira-pipeline-v1 — we link there as the canonical source.
This workflow follows the HTTP Request → Postgres recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"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"
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
jiraApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
How this works
This workflow streamlines quality assurance by automatically capturing new Jira stories, translating them if needed, and logging them into a PostgreSQL database for testing, ensuring your team never misses a requirement to verify. It's ideal for development teams in multilingual environments who rely on Jira for project management and want to integrate seamless QA processes without manual data entry. The key step involves extracting and upserting story details to the database after checking for duplicates, with optional Slack notifications to alert testers of new tasks.
Use this workflow when onboarding stories from Jira into a dedicated QA platform requires translation and deduplication to maintain an accurate test backlog, particularly in agile sprints with international contributors. Avoid it for non-Jira setups or when stories don't need database persistence, as it assumes a PostgreSQL backend. Common variations include adding AI-driven test case generation or routing to tools like TestRail instead of Slack for more advanced reporting.
About this workflow
QA Platform — Jira Story to Test Workflow. Uses jiraTrigger, postgres, httpRequest, slack. Webhook trigger; 20 nodes.
Source: https://github.com/GrimReaper4884/DapFlow/blob/828f98a87f8b0fa20ab0cc9c6ca26a03fd05f2cb/n8n/jira-workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow automates end-to-end research analysis by coordinating multiple AI models—including NVIDIA NIM (Llama), OpenAI GPT-4, and Claude to analyze uploaded documents, extract insights, and gene
Advanced Workflow with Branching and Error Handling. Uses emailSend, httpRequest, postgres, slack. Webhook trigger; 12 nodes.
HR teams, IT Operations, and System Administrators managing employee onboarding at scale. It’s perfect if you use Odoo 18 to trigger account requests and need Redmine + GitLab accounts created instant
This workflow is a complete, production-ready solution for recovering abandoned carts in Shopify stores using a multi-channel, multi-touch approach. It automates personalized follow-ups via Email, SMS
Are you tired of the repetitive dance between git push, creating a pull request in GitHub, updating the corresponding task in JIRA, and then manually notifying your team in Slack, or Notion?