76일차 자료 추가 (답 포함): 실습 15개 · 의도 · 생활 속 예 · 실행 결과, LAB-A · 정답 워크플로우와 25단계 실행 기록, 교안 개요 30장 · 4단계 정리, 바이브 코딩 tretry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f36605043
commit
bbf17e4696
141 files changed
+34072
No files matched your search
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"name": "76 Intake baseline (provided)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "base-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "edb7dfdf-1a51-539f-8d5c-3c4020d1591e",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "6d720b12-5983-53de-b5bb-adccbf9de50e"
|
||||
},
|
||||
{
|
||||
"name": "One item per request",
|
||||
"type": "n8n-nodes-base.splitOut",
|
||||
"typeVersion": 1,
|
||||
"parameters": {
|
||||
"fieldToSplitOut": "body.requests",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "38e4b8e8-57ef-58c3-970c-1db6fbb0945a"
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "e4773f33-6534-57b1-b8a1-c18cab698b48"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
520
|
||||
],
|
||||
"id": "c568219f-c8be-59b4-a28c-c4689db2ab9e"
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item, i) => {\n const req = $('One item per request').all()[i].json;\n let c = {};\n try { c = JSON.parse(String(item.json.output ?? '').replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { c = {}; }\n return { json: { ...req, category: c.category ?? '기타', priority: c.priority ?? '보통' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "5ee2e1dc-dc92-5494-a906-04dbeae4e76b"
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"options": {},
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $execution.id + '-' + $itemIndex, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "9bd6683a-c105-52e2-902a-3d1e8c09bd4a"
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const tickets = $input.all().map((item) => item.json.ticket_id);\nreturn [{ json: { processed: tickets.length, tickets } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "d43029f1-3966-577d-87d6-b5152e764748"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "f423ab4b-e044-53bf-90d2-d7158256525e"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "One item per request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"One item per request": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
{
|
||||
"name": "76 Intake · retry and idempotency key (reference, Lab 01 example 2)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.requests) }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "{ \"error\": \"requests must be a list\" }",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const requests = $('Intake webhook').first().json.body.requests;\nreturn requests.map((r, index) => {\n const problems = [];\n if (!r || typeof r !== 'object') return { json: { index, valid: false, reason: 'not an object', id: null } };\n if (!String(r.id ?? '').trim()) problems.push('id is required');\n if (typeof r.text !== 'string' || !r.text.trim()) problems.push('text is required');\n if (!String(r.user ?? '').trim()) problems.push('user is required');\n return { json: { index, valid: problems.length === 0, reason: problems.join(', '), id: r.id ?? null, user: r.user ?? '', text: typeof r.text === 'string' ? r.text.trim() : '' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Valid?').all().filter((item) => item.json.valid);\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n let c = {};\n try { c = JSON.parse(String(item.json.output ?? '').replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { c = {}; }\n return { json: { ...req, category: c.category ?? '기타', priority: c.priority ?? '보통', classified: true } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $json.id, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 300,
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Mark results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Parse').all();\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n if (item.json.ticket_id) return { json: { id: req.id, status: 'created', ticket_id: item.json.ticket_id, duplicate: item.json.duplicate === true, category: req.category, priority: req.priority } };\n const message = item.json.error?.message ?? item.json.error ?? 'create failed';\n return { json: { id: req.id, status: 'failed', reason: String(message).slice(0, 200), request: { id: req.id, user: req.user, text: req.text } } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mark rejected",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item) => ({ json: { id: item.json.id, status: 'rejected', reason: item.json.reason } }));"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst created = rows.filter((r) => r.status === 'created');\nconst failed = rows.filter((r) => r.status === 'failed');\nconst rejected = rows.filter((r) => r.status === 'rejected');\nreturn [{ json: { total: rows.length, created: created.length, failed: failed.length, rejected: rejected.length,\n duplicates: created.filter((r) => r.duplicate).length, unclassified: created.filter((r) => r.category === '미분류').length,\n results: rows.map(({ request, ...rest }) => rest), failed_requests: failed.map((r) => r.request) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Public result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const { failed_requests, ...rest } = $('Summarize').first().json;\nreturn [{ json: rest }];"
|
||||
},
|
||||
"position": [
|
||||
3300,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3520,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Valid?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Valid?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Mark rejected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mark results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark rejected": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Public result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
{
|
||||
"name": "76 Intake · DLQ (reference, Lab 02 example 1, includes Lab 01 tasks)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.requests) && $json.body.requests.length <= 10 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ error: Array.isArray($json.body.requests) ? 'too many requests (max 10)' : 'requests must be a list' }) }}",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const requests = $('Intake webhook').first().json.body.requests;\nconst seen = new Set();\nreturn requests.map((r, index) => {\n const problems = [];\n if (!r || typeof r !== 'object') return { json: { index, valid: false, reason: 'not an object', id: null } };\n if (!String(r.id ?? '').trim()) problems.push('id is required');\n if (typeof r.text !== 'string' || !r.text.trim()) problems.push('text is required');\n if (!String(r.user ?? '').trim()) problems.push('user is required');\n const id = String(r.id ?? '').trim();\n if (id && seen.has(id)) problems.push('duplicate id in this request');\n if (id) seen.add(id);\n return { json: { index, valid: problems.length === 0, reason: problems.join(', '), id: r.id ?? null, user: r.user ?? '', text: typeof r.text === 'string' ? r.text.trim() : '' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Valid?').all().filter((item) => item.json.valid);\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n let c = {}, fallback = false;\n if (item.json.error || item.json.output === undefined) { fallback = true; }\n else { try { c = JSON.parse(String(item.json.output).replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { fallback = true; } }\n return { json: { ...req, category: fallback ? '미분류' : (c.category ?? '기타'), priority: fallback ? '보통' : (c.priority ?? '보통'), classified: !fallback } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $json.id, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 300,
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Mark results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Parse').all();\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n if (item.json.ticket_id) return { json: { id: req.id, status: 'created', ticket_id: item.json.ticket_id, duplicate: item.json.duplicate === true, category: req.category, priority: req.priority } };\n const message = item.json.error?.message ?? item.json.error ?? 'create failed';\n return { json: { id: req.id, status: 'failed', reason: String(message).slice(0, 200), request: { id: req.id, user: req.user, text: req.text } } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mark rejected",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item) => ({ json: { id: item.json.id, status: 'rejected', reason: item.json.reason } }));"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst created = rows.filter((r) => r.status === 'created');\nconst failed = rows.filter((r) => r.status === 'failed');\nconst rejected = rows.filter((r) => r.status === 'rejected');\nreturn [{ json: { total: rows.length, created: created.length, failed: failed.length, rejected: rejected.length,\n duplicates: created.filter((r) => r.duplicate).length, unclassified: created.filter((r) => r.category === '미분류').length,\n results: rows.map(({ request, ...rest }) => rest), failed_requests: failed.map((r) => r.request) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Public result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const { failed_requests, ...rest } = $('Summarize').first().json;\nreturn [{ json: rest }];"
|
||||
},
|
||||
"position": [
|
||||
3300,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3520,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Any failed?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.failed > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send to DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: $json.failed_requests.map((r, i) => ({ item: r, reason: $json.results.filter((x) => x.status === 'failed')[i]?.reason ?? 'failed', workflow: 'intake' })) }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
140
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Valid?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Valid?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Mark rejected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mark results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark rejected": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Public result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Any failed?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Any failed?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send to DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send to DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"id": "errHandler76001",
|
||||
"name": "76 Error handler (reference)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "On error",
|
||||
"type": "n8n-nodes-base.errorTrigger",
|
||||
"typeVersion": 1,
|
||||
"parameters": {},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Make alert",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const e = $json;\nreturn [{ json: { workflow: e.workflow?.name ?? '?', workflow_id: e.workflow?.id ?? '', node: e.execution?.lastNodeExecuted ?? e.execution?.error?.node?.name ?? '?',\n message: String(e.execution?.error?.message ?? 'unknown error').slice(0, 300), execution_id: e.execution?.id ?? '', severity: 'critical', reason: 'unexpected' } }];"
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send alert",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/alerts",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify($json) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"On error": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Make alert",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Make alert": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send alert",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
{
|
||||
"name": "76 Intake · DLQ + error workflow (reference, Lab 02 example 2)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.requests) && $json.body.requests.length <= 10 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ error: Array.isArray($json.body.requests) ? 'too many requests (max 10)' : 'requests must be a list' }) }}",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const requests = $('Intake webhook').first().json.body.requests;\nconst seen = new Set();\nreturn requests.map((r, index) => {\n const problems = [];\n if (!r || typeof r !== 'object') return { json: { index, valid: false, reason: 'not an object', id: null } };\n if (!String(r.id ?? '').trim()) problems.push('id is required');\n if (typeof r.text !== 'string' || !r.text.trim()) problems.push('text is required');\n if (!String(r.user ?? '').trim()) problems.push('user is required');\n const id = String(r.id ?? '').trim();\n if (id && seen.has(id)) problems.push('duplicate id in this request');\n if (id) seen.add(id);\n return { json: { index, valid: problems.length === 0, reason: problems.join(', '), id: r.id ?? null, user: r.user ?? '', text: typeof r.text === 'string' ? r.text.trim() : '' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Valid?').all().filter((item) => item.json.valid);\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n let c = {}, fallback = false;\n if (item.json.error || item.json.output === undefined) { fallback = true; }\n else { try { c = JSON.parse(String(item.json.output).replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { fallback = true; } }\n return { json: { ...req, category: fallback ? '미분류' : (c.category ?? '기타'), priority: fallback ? '보통' : (c.priority ?? '보통'), classified: !fallback } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $json.id, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 300,
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Mark results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Parse').all();\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n if (item.json.ticket_id) return { json: { id: req.id, status: 'created', ticket_id: item.json.ticket_id, duplicate: item.json.duplicate === true, category: req.category, priority: req.priority } };\n const message = item.json.error?.message ?? item.json.error ?? 'create failed';\n return { json: { id: req.id, status: 'failed', reason: String(message).slice(0, 200), request: { id: req.id, user: req.user, text: req.text } } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mark rejected",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item) => ({ json: { id: item.json.id, status: 'rejected', reason: item.json.reason } }));"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst created = rows.filter((r) => r.status === 'created');\nconst failed = rows.filter((r) => r.status === 'failed');\nconst rejected = rows.filter((r) => r.status === 'rejected');\nreturn [{ json: { total: rows.length, created: created.length, failed: failed.length, rejected: rejected.length,\n duplicates: created.filter((r) => r.duplicate).length, unclassified: created.filter((r) => r.category === '미분류').length,\n results: rows.map(({ request, ...rest }) => rest), failed_requests: failed.map((r) => r.request) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Public result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const { failed_requests, ...rest } = $('Summarize').first().json;\nreturn [{ json: rest }];"
|
||||
},
|
||||
"position": [
|
||||
3300,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3520,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Any failed?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.failed > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send to DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: $json.failed_requests.map((r, i) => ({ item: r, reason: $json.results.filter((x) => x.status === 'failed')[i]?.reason ?? 'failed', workflow: 'intake' })) }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
140
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Valid?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Valid?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Mark rejected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mark results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark rejected": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Public result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Any failed?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Any failed?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send to DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send to DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"errorWorkflow": "errHandler76001"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
{
|
||||
"name": "76 Replay (reference, Lab 03 example 1)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Replay webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-replay",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Pick items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const items = ($json.items ?? []).filter((e) => !e.parked);\nreturn items.map((e) => ({ json: { ...e, action: 'replay' } }));"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"alwaysOutputData": true
|
||||
},
|
||||
{
|
||||
"name": "Has items?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.dlq_id }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "exists",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Nothing to do",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ replayed: 0, ok: 0, still_failed: 0, parked: 0 }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
520
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Run intake again",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:5678/webhook/my-intake",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ requests: [$json.item] }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
},
|
||||
"timeout": 60000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Outcome",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const picked = $('Has items?').all().filter((item) => item.json.action === 'replay');\nreturn $input.all().map((item, i) => ({ json: { dlq_id: picked[i].json.dlq_id, id: picked[i].json.item?.id, ok: item.json.created === 1, duplicate: item.json.duplicates === 1 } }));"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Remove done",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/dlq/update",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ dlq_id: $json.dlq_id, remove: $json.ok }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst outcomes = ran('Outcome') ? $('Outcome').all().map((i) => i.json) : [];\nconst parked = 0;\nconst ok = outcomes.filter((o) => o.ok).length;\nreturn [{ json: { replayed: outcomes.length, ok, still_failed: outcomes.length - ok, parked, duplicates: outcomes.filter((o) => o.duplicate).length } }];"
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send summary",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Replay webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Pick items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Pick items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Has items?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Has items?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Run intake again",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Nothing to do",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Run intake again": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Outcome",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Outcome": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Remove done",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send summary",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Remove done": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "76 Nightly report (provided, has a bug)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Report webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "nightly-report",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "440333b3-117e-5245-be3f-741ec769bfa5",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "168590b9-ccec-5d1c-9375-34b12cfcc2bc"
|
||||
},
|
||||
{
|
||||
"name": "Get created",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/tickets-created",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "80bbb556-246d-5c36-8018-5d7e4fb65ac5"
|
||||
},
|
||||
{
|
||||
"name": "Count by category",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "// bug: the response has \"tickets\", not \"items\"\nconst rows = $json.items;\nconst counts = {};\nfor (const t of rows) counts[t.category] = (counts[t.category] ?? 0) + 1;\nreturn [{ json: { counts } }];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "d1f399a8-bc08-5ee4-84bc-5e2f7107d14b"
|
||||
},
|
||||
{
|
||||
"name": "Send report",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "52d3efaa-4a27-5d84-bb73-76af0e7c1dff"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Report webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get created",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get created": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Count by category",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Count by category": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send report",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"errorWorkflow": "errHandler76001"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user