任务超时洞察

高级

这是一个Project Management、AI Summarization领域的自动化工作流,包含 40 个节点。主要使用 If、Code、Merge、ClickUp、HttpRequest 等节点。 任务时间分析:ClickUp等平台的自动超时支出洞察

前置要求
  • 可能需要目标 API 的认证凭证
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "h7ZyTA0VjSeZqsAI",
  "meta": {
    "instanceId": "00f37b5bf628874c654944094c8a454d5ff5e2df143f17dd729e285ac1e58176",
    "templateCredsSetupCompleted": true
  },
  "name": "任务超时洞察",
  "tags": [
    {
      "id": "1SefZAAaE6fahCis",
      "name": "Extra Effort Requested",
      "createdAt": "2025-05-16T09:37:50.710Z",
      "updatedAt": "2025-05-16T09:37:50.710Z"
    },
    {
      "id": "Cgaq1wafpvP8Ts91",
      "name": "Delivery Logs",
      "createdAt": "2025-05-16T09:15:54.071Z",
      "updatedAt": "2025-05-16T09:15:54.071Z"
    },
    {
      "id": "MTdWgh7kWuzq1arv",
      "name": "Software Release Notes",
      "createdAt": "2025-05-16T09:15:54.122Z",
      "updatedAt": "2025-05-16T09:15:54.122Z"
    },
    {
      "id": "S8pHYN2eFwOS5Ay7",
      "name": "Over Estimation",
      "createdAt": "2025-05-16T09:37:50.665Z",
      "updatedAt": "2025-05-16T09:37:50.665Z"
    },
    {
      "id": "Y82lXPBFFAZ6SDav",
      "name": "Ship Logs",
      "createdAt": "2025-05-16T09:15:54.097Z",
      "updatedAt": "2025-05-16T09:15:54.097Z"
    },
    {
      "id": "Z2Gof9jSDo30wxTR",
      "name": "Release Notes",
      "createdAt": "2025-05-16T09:15:54.147Z",
      "updatedAt": "2025-05-16T09:15:54.147Z"
    }
  ],
  "nodes": [
    {
      "id": "749b4a34-abca-448e-8039-a57801c00689",
      "name": "当点击\"测试工作流\"时",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -3260,
        540
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "7f4fe2c5-3ef6-4887-805b-b2ec73817af2",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1940,
        200
      ],
      "parameters": {
        "content": "为什么申请额外时间?"
      },
      "typeVersion": 1
    },
    {
      "id": "2753b25c-1ded-4f33-91cc-d1b00f593388",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2240,
        200
      ],
      "parameters": {
        "content": "为什么超出预估。"
      },
      "typeVersion": 1
    },
    {
      "id": "714798ea-c08e-4b92-bc28-76e0cefa28ba",
      "name": "OpenAI 聊天模型",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        2280,
        800
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {
          "maxRetries": 1
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "VfDim1ybKintUUHl",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "20f433ee-3723-4e8e-997b-90c6870ef29c",
      "name": "简单记忆",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        2440,
        800
      ],
      "parameters": {
        "sessionKey": "={$json.id}",
        "sessionIdType": "customKey",
        "contextWindowLength": "=3"
      },
      "typeVersion": 1.3
    },
    {
      "id": "b5d541fb-ff1c-4387-adc7-4fd7d3ec0197",
      "name": "转换为文件",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        3540,
        520
      ],
      "parameters": {
        "options": {
          "fileName": "=Task-{{ $json.taskId }}"
        },
        "operation": "toJson"
      },
      "typeVersion": 1.1
    },
    {
      "id": "3d05bf5b-7e97-473c-bef0-a45d8055577f",
      "name": "获取 Clickup 任务",
      "type": "n8n-nodes-base.clickUp",
      "position": [
        -3020,
        540
      ],
      "parameters": {
        "list": "901403418531",
        "team": "9014350065",
        "space": "90141295066",
        "filters": {
          "statuses": [
            "internal review",
            "in progress"
          ],
          "subtasks": "={{ true }}",
          "assignees": [
            82359490
          ]
        },
        "operation": "getAll",
        "folderless": true
      },
      "credentials": {
        "clickUpApi": {
          "id": "HgWmHJHMRSyp06YE",
          "name": "ClickUp account"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "730d01c6-57f8-48ec-9fbe-9fc8d149408c",
      "name": "通过任务ID获取时间记录",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -560,
        1280
      ],
      "parameters": {
        "url": "=https://api.clickup.com/api/v2/task/{{ $json.id }}/time ",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "clickUpApi"
      },
      "credentials": {
        "clickUpApi": {
          "id": "HgWmHJHMRSyp06YE",
          "name": "ClickUp account"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "331d1286-f720-45f6-8e77-d6e822a517b6",
      "name": "从任务中过滤掉不必要的数据",
      "type": "n8n-nodes-base.code",
      "position": [
        -2800,
        540
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Destructure the fields you want from the current comment\nconst { id, name, text_content, status,date_created,date_closed,date_done,date_updated,creator,assignees,group_assignees,checklists,tags, time_estimate,time_estimates_by_user,time_spent,team_id,\n      } = $json;\n\n// Return a new JSON object with the outer fields\nreturn {\n  json: {\n    id,name, text_content, status,date_created,date_closed,date_done,date_updated,creator,assignees,group_assignees,checklists,tags, time_estimate,time_estimates_by_user,time_spent,team_id,\n  }\n};\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2be708ea-adf5-4f41-ae87-ad87539d5704",
      "name": "如果任务已超过预估",
      "type": "n8n-nodes-base.if",
      "position": [
        -2580,
        540
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "fc079044-556f-4a4f-99a9-4928f91c3b5e",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.time_spent }}",
              "rightValue": "={{ $json.time_estimate }}"
            },
            {
              "id": "c1101bc1-3fad-44d5-8b67-eb89faf90ffa",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "86b4frgaw",
              "rightValue": "={{ $json.id }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "040f3bcf-edef-42c6-aa4f-fd54c2cb1287",
      "name": "获取主评论",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1620,
        160
      ],
      "parameters": {
        "url": "=https://api.clickup.com/api/v2/task/{{ $json.id }}/comment ",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "clickUpApi"
      },
      "credentials": {
        "clickUpApi": {
          "id": "HgWmHJHMRSyp06YE",
          "name": "ClickUp account"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "9ff0e6a3-9701-4686-908d-dce489103dc2",
      "name": "循环遍历主评论",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1220,
        160
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "8749943a-1834-4adc-90cc-4c0ff3094bc5",
      "name": "如果评论有线程评论",
      "type": "n8n-nodes-base.if",
      "position": [
        -280,
        180
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "91e31fc5-8ac2-484b-827d-0fe66965595d",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.reply_count }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "98f6f0b5-70d1-4341-b4cf-e6b313cc806a",
      "name": "获取评论线程",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -40,
        -80
      ],
      "parameters": {
        "url": "=https://api.clickup.com/api/v2/comment/{{ $json.id }}/reply",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "clickUpApi"
      },
      "credentials": {
        "clickUpApi": {
          "id": "HgWmHJHMRSyp06YE",
          "name": "ClickUp account"
        }
      },
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "e4af0433-6cb0-4f80-a284-1520b1329971",
      "name": "将线程评论与主评论合并",
      "type": "n8n-nodes-base.merge",
      "position": [
        440,
        -220
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.1
    },
    {
      "id": "236d2c78-0af5-4f78-809d-a740f68beae0",
      "name": "重新合并所有主评论",
      "type": "n8n-nodes-base.merge",
      "position": [
        860,
        180
      ],
      "parameters": {},
      "typeVersion": 3.1,
      "alwaysOutputData": true
    },
    {
      "id": "91ddd52e-7c72-466d-b054-4eb9bf4a08b2",
      "name": "重新构建评论以便在循环节点中处理",
      "type": "n8n-nodes-base.code",
      "position": [
        1080,
        180
      ],
      "parameters": {
        "jsCode": "\n\n// 1. Extract all incoming comment objects into a simple array\nconst commentsArray = items.map(item => item.json);\n\n// 2. Wrap that array under the 'comment' key in one object\nreturn [\n  {\n    json: {\n      comments: commentsArray\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "aafe2a4c-3b8b-4eee-bde6-b8cf19ca8c10",
      "name": "合并任务数据、评论和时间记录",
      "type": "n8n-nodes-base.merge",
      "position": [
        1940,
        520
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition",
        "numberInputs": 3
      },
      "typeVersion": 3.1
    },
    {
      "id": "b531daf1-ceeb-4739-bd3c-1cd0f9ca4d29",
      "name": "修改任务数据",
      "type": "n8n-nodes-base.code",
      "position": [
        -300,
        540
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const { id, name, text_content, status, date_created, date_updated, tags, time_estimate, time_estimates_by_user, time_spent } = $json;\n\n\nreturn {\nid, name, text_content, status, date_created, date_updated, tags, time_estimate, time_estimates_by_user, time_spent\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "8838e6b2-2e60-42a0-864b-e7b2cc640504",
      "name": "移动到下一个主评论",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1300,
        180
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "dfa59ea4-1ed2-49fa-8490-41711685b032",
      "name": "修改主评论数据",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        -220
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Destructure comments out of the payload, collecting the rest in `rest`\nconst { comments,assignee, group_assignee, reactions, ...rest } = $json;\n\n// Return a new JSON object with the outer fields + renamed thread\nreturn {\n  json: {\n    ...rest,\n    comment_thread: comments,\n  },\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b9d06844-c5bb-4897-85cb-4bf992535b84",
      "name": "返回评论数据",
      "type": "n8n-nodes-base.code",
      "position": [
        560,
        -360
      ],
      "parameters": {
        "jsCode": "\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "61f7e795-d952-4161-a27e-338c3d7744a9",
      "name": "修改线程评论数据",
      "type": "n8n-nodes-base.code",
      "position": [
        200,
        -80
      ],
      "parameters": {
        "jsCode": "const result = [];\n\nfor (const item of items) {\n  const comments = item.json.comments || [];\n\n  // Sort comments by date in descending order\n  const sortedComments = comments.sort((a, b) => {\n    const dateA = parseInt(a.date || '0', 10);\n    const dateB = parseInt(b.date || '0', 10);\n    return dateA - dateB;\n  });\n\n  // Map the sorted comments into desired format\n  const filteredComments = sortedComments.map(comment => {\n    return {\n      id: comment.id,\n      comment_text: comment.comment_text,\n      date: comment.date,\n      user: comment.user,\n      reactions: comment.reactions\n    };\n  });\n\n  result.push({\n    json: {\n      comments: filteredComments\n    }\n  });\n}\n\nreturn result;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3435d757-2812-46be-a0a5-546fb3cc7f82",
      "name": "按从旧到新排序主评论",
      "type": "n8n-nodes-base.code",
      "position": [
        -600,
        180
      ],
      "parameters": {
        "jsCode": "// Assuming input data is in the format you provided\nconst sortedItems = items.sort((a, b) => {\n  console.log('check data',a,b)\n  const dateA = parseInt(a.json.date, 10);\n  const dateB = parseInt(b.json.date, 10);\n  return dateA - dateB;\n});\n\n// Return each sorted item individually for downstream use\nreturn sortedItems;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e657b291-a333-449b-82e7-e3e27c4c0c70",
      "name": "修改时间记录数据",
      "type": "n8n-nodes-base.code",
      "position": [
        -340,
        1280
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const { data, ...rest } = $json;\n\nconst getTimeInMinutes = (data) => Math.round(parseInt(data || '0', 10) / 60000) \n\nconst sortedData = data.map(item => {\n  const processedIntervals = [...(item.intervals || [])]\n    .sort((a, b) => parseInt(a.date_added || '0', 10) - parseInt(b.date_added || '0', 10))\n    .map(interval => ({\n      id: interval.id,\n      time_in_minutes: getTimeInMinutes(interval.time),\n  description: interval.description,\n  tags: (interval.tags || []).map(tag => tag.name),\n  category: \"\"\n    }));\n\n  const {time, ...restItemdata} = item;\n\n  return {\n    ...restItemdata,\n \"total_time_in_minutes\" : getTimeInMinutes(time),\n    intervals: processedIntervals,\n  };\n});\n\nreturn {\n  json: {\n    ...rest,\n    time_entries: sortedData,\n  },\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "51e5cf99-7128-4008-b9d5-19f9818f29de",
      "name": "代码",
      "type": "n8n-nodes-base.code",
      "position": [
        2820,
        520
      ],
      "parameters": {
        "jsCode": "const rawOutput = $json[\"output\"];\nconst parsed = JSON.parse(rawOutput);\nreturn parsed;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8ffc6a60-44c0-4a9e-8a3e-517d598b6845",
      "name": "解构和过滤评论数组以循环处理",
      "type": "n8n-nodes-base.code",
      "position": [
        -900,
        180
      ],
      "parameters": {
        "jsCode": "const comments = $input.first().json.comments;\n\n// Filter out any comment where user.username is 'Clickbot'\nconst filteredComments = comments.filter(comment => comment.user?.username !== 'ClickBot');\n\nreturn filteredComments;\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "ffa220b7-5097-4068-ad06-72ccaabcfd0d",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3320,
        420
      ],
      "parameters": {
        "width": 940,
        "height": 320,
        "content": "## 获取超时任务"
      },
      "typeVersion": 1
    },
    {
      "id": "90acbb60-344b-4858-95e0-414af00ad3ec",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -700,
        1200
      ],
      "parameters": {
        "color": 3,
        "width": 540,
        "height": 260,
        "content": "## 获取时间记录"
      },
      "typeVersion": 1
    },
    {
      "id": "8ee5b71b-b9c6-4630-878c-fbbfc8574d57",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1700,
        -380
      ],
      "parameters": {
        "color": 4,
        "width": 3240,
        "height": 800,
        "content": "## 获取评论及其线程"
      },
      "typeVersion": 1
    },
    {
      "id": "3377bb1b-94b8-48e7-b8d0-bcd80ba0b41d",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1840,
        420
      ],
      "parameters": {
        "color": 6,
        "width": 860,
        "height": 520,
        "content": "## AI生成的清单"
      },
      "typeVersion": 1
    },
    {
      "id": "99e3f2b0-8c6a-4780-85c6-f01bed99c0f9",
      "name": "### 需要帮助?",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1020,
        1040
      ],
      "parameters": {
        "color": 2,
        "width": 1200,
        "height": 720,
        "content": "## 时间花费在"
      },
      "typeVersion": 1
    },
    {
      "id": "fd221230-7152-4187-83df-bbd490192ffa",
      "name": "OpenAI 聊天模型1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1380,
        1560
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {
          "maxRetries": 1
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "VfDim1ybKintUUHl",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "bb32a71f-a59a-4a1d-a48f-4bbd87d69c87",
      "name": "简单记忆1",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        1540,
        1560
      ],
      "parameters": {
        "sessionKey": "={$json.id}",
        "sessionIdType": "customKey",
        "contextWindowLength": "=3"
      },
      "typeVersion": 1.3
    },
    {
      "id": "e3ea3616-d303-4691-bbd9-a0a619cf7511",
      "name": "生成时间洞察",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1500,
        1280
      ],
      "parameters": {
        "text": "={{$json.prompt}}\n",
        "options": {
          "maxIterations": 1
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.9
    },
    {
      "id": "07941593-50ca-4c08-b20e-b127fc5b8c85",
      "name": "生成原因清单",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        520
      ],
      "parameters": {
        "text": "=TASK INFO\n---------\ntaskId: {{$json.id}}\ntaskName: {{$json.name}}\ntextContent: {{$json.text_content}}\nstatus: {{$json.status.status}} (type: {{$json.status.type}}, color: {{$json.status.color}})\ntags: {{ JSON.stringify($json.tags) }}\n\ntimeEstimate: {{$json.time_estimate}}\ntimeEstimatesByUser: {{ JSON.stringify($json.time_estimates_by_user) }}\ntimeSpent: {{$json.time_spent}}\n\nCOMMENTS\n--------\ncomments: {{ JSON.stringify($json.comments) }}\n\nEach comment object includes:\n- id\n- comment_text\n- date\n- user (id, username)\n- reply_count\n- comment_thread (array of replies)\n\n→ You must analyze the full thread of each comment (main + replies) to extract **literal**, factual insights only.\n\nTIME ENTRIES\n------------\ntimeEntries: {{ JSON.stringify($json.time_entries) }}\n\nEach time entry includes:\n- user (id, username)\n- time\n- intervals (each with start, end, time, description, tags)\n\nYOUR TASK\n---------\nYou must determine **if and why** more time was needed or estimates were exceeded, **based only on literal phrases present in the data**. Do **not infer** or invent. Follow these strict rules:\n\n1. **Analyze COMMENTS**\n   - Extract a reason only if you find an **exact statement** such as:\n     - Request for more time\n     - Mention of blockers\n     - Delays due to clarifications or product change\n     - Rework initiated from comments\n   - Ignore comments like: “PR created”, “Done”, “Tested”, “Reviewed”, or “Added field” unless they **explicitly mention a delay or blocker**.\n\n2. **Analyze TIME ENTRIES**\n   - Extract reasons only if **descriptions or tags** mention:\n     - Debugging, research, clarification, or rework\n     - A specific delay reason like “API didn’t respond” or “Issue with X logic”\n   - If interval notes are vague or missing, ignore them.\n\n3. **Build Checklist Output**\n   - If no factual, literal reason is found from either comments or time entries, return the following fallback result:\n\n```json\n[\n  {\n    \"taskId\": \"{{$json.id}}\",\n    \"check_list\": [\n      {\n        \"check_list_name\": \"why_needed_extra_time\",\n        \"check_list_items\": [\n          \"Not enough data available to determine why extra time was needed.\"\n        ]\n      },\n      {\n        \"check_list_name\": \"why_gone_over_estimate\",\n        \"check_list_items\": [\n          \"Not enough data available to determine why the estimate was exceeded.\"\n        ]\n      }\n    ]\n  }\n]\n\nDO NOT skip this fallback. It is mandatory if data lacks solid insights.\n\nCHECKLIST WRITING RULES\n------------------------\nThe checklist must contain **short, meaningful bullet points**, not copied sentences or chat-style remarks.\n\nExamples:\n❌ \"I need 10hr extra on top of spent time to...\"\n✅ \"Requested 10 additional hours due to technical differences in extension vs DA frontend.\"\n\n❌ \"During the demo we noticed a bug...\"\n✅ \"Bug identified during demo required a workaround due to Chrome extension restrictions.\"\n\n✔ Each checklist item must be:\n- Condensed to a few words or a sentence\n- Free of filler phrases like \"I noticed\", \"we saw\", \"I need\", etc.\n- Framed as standalone explanations — useful even when seen alone\n- Group similar reasons (e.g., debugging multiple issues = one bullet on extended debugging)\n- Start with verbs when possible: “Requested”, “Debugged”, “Reworked”, “Integrated”, “Adjusted”, etc.\n\nDO NOT:\n- Copy-paste comment text\n- Repeat multiple versions of the same cause\n- Include vague, conversational, or non-actionable text\n\nStrict Rules:\n-------------\n1. ❌ **Do not infer**, assume, or fabricate any reasons.\n2. ✅ Extract a reason only if it is **clearly stated** in:\n   - a comment or its replies (e.g., \"need more time\", \"blocker found\", \"needs rework\")\n   - a time entry description (e.g., \"debugging API issue\", \"researching logic\")\n3. ⛔️ Ignore vague or default phrases like \"work done\", \"tested\", \"code pushed\", etc.\n4. ✅ If no valid reasons are found in a section, insert this **mandatory fallback reason**:\n\n\"Not enough data available to determine why extra time was needed.\"\nor\n\n\"Not enough data available to determine why the estimate was exceeded.\"\nYou MUST include these fallback messages if no real reasons are found. Do NOT return an empty checklist.\n\nOUTPUT FORMAT\nReturn only the raw JSON object in this format (no quotes, markdown, or stringification):\n\n[\n{\n\"taskId\": \"{{$json.id}}\",\n\"check_list\": [\n{\n\"check_list_name\": \"why_needed_extra_time\",\n\"check_list_items\": [\n/* factual reasons based on real comment or entry content — or fallback if none /\n]\n},\n{\n\"check_list_name\": \"why_gone_over_estimate\",\n\"check_list_items\": [\n/ factual reasons based on real comment or entry content — or fallback if none */\n]\n}\n]\n}\n]\nDO NOT wrap the result in quotes or return it as a string. Just return pure raw JSON array as output.\n",
        "options": {
          "maxIterations": 1
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.9
    },
    {
      "id": "bb11bcba-3420-454f-ac54-4d02af4a06c4",
      "name": "代码2",
      "type": "n8n-nodes-base.code",
      "position": [
        2040,
        1280
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const rawOutput = $json[\"output\"];\nconst parsed = JSON.parse(rawOutput);\nreturn {time_entries_by_category:parsed};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f4778407-9c8c-471d-ac18-b4c6c2c8ddd7",
      "name": "合并",
      "type": "n8n-nodes-base.merge",
      "position": [
        3140,
        520
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.1
    },
    {
      "id": "8237e6ce-da95-4106-bd19-a09e8bfd92f8",
      "name": "代码3",
      "type": "n8n-nodes-base.code",
      "position": [
        540,
        1700
      ],
      "parameters": {
        "jsCode": "// n8n Code node: Categorize & Sum Time Entries\n\n// 1) Grab the array of users\nconst users = $input.first().json.time_entries || [];\n\n// 2) Define keyword lists & precedence\nconst categories = [\n  { name: 'Development', keywords: ['development','dev','feature','implement','coding','build'] },\n  { name: 'Commenting+Call+Documentation', \n    keywords: ['comment','comments','call','calls','documentation','docs','doc'] },\n  { name: 'Miscellaneous', keywords: [] },  // fallback\n  { name: 'PR Review',   keywords: ['review','pr','merge','approve'] },\n  { name: 'QA',          keywords: ['qa','test','bug','verify','validate'] },\n  { name: 'Scoping',     keywords: ['scope','estimation','plan','analy'] },\n];\n\n// Helper to find category\nfunction categorizeInterval(interval) {\n  const text = [\n    interval.description || '',\n    ...(interval.tags || [])\n  ].join(' | ').toLowerCase();\n\n  for (let cat of categories.slice(0, -1)) {\n    if (cat.keywords.some(k => text.includes(k))) {\n      return cat.name;\n    }\n  }\n  // No match → fallback\n  return 'Miscellaneous';\n}\n\n// Helper to format minutes as Xh Ym\nfunction formatMinutes(mins) {\n  const h = Math.floor(mins / 60);\n  const m = mins % 60;\n  return `${h}h ${m}m`;\n}\n\n// 3) Process each user\nconst result = users.map(userObj => {\n  const sums = {};\n  // Initialize sums\n  for (let cat of categories) sums[cat.name] = 0;\n\n  // Categorize & accumulate\n  for (let interval of userObj.intervals || []) {\n    const cat = categorizeInterval(interval);\n    sums[cat] += interval.time_in_minutes || 0;\n  }\n\n  // Build output array, omitting zeros\n  const time_spent_by_category = Object.entries(sums)\n    .filter(([, total]) => total > 0)\n    .map(([category, total]) => ({\n      category,\n      time: formatMinutes(total)\n    }));\n\n  return {\n    user: userObj.user.username,\n    time_spent_by_category\n  };\n});\n\n// 4) Return as JSON\nreturn {\n   result\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c2c2dd87-4a59-4f28-9a2d-4ac82b3baa01",
      "name": "生成带时间记录的提示",
      "type": "n8n-nodes-base.code",
      "position": [
        1100,
        1280
      ],
      "parameters": {
        "jsCode": "// Function node: “Build Prompt With time_entries + Keyword‑Count + Resources”\nconst timeEntries = $input.first().json.time_entries || [];\n\nlet msg = { prompt: '' };\nconst rawJson = JSON.stringify(timeEntries, null, 2);\nconst taskInfo = `**TASK INFO**  \n${rawJson}`;\n\nconst instruction = `You are a Time‑Tracking Categorization Agent.  \nYou must process an array of users’ intervals and assign each interval to exactly one category using **keyword match counting**.\n\n---\n\n### CATEGORIES & KEYWORDS  \n(Case-insensitive, substring match on description or tags.)\n\n- Development:      development, dev, feature, implement, coding, build  \n- PR Review:        review, pr, merge, approve  \n- QA:               qa, test, bug, verify, validate  \n- Scoping:          scope, estimation, plan, analy  \n- Commenting+Call+Documentation: comment, comments, call, calls, documentation, docs, doc  \n- Miscellaneous:    (fallback — when no keywords match)\n\n---\n\n### PROCESS PER INTERVAL:\n1. Combine description and tags into a single string.  \n2. Count keyword matches for each category.\n3. Assign the interval to the category with the **most matches**.  \n4. Break ties by choosing the category with the **alphabetically first name**.  \n5. If no keywords match, assign to **Miscellaneous**.  \n6. **Each interval must appear in only one category.**\n\n---\n\n### OUTPUT RULES:\n\n- For each user:\n   - Group intervals by category.\n   - Include exact intervals as an array under \\`resources\\`.\n   - Set the \"time\" field strictly by summing time_in_minutes from each item in resources.\n   - Format it as Xh Ym (e.g., 1h 45m).\n   - You must not guess, round, or calculate it separately.   \n  - Format the total as “Xh Ym”.\n   - Omit any category with 0 total time.\n\n---\n\n### OUTPUT FORMAT:\n\nRespond with only the final JSON array (no markdown or code fences):\n\n[\n  {\n    \"user\": \"<username>\",\n    \"time_spent_by_category\": [\n      {\n        \"category\": \"Development\",\n        \"time\": \"Xh Ym\",\n        \"resources\": [<interval objects>]\n      },\n      {\n        \"category\": \"PR Review\",\n        \"time\": \"Xh Ym\",\n        \"resources\": [...]\n      },\n      ...\n    ]\n  },\n  … more users …\n]`;\n\n\nmsg.prompt = [instruction, taskInfo].join('\\n\\n');\nreturn msg;\n"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "executionOrder": "v1"
  },
  "versionId": "54ab225c-bc77-4a90-801c-8b2673494faa",
  "connections": {
    "Code": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code2": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Convert to File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "Generate Reason checklist",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory1": {
      "ai_memory": [
        [
          {
            "node": "Generate time insights",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Modify Task data": {
      "main": [
        [
          {
            "node": "Merge task data, comments and time entries",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get Clickup Tasks": {
      "main": [
        [
          {
            "node": "Filter out unnecessary data from Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Reason checklist",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Generate time insights",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Return Comments data": {
      "main": [
        [
          {
            "node": "Merge task data, comments and time entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Master comments": {
      "main": [
        [
          {
            "node": "Loop Over Master comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch comment threads": {
      "main": [
        [
          {
            "node": "Modify threads comment data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate time insights": {
      "main": [
        [
          {
            "node": "Code2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Modify Time entries data": {
      "main": [
        [
          {
            "node": "Merge task data, comments and time entries",
            "type": "main",
            "index": 2
          },
          {
            "node": "Generate Prompt with Time entires",
            "type": "main",
            "index": 0
          },
          {
            "node": "Code3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Reason checklist": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Master comments": {
      "main": [
        [
          {
            "node": "Return Comments data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Destructure & Filter comments array to loop them",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Modify Master comment data": {
      "main": [
        [
          {
            "node": "Re-merge all master comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Modify threads comment data": {
      "main": [
        [
          {
            "node": "Merge thread comments with master comments",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Move to next master comment": {
      "main": [
        [
          {
            "node": "Loop Over Master comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Re-merge all master comments": {
      "main": [
        [
          {
            "node": "Re-structure comments to process them in loop node",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If task has crossed estimation": {
      "main": [
        [
          {
            "node": "Fetch Time entries via task IDs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Modify Task data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Master comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Time entries via task IDs": {
      "main": [
        [
          {
            "node": "Modify Time entries data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If comments got thread comments": {
      "main": [
        [
          {
            "node": "Fetch comment threads",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge thread comments with master comments",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Re-merge all master comments",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Sort Master comments old to new": {
      "main": [
        [
          {
            "node": "If comments got thread comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Prompt with Time entires": {
      "main": [
        [
          {
            "node": "Generate time insights",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking ‘Test workflow’": {
      "main": [
        [
          {
            "node": "Get Clickup Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter out unnecessary data from Tasks": {
      "main": [
        [
          {
            "node": "If task has crossed estimation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge task data, comments and time entries": {
      "main": [
        [
          {
            "node": "Generate Reason checklist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge thread comments with master comments": {
      "main": [
        [
          {
            "node": "Modify Master comment data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Destructure & Filter comments array to loop them": {
      "main": [
        [
          {
            "node": "Sort Master comments old to new",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Re-structure comments to process them in loop node": {
      "main": [
        [
          {
            "node": "Move to next master comment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

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

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

这是一个高级难度的工作流,适用于Project Management、AI Summarization等场景。适合高级用户,包含 16+ 个节点的复杂工作流

需要付费吗?

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

工作流信息
难度等级
高级
节点数量40
分类2
节点类型13
难度说明

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

作者
Krupal Patel

Krupal Patel

@krupalpatel

With having Lifelong Passion for Software Development. With over 15 years of experience, I've had the privilege of working with Fortune 500 companies, startups, and enterprise giants like Stryker Corporation, Ulta Beauty, Pepsico, Infosys, Fossil, and Bosch. My journey has been about taking complex projects from concept to market delivery, contributing to the success stories of these organizations.

外部链接
在 n8n.io 上查看 →

分享此工作流