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 →
{
"id": "BugSync001TuleapQC",
"name": "Tuleap Bug \u2192 QC-Manager Sync",
"active": true,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "tuleap-bug",
"responseMode": "responseNode",
"options": {}
},
"id": "b1234567-0001-4001-8001-000000000001",
"name": "Webhook: Tuleap Bug",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
304
]
},
{
"parameters": {
"jsCode": "// \u2500\u2500 FIXED: Handle both Tuleap webhook formats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Format A (legacy): application/x-www-form-urlencoded, body = { payload: '<JSON string>' }\n// Format B (current): application/json, body = { artifact, project, user }\n\nconst raw = $input.first().json;\n// In n8n v1+ with typeVersion 2, form fields are hoisted; check both locations\nconst body = (raw.body !== undefined && typeof raw.body === 'object') ? raw.body : raw;\n\nlet payload;\n\nif (body.payload !== undefined) {\n // Format A: payload key contains a JSON string (or already-parsed object)\n payload = typeof body.payload === 'string' ? JSON.parse(body.payload) : body.payload;\n // Validate the parsed payload has the expected shape\n if (!payload || typeof payload !== 'object') {\n throw new Error('Parsed payload is not a valid object');\n }\n} else if (body.artifact !== undefined) {\n // Format B: direct JSON \u2014 normalize to the same internal shape\n payload = {\n current: body.artifact,\n project: body.project || null,\n user: body.user || null,\n action: body.action || 'artifact:created'\n };\n} else {\n const keys = Object.keys(body).join(', ') || '(empty body)';\n throw new Error(`Unrecognized Tuleap webhook format. Body keys: ${keys}. Check Tuleap webhook configuration.`);\n}\n\n// Validate the artifact and tracker exist\nconst tracker = payload.tracker || payload.current?.tracker;\nif (!tracker?.id) {\n throw new Error(\n `Missing tracker.id in payload. ` +\n `tracker = ${JSON.stringify(tracker)}`\n );\n}\n\nreturn [{ json: { payload } }];"
},
"id": "b1234567-0002-4002-8002-000000000002",
"name": "Parse Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
304
]
},
{
"parameters": {
"url": "http://qc-api:3001/tuleap-webhook/config",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "tracker_type",
"value": "bug"
},
{
"name": "is_active",
"value": "true"
}
]
},
"options": {}
},
"id": "b1234567-0003-4003-8003-000000000003",
"name": "Fetch Sync Config",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
720,
304
]
},
{
"parameters": {
"jsCode": "// Find the sync config matching this tracker ID\nconst payload = $('Parse Payload').first().json.payload;\nconst configs = $('Fetch Sync Config').first().json.data || [];\nconst trackerId = payload?.tracker?.id;\n\nconst config = configs.find(c => String(c.tuleap_tracker_id) === String(trackerId));\nif (!config) {\n return [{ json: { hasConfig: false, trackerId, payload } }];\n}\nreturn [{ json: { hasConfig: true, config, payload } }];"
},
"id": "b1234567-0004-4004-8004-000000000004",
"name": "Match Config",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
304
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "has-config-check",
"leftValue": "={{ $json.hasConfig }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals",
"rightType": "boolean"
}
}
]
},
"options": {}
},
"id": "b1234567-0005-4005-8005-000000000005",
"name": "Has Config?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1200,
304
]
},
{
"parameters": {
"method": "POST",
"url": "http://qc-api:3001/tuleap-webhook/project",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ project_id: $('Match Config').first().json.payload.project?.id, name: $('Match Config').first().json.payload.project?.label || ('Tuleap Project ' + String($('Match Config').first().json.payload.project?.id || 'unknown')), path: $('Match Config').first().json.payload.project?.shortname || null }) }}",
"options": {
"timeout": 15000
}
},
"id": "b1234567-0010-4010-8010-000000000010",
"name": "Provision Project",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1440,
464
]
},
{
"parameters": {
"jsCode": "// Build a synthetic config from the auto-provisioned QC project\nconst parsed = $('Match Config').first().json;\nconst apiResp = $input.first().json;\n\nconst qcProject = apiResp.data;\nif (!qcProject?.id) {\n throw new Error(`Project provisioning failed. API response: ${JSON.stringify(apiResp).slice(0, 300)}`);\n}\n\nconst config = {\n qc_project_id: qcProject.id,\n qc_project_name: qcProject.project_name || null,\n tuleap_project_id: parsed.payload?.project?.id || null,\n tuleap_base_url: null,\n status_mappings: {}\n};\n\nreturn [{ json: { hasConfig: true, config, payload: parsed.payload } }];"
},
"id": "b1234567-0011-4011-8011-000000000011",
"name": "Build Config from Provision",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1680,
464
]
},
{
"parameters": {
"jsCode": "const { config, payload } = $input.first().json;\nconst artifact = payload.current;\nconst statusMappings = config.status_mappings || {};\n\nconst fields = {};\nfor (const v of (artifact.values || [])) { fields[v.label] = v; }\n\nfunction txt(...labels) {\n for (const lbl of labels) {\n const f = fields[lbl];\n if (!f) continue;\n if (f.value !== undefined && f.value !== null) return String(f.value);\n }\n return null;\n}\n\nfunction sel(...labels) {\n for (const lbl of labels) {\n const f = fields[lbl];\n if (!f) continue;\n if (f.values && f.values[0]) return f.values[0].label || f.values[0].display_name || String(f.values[0]);\n }\n return null;\n}\n\nfunction user(...labels) {\n for (const lbl of labels) {\n const f = fields[lbl];\n if (!f || !f.values) continue;\n const u = f.values[0];\n if (u) return u.display_name || u.real_name || u.username || null;\n }\n return null;\n}\n\nfunction mapStatus(s) {\n if (!s) return 'Open';\n return statusMappings[s] || { New:'Open', Open:'open', Assigned:'In Progress',\n Fixed:'Resolved', Resolved:'Resolved', Closed:'Closed',\n Rejected:'Closed', Reopened:'Reopened' }[s] || 'Open';\n}\n\nfunction mapSeverity(s) {\n if (!s) return 'medium';\n const lower = s.toLowerCase();\n if (lower.includes('critical')) return 'critical';\n if (lower.includes('major')) return 'high';\n if (lower.includes('minor')) return 'medium';\n if (lower.includes('cosmetic')) return 'low';\n if (lower.includes('high')) return 'high';\n if (lower.includes('medium')) return 'medium';\n if (lower.includes('normal')) return 'medium';\n if (lower.includes('low')) return 'low';\n return 'medium';\n}\n\nconst artifactId = payload.id || artifact.id;\nconst allValues = artifact.values || [];\n\n// Check if bug has links to Test Case trackers via art_link fields\nconst hasTestCaseLink = allValues.some(v =>\n v.type === 'art_link' &&\n (\n (Array.isArray(v.links) &&\n v.links.some(link => link.tracker && link.tracker.label === 'Test Case')) ||\n (Array.isArray(v.reverse_links) &&\n v.reverse_links.some(link => link.tracker && link.tracker.label === 'Test Case'))\n )\n);\n\nconst source = hasTestCaseLink ? 'TEST_CASE' : 'EXPLORATORY';\n\nconst bugData = {\n tuleap_artifact_id: artifactId,\n tuleap_tracker_id: payload.tracker?.id,\n tuleap_url: config.tuleap_base_url ? `${config.tuleap_base_url}/plugins/tracker/?aid=${artifactId}` : null,\n bug_id: `TLP-${artifactId}`,\n title: txt('Bug Title','Summary','Title','summary','title') || `Bug ${artifactId}`,\n description: txt('Description + Steps to reproduce','Description','Details','Original Submission','description'),\n status: mapStatus(sel('Status','status')),\n severity: mapSeverity(sel('Severity','severity','Importance')),\n priority: (sel('Priority','priority') || 'medium').toLowerCase(),\n bug_type: sel('Type','Category','Bug Type','type'),\n component: sel('Component','Module','component'),\n project_id: config.qc_project_id,\n linked_test_case_ids: [],\n linked_test_execution_ids: [],\n reported_by: payload.user?.display_name || null,\n updated_by: payload.user?.display_name || null,\n assigned_to: user('Assigned to','Assigned To','assigned_to'),\n reported_date: artifact.submitted_on || null,\n raw_tuleap_payload: payload,\n source: source,\n submitted_by_email: artifact.submitted_by?.email ?? null,\n submitted_by_username: artifact.submitted_by?.username ?? null\n};\n\nreturn [{ json: { bugData } }];"
},
"id": "b1234567-0006-4006-8006-000000000006",
"name": "Transform Bug Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1920,
304
]
},
{
"parameters": {
"method": "POST",
"url": "http://qc-api:3001/tuleap-webhook/bug",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.bugData) }}",
"options": {
"timeout": 30000
}
},
"id": "b1234567-0007-4007-8007-000000000007",
"name": "Send to QC API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2160,
304
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ success: true, message: 'Bug processed', bug_id: $json.data?.id || 'unknown' }) }}",
"options": {}
},
"id": "b1234567-0008-4008-8008-000000000008",
"name": "Respond: OK",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2400,
304
]
}
],
"connections": {
"Webhook: Tuleap Bug": {
"main": [
[
{
"node": "Parse Payload",
"type": "main",
"index": 0
}
]
]
},
"Parse Payload": {
"main": [
[
{
"node": "Fetch Sync Config",
"type": "main",
"index": 0
}
]
]
},
"Fetch Sync Config": {
"main": [
[
{
"node": "Match Config",
"type": "main",
"index": 0
}
]
]
},
"Match Config": {
"main": [
[
{
"node": "Has Config?",
"type": "main",
"index": 0
}
]
]
},
"Has Config?": {
"main": [
[
{
"node": "Transform Bug Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Provision Project",
"type": "main",
"index": 0
}
]
]
},
"Provision Project": {
"main": [
[
{
"node": "Build Config from Provision",
"type": "main",
"index": 0
}
]
]
},
"Build Config from Provision": {
"main": [
[
{
"node": "Transform Bug Data",
"type": "main",
"index": 0
}
]
]
},
"Transform Bug Data": {
"main": [
[
{
"node": "Send to QC API",
"type": "main",
"index": 0
}
]
]
},
"Send to QC API": {
"main": [
[
{
"node": "Respond: OK",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [
{
"name": "tuleap"
}
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Tuleap Bug → QC-Manager Sync. Uses httpRequest. Webhook trigger; 10 nodes.
Source: https://github.com/Gebrilo/QC-Manager/blob/48bdcc4c151775e3e075bfe1a53fb58cd5945d9d/n8n-workflows/tuleap-bug-sync.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 n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c
Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.
📡 This workflow serves as the central Alpha Vantage API fetcher for Tesla trading indicators, delivering cleaned 20-point JSON outputs for three timeframes: , , and . It is required by the following a