有組長問我,如果他們的團隊也想導入這個流程,要怎麼做?
今天就來分享 n8n 模組的匯出與匯入功能,並公開我製作的 workflow 模板,供大家參考使用。
如果你想將自己寫好的範本分享給其他人使用,可以在 n8n workflow 右上角點選「...」,選擇 Download 來匯出。
我將自己花幾天整理的 workflow 分享給大家,以下是 workflow.json 範例:
workflow.json
檔案。{
"name": "Gitlab Code Review- Prod",
"nodes": [
{
"parameters": {
"promptType": "define",
"text": "=請你扮演一位資深軟體工程師,針對程式碼進行全面性的 Code Review。請依照以下面向給出具體建議:\n\n1. ✅ 程式邏輯是否正確、是否有潛在錯誤或邊界條件未處理\n2. 🧹 是否符合的慣用寫法,例如錯誤處理、命名風格、簡潔性\n3. 🔒 是否有安全性問題,例如硬編碼、未處理的錯誤、資源洩漏等\n4. 🚀 是否有效能瓶頸,例如不必要的記憶體分配、重複運算、goroutine 使用不當\n5. 📚 是否具備良好的可讀性與可維護性,包括註解、結構清晰度、模組化程度\n\n在評估每個檔案時,請遵循以下結構化步驟:\n1. 先逐個面向分析問題,並列出證據。\n2. 綜合所有問題,決定 severity 等級。\n3. 只在有明確證據時提升 severity,避免過度評估。\n\nseverity 等級的詳細定義(必須嚴格遵守):\n- high(高風險):問題可能導致系統崩潰、安全漏洞、資料洩漏、嚴重效能衰退,或在生產環境造成重大損失。例如:未處理的 nil 指標、SQL 注入風險、無限迴圈。\n- medium(中風險):問題可能引起偶發錯誤、非最佳實踐,但不立即危險。例如:未優化的迴圈、缺少錯誤檢查、輕微資源浪費。\n- low(低風險):小問題,如命名不一致、缺少註解、多餘空白,但不影響功能或效能。\n- no problem(無問題):不用輸出到 JSON,所有面向皆符合最佳實踐,無任何可改進點或者單元測試的 error 寫底線也是可以,放到 no problem 例如: req, _ := http.NewRequest(\"GET\", \"/ping\", nil)\n\n範例:\n假設程式碼片段:\nfunc add(a, b int) int { return a + b } // 簡單加法,無問題。\n- 分析:邏輯正確、無錯誤;慣用寫法符合;無安全問題;效能佳;可讀性高。\n- severity: no problem(不輸出到 JSON)。\n\n另一範例:\nfunc connectDB() { db, _ = sql.Open(\"mysql\", \"user:pass@tcp(host)/db\") } // 硬編碼密碼。\n- 分析:邏輯正確,但安全問題(硬編碼憑證);其他面向 OK。\n- severity: high(高風險,因為安全漏洞)。\n- issues_found: [\"硬編碼憑證\"]\n- comment: \"檔案中存在硬編碼資料庫憑證的安全漏洞,建議使用環境變數或秘密管理工具。\"\n\n以下是程式碼內容:\n\n{{ $json[\"changes_with_first_line\"].toJsonString() }}\n\n---\n\n你的回覆必須嚴格遵守以下結構,且不得包含標題與 JSON 塊以外的任何文字:\n1. 先輸出標題:GitLab Discussion Request\n2. 然後立即輸出以 ```json\n\nJSON 格式如下(抓取元 input json 資料不要異動):\n\n每個檔案最多一個討論\n\n討論行對應該檔案的第一個變更行\n\ncomment 必須包含該檔案所有重要問題的綜合評論,使用繁體中文\n\nissues_found 為陣列,每項為短字串描述主要問題,不可用段落文字\n\nseverity 分為四個等級:high(高風險)、medium(中風險)、low(低風險)、no problem(無問題)。請分析所有檔案,但只在 JSON 回應中輸出 severity 為 high、medium、low 的檔案。\n如果檔案評估為 no problem,則不要在 gitlab_discussions 陣列中包含該檔案。\n\n回應的 position 拿取原本的資料 {{ $json[\"changes_with_first_line\"].toJsonString() }}\n\n```json\n{\n \"gitlab_discussions\": [\n {\n \"file_path\": \"<檔案完整路徑>\",\n \"comment\": \"<針對整個檔案的重要問題綜合評論,繁體中文>\",\n \"severity\": \"high|medium|low|no problem\",\n \"issues_found\": [\"短字串描述主要問題\"],\n \"position\": {\n \"base_sha\": \"<base sha>\",\n \"head_sha\": \"<head commit sha>\",\n \"start_sha\": \"<start commit sha>\",\n \"position_type\": \"text\",\n \"new_path\": \"<檔案完整路徑>\",\n \"old_path\": \"<檔案完整路徑>\",\n \"new_line\": null or 數字(依照 input),\n \"old_line\": null or 數字(依照 input)\n }\n }\n ]\n}",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 2.2,
"position": [
1584,
592
],
"id": "d7a3205d-fb14-4b89-897c-a95bd4e626c2",
"name": "AI Agent1",
"notes": "const PROMPT = `\n請你扮演一位資深 Golang 工程師,針對以下 Go 程式碼進行全面性的 Code Review。請依照以下面向給出具體建議:\n\n1. ✅ 程式邏輯是否正確、是否有潛在錯誤或邊界條件未處理\n2. 🧹 是否符合 Go 的慣用寫法(idiomatic Go),例如錯誤處理、命名風格、簡潔性\n3. 🔒 是否有安全性問題,例如硬編碼、未處理的錯誤、資源洩漏等\n4. 🚀 是否有效能瓶頸,例如不必要的記憶體分配、重複運算、goroutine 使用不當\n5. 📚 是否具備良好的可讀性與可維護性,包括註解、結構清晰度、模組化程度\n\n請以條列方式回覆,每一點請包含:\n- 問題描述\n- 改善建議\n- 若有需要,請附上修改後的程式碼片段\n\n以下是程式碼內容:\n\\`\\`\\`go\n{{ $json[\"mergedDiffs\"] }}\n\\`\\`\\`\n\n---\n\n### 📝 GitLab Discussion Request\n請在回覆最後,**務必提供一份合法 JSON 區塊**,格式如下(每個檔案最多一個討論):\n\n\\`\\`\\`json\n{\n \"gitlab_discussions\": [\n {\n \"file_path\": \"完整檔案路徑\",\n \"comment\": \"針對整個檔案的重要評論(繁體中文,綜合性總結)\",\n \"severity\": \"high|medium|low\",\n \"issues_found\": [\"短字串問題1\", \"短字串問題2\"]\n }\n ]\n}\n\\`\\`\\`\n\n**重要規則:**\n1. 請務必輸出合法 JSON,且只出現一次 JSON 區塊\n2. 每個檔案最多一個討論,在該檔案的第一個異動行留言\n3. comment 要包含該檔案所有重要問題的綜合評論\n4. 只針對 severity 為 \"high\" 或 \"medium\" 的檔案提供此資料\n5. issues_found 請用短字串(避免段落文字)\n`;\n"
},
{
"parameters": {
"sessionIdType": "customKey",
"sessionKey": "gitlab-ai-code-review",
"contextWindowLength": 50
},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.3,
"position": [
1680,
976
],
"id": "e300a7fb-4ee6-4dba-829a-96304088c119",
"name": "Simple Memory1"
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
1584,
832
],
"id": "1d6b0f80-5ae4-4571-81a7-3daa78a97115",
"name": "Google Gemini Chat Model1",
"credentials": {
"googlePalmApi": {
"id": "gemini key",
"name": "Google Gemini(PaLM) Api account"
}
}
},
{
"parameters": {
"content": "(線上 prod)\n只針對異常的檔案留言給建議\n"
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-432,
624
],
"typeVersion": 1,
"id": "f92b3c9b-1ee3-4dfe-a435-1fcfe28ea0fc",
"name": "Sticky Note1"
},
{
"parameters": {
"method": "POST",
"url": "=https://{your gitlab host}/api/v4/projects/{{ $node[\"get diff\"].json[\"project_id\"] }}/merge_requests/{{ $node[\"get diff\"].json[\"iid\"] }}/notes",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "PRIVATE-TOKEN",
"value": "={{ $('gitlab-project token list').item.json.token }}"
}
]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "body",
"value": "=沒有異常內容, {{ $json.summary }}"
}
]
},
"options": {
"redirect": {
"redirect": {}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2432,
768
],
"id": "3e777f57-0e81-44e0-8c85-8bd2dbc2a3d8",
"name": "note summary"
},
{
"parameters": {
"jsCode": "const changes = $json.changes || [];\nconst headSha = $json?.diff_refs?.head_sha || '';\nconst baseSha = $json?.diff_refs?.base_sha || '';\nconst startSha = $json?.diff_refs?.start_sha || baseSha;\n\nconst projectId = $json.project_id || 0;\nconst iid = $json.iid || 0;\n\n// 只解析每個 diff 的第一個異動行\nfunction getFirstChangedLine(diff) {\n const lines = diff.split('\\n');\n let oldLineNum = 0;\n let newLineNum = 0;\n let headerFound = false;\n\n for (const line of lines) {\n if (line.startsWith('@@')) {\n const match = line.match(/@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/);\n if (match) {\n oldLineNum = parseInt(match[1]) - 1;\n newLineNum = parseInt(match[2]) - 1;\n headerFound = true;\n }\n } else if (headerFound && line.startsWith('+') && !line.startsWith('+++')) {\n newLineNum++;\n return {\n type: 'added',\n old_line: null,\n new_line: newLineNum,\n content: line.substring(1),\n original_line: line\n };\n } else if (headerFound && line.startsWith('-') && !line.startsWith('---')) {\n oldLineNum++;\n return {\n type: 'removed',\n old_line: oldLineNum,\n new_line: null,\n content: line.substring(1),\n original_line: line\n };\n } else if (headerFound && line.startsWith(' ')) {\n oldLineNum++;\n newLineNum++;\n }\n }\n\n return null;\n}\n\n// 為每個檔案生成第一個變更行資料\nconst changesWithFirstLine = changes.map(change => {\n const firstChangedLine = getFirstChangedLine(change.diff);\n \n if (!firstChangedLine) {\n return null;\n }\n\n return {\n diff: change.diff,\n new_path: change.new_path,\n old_path: change.old_path || change.new_path, // 確保 old_path 不為空\n new_file: change.new_file,\n deleted_file: change.deleted_file,\n old_line: firstChangedLine.old_line,\n new_line: firstChangedLine.new_line,\n baseSha: baseSha,\n headSha: headSha,\n startSha: startSha\n };\n}).filter(item => item !== null); // 過濾掉無效項目\n\n// 輸出 JSON,欄位符合 GitLab Discussion API\nreturn [\n {\n json: {\n project_id: projectId,\n iid,\n changes_with_first_line: changesWithFirstLine\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1296,
592
],
"id": "31be98c8-a92d-48a9-b110-f6834483994a",
"name": "get diff"
},
{
"parameters": {
"jsCode": "// ===== 只抓 GitLab Discussion Request =====\nfunction extractGitlabDiscussion(text) {\n if (!text) return null;\n\n // 找到 GitLab Discussion Request 標題\n const marker = 'GitLab Discussion Request';\n const markerIndex = text.indexOf(marker);\n if (markerIndex === -1) return null;\n\n // 從 marker 開始找 ```json ... ```\n const jsonStart = text.indexOf('```json', markerIndex);\n const jsonEnd = text.indexOf('```', jsonStart + 1);\n if (jsonStart === -1 || jsonEnd === -1) return null;\n\n // 擷取區塊,去掉 ```json\n const jsonStr = text.slice(jsonStart + 7, jsonEnd).trim();\n\n try {\n return JSON.parse(jsonStr);\n } catch (err) {\n console.error('GitLab Discussion JSON 解析失敗:', err);\n return null;\n }\n}\n\ntry {\n // 取得 AI 回應字串\n const aiResponse = $json.output \n || $json.response \n || $input.all()[0]?.json?.output \n || $input.all()[0]?.json?.response \n || '';\n\n if (!aiResponse) {\n return [{\n json: {\n summary: '錯誤:未收到 AI 回應',\n discussions: []\n }\n }];\n }\n\n // 解析 GitLab Discussion JSON\n const parsedData = extractGitlabDiscussion(aiResponse);\n\n if (parsedData && parsedData.gitlab_discussions?.length > 0) {\n return [{\n json: {\n summary: `找到 ${parsedData.gitlab_discussions.length} 個討論項目`,\n discussions: parsedData.gitlab_discussions\n }\n }];\n }\n\n return [{\n json: {\n summary: 'AI 回應中沒有需要討論的異常檔案',\n discussions: []\n }\n }];\n} catch (error) {\n return [{\n json: {\n summary: `執行失敗:${error.message}`,\n discussions: []\n }\n }];\n}\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1936,
592
],
"id": "3ecea285-252d-46eb-a9da-204015d2662f",
"name": "generate discussion data "
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "7875626d-0b2d-4bc8-a473-779854b05c03",
"leftValue": "={{ $json.discussions }}",
"rightValue": "[",
"operator": {
"type": "array",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2144,
592
],
"id": "3393a3b1-97a8-452e-80ce-6c49d30deb8b",
"name": "if has discussion"
},
{
"parameters": {
"jsCode": "const projectId = $node[\"get diff\"].json[\"project_id\"];\nconst iid = $node[\"get diff\"].json[\"iid\"];\n// 回傳格式與原始 JSON 類似\nlet result = $input.first().json.discussions.map((discussion) => {\n return {\n ...discussion,\n projectId,\n iid\n }\n})\nreturn result;\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2432,
576
],
"id": "3679d630-7561-4b0a-bd19-dde02934b71e",
"name": "to items"
},
{
"parameters": {
"method": "POST",
"url": "=https://{{your gitlab host}}.vip/api/v4/projects/{{ $json.projectId }}/merge_requests/{{ $json.iid}}/discussions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "=PRIVATE-TOKEN",
"value": "={{ $('gitlab-project token list').item.json.token }}"
}
]
},
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"name": "body",
"value": "=- 嚴重程度: {{ $json.severity }}\n- 異常摘要:{{ $json.issues_found }}\n- 評論: {{ $json.comment }}"
},
{
"name": "position[position_type]",
"value": "text"
},
{
"name": "position[old_path]",
"value": "={{ $json.position.old_path }}"
},
{
"name": "position[new_path]",
"value": "={{ $json.position.new_path }}"
},
{
"name": "position[start_sha]",
"value": "={{ $json.position.start_sha }}"
},
{
"name": "position[head_sha]",
"value": "={{ $json.position.head_sha }}"
},
{
"name": "position[base_sha]",
"value": "={{ $json.position.base_sha }}"
},
{
"name": "=position[new_line]",
"value": "={{ $json.position.new_line !== null ? $json.position.new_line : '' }}"
},
{
"parameterType": "=formData",
"name": "=position[old_line]",
"value": "={{ $json.position.old_line !== null ? $json.position.old_line : '' }}"
}
]
},
"options": {
"redirect": {
"redirect": {}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2768,
576
],
"id": "9c981c2a-a937-4da0-99de-ddb0eb434b74",
"name": "discuss(comment) each file"
},
{
"parameters": {
"jsCode": "// 專案 token 設定: 格式 key:id, value:token\n// 目前多 token(credential) 整合到同一個 workflow 沒有支援,所以使用程式做整併\n// 參考網址: https://community.n8n.io/t/use-the-same-workflow-with-different-credentials/56940\nvar mapToken = new Map();\nmapToken.set(\"{project_id}\", \"glpat-XXXXX\") // callduck\nmapToken.set(\"{project_id}\", \"glpat-XXXXX\") // beacon\n\nreturn $input.all().map(item => {\n const projectId = item.json.body.project.id;\n const iid = item.json.body.object_attributes.iid\n const token = mapToken.get(projectId.toString());\n const sourceBranch = item.json.body.object_attributes.source_branch\n return {\n json: {\n projectId: projectId,\n iid: iid,\n token: token,\n sourceBranch: sourceBranch\n }\n }\n})"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
688,
592
],
"id": "8b3955bf-ef9f-4751-bacd-41b41a3fa900",
"name": "gitlab-project token list"
},
{
"parameters": {
"httpMethod": "POST",
"path": "{generated_uuid}",
"options": {}
},
"id": "80014652-48ee-4719-89a4-71e5923c0a70",
"name": "GitLab Webhook(Beacon)",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
48,
848
],
"webhookDescription": {
"httpMethod": "POST",
"responseMode": "onReceived",
"responseData": "default"
},
"webhookId": "cd96d9d1-1bf7-4781-958e-3bdf590f36fa"
},
{
"parameters": {
"url": "=https:/{yourgitlab host}/api/v4/projects/{{ $json[\"projectId\"] }}/merge_requests/{{ $json[\"iid\"] }}/changes",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "gitlabApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "PRIVATE-TOKEN",
"value": "={{ $json.token }}"
}
]
},
"options": {
"redirect": {
"redirect": {}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
992,
592
],
"id": "c4412d8f-1f25-4ef2-a9c0-445ccefd7895",
"name": "diff",
"credentials": {
"gitlabApi": {
"id": "crzxH5zCI2MdAxsv",
"name": "GitLab account"
}
}
},
{
"parameters": {
"httpMethod": "POST",
"path": "d73bba06-4622-41d9-acb7-4c94412ac5af",
"options": {}
},
"id": "89e368fc-0928-498d-b1de-6dca15851260",
"name": "GitLab Webhook(Nami)",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
48,
608
],
"webhookDescription": {
"httpMethod": "POST",
"responseMode": "onReceived",
"responseData": "default"
},
"webhookId": "d73bba06-4622-41d9-acb7-4c94412ac5af",
"disabled": true
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "650f84b9-7505-47b5-add0-7a3354592b08",
"leftValue": "={{ \n (\n $json.body.object_attributes.action === \"open\" ||\n $json.body.object_attributes.action === \"update\" \n ) && \n $json.body.object_attributes.state === \"opened\" \n}}",
"rightValue": "\"update\"",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
368,
608
],
"id": "5c83a22a-8df6-49ac-9aca-094656a27618",
"name": "need code review or not"
}
],
"pinData": {},
"connections": {
"AI Agent1": {
"main": [
[
{
"node": "generate discussion data ",
"type": "main",
"index": 0
}
]
]
},
"Simple Memory1": {
"ai_memory": [
[
{
"node": "AI Agent1",
"type": "ai_memory",
"index": 0
}
]
]
},
"Google Gemini Chat Model1": {
"ai_languageModel": [
[
{
"node": "AI Agent1",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"get diff": {
"main": [
[
{
"node": "AI Agent1",
"type": "main",
"index": 0
}
]
]
},
"generate discussion data ": {
"main": [
[
{
"node": "if has discussion",
"type": "main",
"index": 0
}
]
]
},
"if has discussion": {
"main": [
[
{
"node": "to items",
"type": "main",
"index": 0
}
],
[
{
"node": "note summary",
"type": "main",
"index": 0
}
]
]
},
"to items": {
"main": [
[
{
"node": "discuss(comment) each file",
"type": "main",
"index": 0
}
]
]
},
"gitlab-project token list": {
"main": [
[
{
"node": "diff",
"type": "main",
"index": 0
}
]
]
},
"GitLab Webhook(Beacon)": {
"main": [
[
{
"node": "need code review or not",
"type": "main",
"index": 0
}
]
]
},
"diff": {
"main": [
[
{
"node": "get diff",
"type": "main",
"index": 0
}
]
]
},
"GitLab Webhook(Nami)": {
"main": [
[
{
"node": "need code review or not",
"type": "main",
"index": 0
}
]
]
},
"need code review or not": {
"main": [
[
{
"node": "gitlab-project token list",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"saveExecutionProgress": true,
"callerPolicy": "workflowsFromSameOwner"
},
"versionId": "30aa51fb-05c5-413c-b85b-8e01995791ac",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "d1c1e5a8f255713e433449e47160fa73b6ec995c5402084cb4d58e55b2e5a66c"
},
"id": "yLjkgrtCIGEVjllZ",
"tags": []
}
匯入後,畫面上可能會看到幾個節點出現驚嘆號,需要調整以下設定:
GitLab Webhook:移除 {generated_uuid},讓 n8n 自動產出 UUID。
HTTP Request 節點(diff + discuss(comment each file)):調整 GitLab Host 與 Access Token。
gitlab-project-token list:填入你的 Access Token。
AI Agent:選擇你要的模型與 API Key。
總之,所有金鑰與 token 等敏感資訊都需要自行填寫。完成後即可直接使用,是不是超級方便!
今天分享了如何透過 n8n 匯出與匯入模板,也公開了我做好的 AI Code Review workflow。
若在使用上有任何問題或心得,歡迎隨時回饋給我,我會非常樂意與大家交流。