Crear automáticamente tickets de Jira desde una aplicación de Streamlit

Avanzado

Este es unProject Managementflujo de automatización del dominio deautomatización que contiene 16 nodos.Utiliza principalmente nodos como If, Set, Code, Webhook, HttpRequest. Crear tickets de Jira a partir de formularios de Streamlit mediante Webhook y REST API

Requisitos previos
  • Punto final de HTTP Webhook (n8n generará automáticamente)
  • Pueden requerirse credenciales de autenticación para la API de destino
Vista previa del flujo de trabajo
Visualización de las conexiones entre nodos, con soporte para zoom y panorámica
Exportar flujo de trabajo
Copie la siguiente configuración JSON en n8n para importar y usar este flujo de trabajo
{
  "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": "Disparador 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": "respuesta 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": "solicitud HTTP Jira",
      "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": "datos brutos 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": "Procesar datos de 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": "Datos procesados",
      "type": "n8n-nodes-base.set",
      "position": [
        928,
        0
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={{ $json }}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "04a874db-e6f5-45b0-9c24-c1353fbe82c4",
      "name": "Resultado",
      "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 duplicados",
      "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": "solicitud bloqueada",
      "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": "condicional para duplicados",
      "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": "Nota adhesiva1",
      "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": "Nota adhesiva",
      "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": "Nota adhesiva3",
      "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": "Nota adhesiva2",
      "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": "Nota adhesiva4",
      "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": "Nota adhesiva5",
      "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
          }
        ]
      ]
    },
    "Webhook streamlit": {
      "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
          }
        ]
      ]
    }
  }
}
Preguntas frecuentes

¿Cómo usar este flujo de trabajo?

Copie el código de configuración JSON de arriba, cree un nuevo flujo de trabajo en su instancia de n8n y seleccione "Importar desde JSON", pegue la configuración y luego modifique la configuración de credenciales según sea necesario.

¿En qué escenarios es adecuado este flujo de trabajo?

Avanzado - Gestión de proyectos

¿Es de pago?

Este flujo de trabajo es completamente gratuito, puede importarlo y usarlo directamente. Sin embargo, tenga en cuenta que los servicios de terceros utilizados en el flujo de trabajo (como la API de OpenAI) pueden requerir un pago por su cuenta.

Información del flujo de trabajo
Nivel de dificultad
Avanzado
Número de nodos16
Categoría1
Tipos de nodos6
Descripción de la dificultad

Adecuado para usuarios avanzados, flujos de trabajo complejos con 16+ nodos

Autor
Yassin Zehar

Yassin Zehar

@yassinzehar

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

Enlaces externos
Ver en n8n.io

Compartir este flujo de trabajo

Categorías

Categorías: 34