Automatische Erstellung von Jira-Tickets aus einer Streamlit-App
Dies ist ein Project Management-Bereich Automatisierungsworkflow mit 16 Nodes. Hauptsächlich werden If, Set, Code, Webhook, HttpRequest und andere Nodes verwendet. Jira-Tickets aus einem Streamlit-Formular via Webhook und REST API erstellen
- •HTTP Webhook-Endpunkt (wird von n8n automatisch generiert)
- •Möglicherweise sind Ziel-API-Anmeldedaten erforderlich
Verwendete Nodes (16)
Kategorie
{
"id": "2DxalJmssGiuQC9h",
"meta": {
"instanceId": "0430772da25f7bca29bf5ef2b251086a85fb4096503a6f781526d32befd038d6",
"templateCredsSetupCompleted": true
},
"name": "Automate Jira Ticket Creation from Your Streamlit App",
"tags": [],
"nodes": [
{
"id": "26e7793d-e192-4075-8873-4c97fa2fe616",
"name": "Webhook streamlit",
"type": "n8n-nodes-base.webhook",
"position": [
-352,
16
],
"webhookId": "b8bb8a6a-5f41-4199-88f6-35b8937c4a82",
"parameters": {
"path": "your path here",
"options": {},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 2.1
},
{
"id": "e3d42140-21c8-414e-92bb-9b370db03cd8",
"name": "jira response",
"type": "n8n-nodes-base.set",
"position": [
1488,
0
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{$json}}"
},
"typeVersion": 3.4
},
{
"id": "99fc511f-63a6-4bae-bd76-c71e7d8cd41f",
"name": "Jira HTTP request",
"type": "n8n-nodes-base.httpRequest",
"position": [
1248,
0
],
"parameters": {
"body": "={{ JSON.stringify($json) }}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"rawContentType": "application/json",
"headerParameters": {
"parameters": [
{
"name": "=Content-Type",
"value": "=application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "ed82c023-8052-4431-81cc-1474cdd41d7d",
"name": "Rohdaten von streamlit",
"type": "n8n-nodes-base.set",
"position": [
80,
16
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{ $json }}"
},
"typeVersion": 3.4
},
{
"id": "561cad41-bb3f-4c79-8040-dd8eea591ecf",
"name": "Streamlit-Daten verarbeiten",
"type": "n8n-nodes-base.code",
"position": [
304,
16
],
"parameters": {
"jsCode": "const input = $json;\n\n// Journal to debug\nconsole.log(\"=== DATA RECEIVED ===\");\nconsole.log(JSON.stringify(input, null, 2));\n\n// stop if request empty\nconst quickCheck = input.body?.ticket?.summary || input.ticket?.summary || input.summary;\nif (!quickCheck || quickCheck.toString().trim() === \"\") {\n console.log(\"ARRÊT : Empty request detected\");\n return [{ json: { blocked: true, reason: \"empty_request\" } }];\n}\n\n// check: stop if no useful data\nif (!input || Object.keys(input).length === 0) {\n console.log(\"ARRÊT : No data received\");\n return [{ json: { blocked: true, reason: \"no_data\" } }];\n}\n\n// Extraction\nconst getValue = (obj, keys, defaultValue = null) => {\n for (const key of keys) {\n if (obj && obj[key] !== undefined && obj[key] !== null && obj[key] !== \"\") {\n return obj[key];\n }\n }\n return defaultValue;\n};\n\n// In case if data are in input.ticket\nconst ticketData = input.ticket || input.body?.ticket || input;\n\n// Extract with strict validation\nconst projectKey = getValue(ticketData, ['projectKey', 'project'], \"TES\");\nconst summary = getValue(ticketData, ['summary', 'title', 'name']);\nconst type = getValue(ticketData, ['type', 'issuetype'], \"Task\");\nconst description = getValue(ticketData, ['description', 'desc']);\nconst priority = getValue(ticketData, ['priority', 'severity']);\nconst storyPoints = getValue(ticketData, ['story_points', 'storyPoints', 'points']);\nconst assignee = getValue(ticketData, ['assignee', 'assignedTo', 'owner']);\n\n// critical validation, stop if summary is empty\nif (!summary || summary.trim() === \"\" || summary === \"Ticket without title\") {\n console.log(\"ARRÊT : Empty Summary or by default detected\");\n return [{ json: { blocked: true, reason: \"no_summary\" } }];\n}\n\n// critical validation, stop if description is empty\nif (!description || description.trim() === \"\" || description === \"Aucune description fournie\") {\n console.log(\"STOP : Empty Description or by default detected\");\n return [{ json: { blocked: true, reason: \"no_description\" } }];\n}\n\nconsole.log(\"=== Values Validated ===\");\nconsole.log(JSON.stringify({\n projectKey,\n summary,\n type,\n description,\n priority,\n storyPoints,\n assignee\n}, null, 2));\n\n// payload only if data are ok\nconst payload = {\n fields: {\n project: { key: projectKey.toString().toUpperCase() },\n summary: summary.toString().trim(),\n issuetype: { name: type.toString() },\n description: {\n type: \"doc\",\n version: 1,\n content: [{\n type: \"paragraph\",\n content: [{ type: \"text\", text: description.toString().trim() }]\n }]\n }\n }\n};\n\n\nif (priority && priority !== \"null\" && priority.trim() !== \"\") {\n payload.fields.priority = { name: priority.toString().trim() };\n}\n\n// Story points : ID can change depending on Jira\nif (storyPoints !== null && storyPoints !== undefined && !isNaN(parseInt(storyPoints))) {\npayload.fields.customfield_10016 = parseInt(storyPoints);\n}\n\n// Assignee deleted : needs accountId in Jira Cloud, not 'name'\n// if (assignee && assignee.trim() !== \"\") {\n// payload.fields.assignee = { name: assignee.toString().trim() };\n// }\n\nconsole.log(\"=== FINAL PAYLOAD VALIDATED ===\");\nconsole.log(JSON.stringify(payload, null, 2));\n\nreturn [{ json: payload }];"
},
"typeVersion": 2
},
{
"id": "08a64f5f-a115-492b-a74d-729a26e91964",
"name": "Verarbeitete Daten",
"type": "n8n-nodes-base.set",
"position": [
928,
0
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{ $json }}"
},
"typeVersion": 3.4
},
{
"id": "04a874db-e6f5-45b0-9c24-c1353fbe82c4",
"name": "Ergebnis",
"type": "n8n-nodes-base.code",
"position": [
1744,
0
],
"parameters": {
"jsCode": "return [{ json: {\n ok: true,\n jiraKey: $json.key,\n url: `https://YOURJIRAURL.atlassian.net/browse/${$json.key}`\n}}];\n"
},
"typeVersion": 2
},
{
"id": "f73b22ee-e14e-4d0c-a638-86c79c79b540",
"name": "Anti-Double",
"type": "n8n-nodes-base.code",
"position": [
-144,
16
],
"parameters": {
"jsCode": "// Block empty request\nconst hasValidSummary = $json.body?.ticket?.summary || $json.ticket?.summary || $json.summary;\n\nif (!hasValidSummary || hasValidSummary.trim() === \"\") {\n console.log(\"Empty request detected - blocked\");\n return [{ json: { blocked: true, reason: \"empty_request\" } }];\n}\n\n// allow valid data\nreturn items;"
},
"typeVersion": 2
},
{
"id": "eb9ab21e-f39e-4f79-b7e1-7ea092e2d8e7",
"name": "Blockierte Anfrage",
"type": "n8n-nodes-base.code",
"position": [
624,
416
],
"parameters": {
"jsCode": "return [{ json: { ok: false, message: \"Empty request blocked\" } }];"
},
"typeVersion": 2
},
{
"id": "1d1a8b2d-6cbf-4844-9ca3-112d1786ae53",
"name": "If für Doubles",
"type": "n8n-nodes-base.if",
"position": [
496,
16
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b785d1c2-020d-4456-87cc-9d2389a30635",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
},
"leftValue": "={{ $json.blocked }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "25ce6d92-a06e-46fc-9cd4-61a3a598b9ab",
"name": "Notiz1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-976,
-704
],
"parameters": {
"color": 3,
"width": 304,
"height": 176,
"content": "## Required\n\n\n- Streamlit creation ticket app\n- Jira account"
},
"typeVersion": 1
},
{
"id": "f81324d8-fd8e-485a-8398-72768c64ff09",
"name": "Notiz",
"type": "n8n-nodes-base.stickyNote",
"position": [
-624,
-704
],
"parameters": {
"color": 2,
"width": 416,
"height": 1264,
"content": "## 1) Trigger & intake (Streamlit → n8n)\n\nPurpose: receive the ticket from the app and hand it to the workflow.\n\nNodes\n\nWebhook streamlit – receives action=create_ticket and a ticket object (id, projectKey, type, summary, description, priority, story_points, due_date, labels…).\n\nraw data from streamlit (Set) – optional pass-through to visualize the raw payload in executions.\n\nSetup\n\nWebhook → Response mode: lastNode (so the final node’s response goes back to Streamlit).\n\nUse the Production URL in the app (avoid the Test URL in production).\n\nDon’t run the Jira HTTP node manually; always trigger through the Webhook."
},
"typeVersion": 1
},
{
"id": "5e0bc57b-223c-46fe-bd90-5682ef4c5e67",
"name": "Notiz3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1152,
-704
],
"parameters": {
"color": 4,
"width": 464,
"height": 1264,
"content": "## 4) Create issue in Jira\n\nPurpose: call Jira REST API and create the real ticket.\n\nNodes\n\nProcessed data (Set) – passes through the fields object so you can inspect it.\n\nJira HTTP request (HTTP Request)\n\nMethod: POST\n\nURL: https://<your-domain>.atlassian.net/rest/api/3/issue\n\nAuth: Jira Software Cloud API (email + API token)\n\nHeaders: Content-Type: application/json (Accept optional)\n\nBody: Raw JSON = { \"fields\": … } from the previous node (not a stringified “[object Object]”).\n\nExpected output\n\nJira returns JSON with the new issue key (e.g., TES-123).\n"
},
"typeVersion": 1
},
{
"id": "5175079e-7ddc-46f1-bf93-532421c2e16a",
"name": "Notiz2",
"type": "n8n-nodes-base.stickyNote",
"position": [
816,
-704
],
"parameters": {
"color": 5,
"width": 304,
"height": 1264,
"content": "## 3) Normalize & build Jira payload\n\nPurpose: transform app fields into a valid Jira JSON.\n\nNodes\n\nProcess streamlit data (Code) – outputs a clean fields object:\n\nproject.key (uppercase from projectKey)\n\nissuetype.name (Task/Story/Bug/Epic)\n\nsummary\n\ndescription in Atlassian document format (doc → paragraph → text)\n\nOptional: priority.name, duedate (YYYY-MM-DD), labels (array)\n\nOptional: customfield_10016 for Story Points (or your instance’s ID)\n\nNotes\n\nDon’t set assignee.name in Jira Cloud; use assignee.accountId only if you have it."
},
"typeVersion": 1
},
{
"id": "953b841a-8814-4047-bce8-a8644d266005",
"name": "Notiz4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-176,
-704
],
"parameters": {
"color": 6,
"width": 944,
"height": 1264,
"content": "## 2) De-dup guard & branching (count/IF)\n\nPurpose: prevent empty/invalid calls and duplicates from creating issues.\n\nNodes\n\nanti double (Code) – builds a short-lived memory in workflow static data.\n\nIf ticket.id already seen (or fingerprint of projectKey+type+summary+description), mark as duplicate.\n\nIf action isn’t create_ticket or required fields are missing, mark as invalid.\n\nIF for doubles\n\nTrue branch (duplicate/invalid) → blocked request (Code) returns { ok: true, duplicate: true | ignored: true } to the Webhook and stops.\n\nFalse branch (clean request) → continue.\n\nValidation\n\nSubmit the same form twice quickly → True path should fire once (no second Jira issue).\n\n“Test connection” from the app should be ignored and not create anything."
},
"typeVersion": 1
},
{
"id": "d933ae95-f6f6-4044-85de-f9a5fcca67e3",
"name": "Notiz5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1664,
-704
],
"parameters": {
"height": 1264,
"content": "## 5) Return result to the app\n\nPurpose: send a friendly response back to Streamlit.\n\nNodes\n\njira response (Set) – optional pass-through of the Jira response for visibility.\n\nResult (Code) – returns to the Webhook:\n{ ok: true, jiraKey: <KEY>, url: https://<domain>.atlassian.net/browse/<KEY> }\n\nUser experience\n\nStreamlit shows the created key and a link to open the ticket.\n\nMake sure to include your Jira space link in the Javascript code"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {
"Webhook streamlit": [
{
"json": {
"body": {
"action": "ping"
},
"query": {},
"params": {},
"headers": {
"host": "localhost:5678",
"accept": "*/*",
"connection": "keep-alive",
"user-agent": "python-requests/2.32.5",
"content-type": "application/json",
"content-length": "18",
"accept-encoding": "gzip, deflate"
},
"webhookUrl": "http://localhost:5678/webhook/19e7046a-ed85-4f0f-a54f-bc190e889257",
"executionMode": "production"
}
}
]
},
"settings": {
"executionOrder": "v1"
},
"versionId": "10641e4a-3357-4aeb-939e-c22ae7a73db2",
"connections": {
"04a874db-e6f5-45b0-9c24-c1353fbe82c4": {
"main": [
[]
]
},
"f73b22ee-e14e-4d0c-a638-86c79c79b540": {
"main": [
[
{
"node": "ed82c023-8052-4431-81cc-1474cdd41d7d",
"type": "main",
"index": 0
}
]
]
},
"e3d42140-21c8-414e-92bb-9b370db03cd8": {
"main": [
[
{
"node": "04a874db-e6f5-45b0-9c24-c1353fbe82c4",
"type": "main",
"index": 0
}
]
]
},
"08a64f5f-a115-492b-a74d-729a26e91964": {
"main": [
[
{
"node": "99fc511f-63a6-4bae-bd76-c71e7d8cd41f",
"type": "main",
"index": 0
}
]
]
},
"1d1a8b2d-6cbf-4844-9ca3-112d1786ae53": {
"main": [
[
{
"node": "08a64f5f-a115-492b-a74d-729a26e91964",
"type": "main",
"index": 0
}
],
[
{
"node": "eb9ab21e-f39e-4f79-b7e1-7ea092e2d8e7",
"type": "main",
"index": 0
}
]
]
},
"eb9ab21e-f39e-4f79-b7e1-7ea092e2d8e7": {
"main": [
[]
]
},
"99fc511f-63a6-4bae-bd76-c71e7d8cd41f": {
"main": [
[
{
"node": "e3d42140-21c8-414e-92bb-9b370db03cd8",
"type": "main",
"index": 0
}
]
]
},
"26e7793d-e192-4075-8873-4c97fa2fe616": {
"main": [
[
{
"node": "f73b22ee-e14e-4d0c-a638-86c79c79b540",
"type": "main",
"index": 0
}
]
]
},
"561cad41-bb3f-4c79-8040-dd8eea591ecf": {
"main": [
[
{
"node": "1d1a8b2d-6cbf-4844-9ca3-112d1786ae53",
"type": "main",
"index": 0
}
]
]
},
"ed82c023-8052-4431-81cc-1474cdd41d7d": {
"main": [
[
{
"node": "561cad41-bb3f-4c79-8040-dd8eea591ecf",
"type": "main",
"index": 0
}
]
]
}
}
}Wie verwende ich diesen Workflow?
Kopieren Sie den obigen JSON-Code, erstellen Sie einen neuen Workflow in Ihrer n8n-Instanz und wählen Sie "Aus JSON importieren". Fügen Sie die Konfiguration ein und passen Sie die Anmeldedaten nach Bedarf an.
Für welche Szenarien ist dieser Workflow geeignet?
Experte - Projektmanagement
Ist es kostenpflichtig?
Dieser Workflow ist völlig kostenlos. Beachten Sie jedoch, dass Drittanbieterdienste (wie OpenAI API), die im Workflow verwendet werden, möglicherweise kostenpflichtig sind.
Verwandte Workflows
Yassin Zehar
@yassinzeharDigital & IT Project Manager | Data-oriented | Agile certified (PSM I, PSPO I) | Paris
Diesen Workflow teilen