Créer automatiquement des tickets Jira à partir d'applications Streamlit

Avancé

Ceci est unProject Managementworkflow d'automatisation du domainecontenant 16 nœuds.Utilise principalement des nœuds comme If, Set, Code, Webhook, HttpRequest. Créer des tickets Jira à partir de formulaires Streamlit via Webhook et REST API

Prérequis
  • Point de terminaison HTTP Webhook (généré automatiquement par n8n)
  • Peut nécessiter les informations d'identification d'authentification de l'API cible
Aperçu du workflow
Visualisation des connexions entre les nœuds, avec support du zoom et du déplacement
Exporter le workflow
Copiez la configuration JSON suivante dans n8n pour importer et utiliser ce workflow
{
  "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": "Réponse jira",
      "type": "n8n-nodes-base.set",
      "position": [
        1488,
        0
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={{$json}}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "99fc511f-63a6-4bae-bd76-c71e7d8cd41f",
      "name": "Requête Jira HTTP",
      "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": "Données brutes de 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": "Traiter les données Streamlit",
      "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": "Données traitées",
      "type": "n8n-nodes-base.set",
      "position": [
        928,
        0
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={{ $json }}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "04a874db-e6f5-45b0-9c24-c1353fbe82c4",
      "name": "Résultat",
      "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-doublon",
      "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": "Requête bloquée",
      "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": "Condition pour doublons",
      "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": "Note adhésive1",
      "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": "Note adhésive",
      "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": "Note adhésive3",
      "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": "Note adhésive2",
      "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": "Note adhésive4",
      "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": "Note adhésive5",
      "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
          }
        ]
      ]
    }
  }
}
Foire aux questions

Comment utiliser ce workflow ?

Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.

Dans quelles scénarios ce workflow est-il adapté ?

Avancé - Gestion de projet

Est-ce payant ?

Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.

Informations sur le workflow
Niveau de difficulté
Avancé
Nombre de nœuds16
Catégorie1
Types de nœuds6
Description de la difficulté

Adapté aux utilisateurs avancés, avec des workflows complexes contenant 16+ nœuds

Auteur
Yassin Zehar

Yassin Zehar

@yassinzehar

Digital & IT Project Manager | Data-oriented | Agile certified (PSM I, PSPO I) | Paris

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34