8
n8n 中文网amn8n.com

从 Streamlit 应用自动创建 Jira 工单

高级

这是一个Project Management领域的自动化工作流,包含 16 个节点。主要使用 If, Set, Code, Webhook, HttpRequest 等节点。 通过 Webhook 和 REST API 从 Streamlit 表单创建 Jira 工单

前置要求
  • HTTP Webhook 端点(n8n 会自动生成)
  • 可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "2DxalJmssGiuQC9h",
  "meta": {
    "instanceId": "0430772da25f7bca29bf5ef2b251086a85fb4096503a6f781526d32befd038d6",
    "templateCredsSetupCompleted": true
  },
  "name": "从 Streamlit 应用自动创建 Jira 工单",
  "tags": [],
  "nodes": [
    {
      "id": "26e7793d-e192-4075-8873-4c97fa2fe616",
      "name": "Streamlit Webhook",
      "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 响应",
      "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 请求",
      "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": "来自 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 数据",
      "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": "已处理数据",
      "type": "n8n-nodes-base.set",
      "position": [
        928,
        0
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={{ $json }}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "04a874db-e6f5-45b0-9c24-c1353fbe82c4",
      "name": "结果",
      "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": "防重复",
      "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": "被阻止的请求",
      "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": "重复判断条件",
      "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": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -976,
        -704
      ],
      "parameters": {
        "color": 3,
        "width": 304,
        "height": 176,
        "content": "## 必需条件\\n\\n- Streamlit 创建工单应用\\n- Jira 账户"
      },
      "typeVersion": 1
    },
    {
      "id": "f81324d8-fd8e-485a-8398-72768c64ff09",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -624,
        -704
      ],
      "parameters": {
        "color": 2,
        "width": 416,
        "height": 1264,
        "content": "## 1) 触发器与数据接收 (Streamlit → n8n)\\n\\n目的:从应用接收工单并传递给工作流。\\n\\n节点\\n\\nStreamlit Webhook – 接收 action=create_ticket 和工单对象(id、projectKey、type、summary、description、priority、story_points、due_date、labels…)。\\n\\n来自 Streamlit 的原始数据(设置节点)– 可选透传,用于在执行中可视化原始负载。\\n\\n设置\\n\\nWebhook → 响应模式:lastNode(这样最终节点的响应会返回给 Streamlit)。\\n\\n在应用中使用生产 URL(避免在生产环境中使用测试 URL)。\\n\\n不要手动运行 Jira HTTP 节点;始终通过 Webhook 触发。"
      },
      "typeVersion": 1
    },
    {
      "id": "5e0bc57b-223c-46fe-bd90-5682ef4c5e67",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        -704
      ],
      "parameters": {
        "color": 4,
        "width": 464,
        "height": 1264,
        "content": "## 4) 在 Jira 中创建问题\\n\\n目的:调用 Jira REST API 并创建实际工单。\\n\\n节点\\n\\n已处理数据(设置节点)– 透传字段对象以便检查。\\n\\nJira HTTP 请求(HTTP 请求节点)\\n\\n方法:POST\\n\\nURL:https://<你的域名>.atlassian.net/rest/api/3/issue\\n\\n认证:Jira Software Cloud API(邮箱 + API 令牌)\\n\\n请求头:Content-Type: application/json(Accept 可选)\\n\\n请求体:原始 JSON = { \"fields\": … } 来自前一个节点(不是字符串化的 \"[object Object]\")。\\n\\n预期输出\\n\\nJira 返回包含新问题键的 JSON(例如 TES-123)。"
      },
      "typeVersion": 1
    },
    {
      "id": "5175079e-7ddc-46f1-bf93-532421c2e16a",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        -704
      ],
      "parameters": {
        "color": 5,
        "width": 304,
        "height": 1264,
        "content": "## 3) 规范化并构建 Jira 负载\\n\\n目的:将应用字段转换为有效的 Jira JSON。\\n\\n节点\\n\\n处理 Streamlit 数据(代码节点)– 输出干净的字段对象:\\n\\nproject.key(来自 projectKey 的大写)\\n\\nissuetype.name(Task/Story/Bug/Epic)\\n\\nsummary\\n\\nAtlassian 文档格式的 description(doc → paragraph → text)\\n\\n可选:priority.name、duedate(YYYY-MM-DD)、labels(数组)\\n\\n可选:Story Points 的 customfield_10016(或您实例的 ID)\\n\\n注意事项\\n\\n不要在 Jira Cloud 中设置 assignee.name;仅当您拥有 assignee.accountId 时才使用它。"
      },
      "typeVersion": 1
    },
    {
      "id": "953b841a-8814-4047-bce8-a8644d266005",
      "name": "便签说明4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -176,
        -704
      ],
      "parameters": {
        "color": 6,
        "width": 944,
        "height": 1264,
        "content": "## 2) 去重防护与分支(计数/条件判断)\\n\\n目的:防止空/无效调用和重复创建问题。\\n\\n节点\\n\\n防重复(代码节点)– 在工作流静态数据中构建短期内存。\\n\\n如果 ticket.id 已存在(或 projectKey+type+summary+description 的指纹),标记为重复。\\n\\n如果 action 不是 create_ticket 或必填字段缺失,标记为无效。\\n\\n重复判断条件\\n\\n真分支(重复/无效)→ 被阻止的请求(代码节点)返回 { ok: true, duplicate: true | ignored: true } 给 Webhook 并停止。\\n\\n假分支(干净请求)→ 继续。\\n\\n验证\\n\\n快速提交相同表单两次 → 真路径应触发一次(不会创建第二个 Jira 问题)。\\n\\n来自应用的“测试连接”应被忽略且不创建任何内容。"
      },
      "typeVersion": 1
    },
    {
      "id": "d933ae95-f6f6-4044-85de-f9a5fcca67e3",
      "name": "便签说明5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        -704
      ],
      "parameters": {
        "height": 1264,
        "content": "## 5) 将结果返回给应用\\n\\n目的:向 Streamlit 返回友好的响应。\\n\\n节点\\n\\nJira 响应(设置节点)– 可选透传 Jira 响应以便查看。\\n\\n结果(代码节点)– 返回给 Webhook:\\n{ ok: true, jiraKey: <KEY>, url: https://<域名>.atlassian.net/browse/<KEY> }\\n\\n用户体验\\n\\nStreamlit 显示创建的键和打开工单的链接。\\n\\n确保在 Javascript 代码中包含您的 Jira 空间链接"
      },
      "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": {
    "Result": {
      "main": [
        []
      ]
    },
    "anti double": {
      "main": [
        [
          {
            "node": "raw data from streamlit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "jira response": {
      "main": [
        [
          {
            "node": "Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Processed data": {
      "main": [
        [
          {
            "node": "Jira HTTP request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "if for doubles": {
      "main": [
        [
          {
            "node": "Processed data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "blocked request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "blocked request": {
      "main": [
        []
      ]
    },
    "Jira HTTP request": {
      "main": [
        [
          {
            "node": "jira response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook streamlit": {
      "main": [
        [
          {
            "node": "anti double",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process streamlit data": {
      "main": [
        [
          {
            "node": "if for doubles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "raw data from streamlit": {
      "main": [
        [
          {
            "node": "Process streamlit data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 项目管理

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量16
分类1
节点类型6
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Yassin Zehar

Yassin Zehar

@yassinzehar

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

外部链接
在 n8n.io 查看

分享此工作流