This workflow follows the Agent → Chainllm recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "supporttriage001",
"name": "03-support-triage",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"authentication": "headerAuth",
"path": "support-triage",
"responseMode": "responseNode",
"options": {
"rawBody": true
}
},
"id": "50000000-0000-4000-8000-000000000001",
"name": "Ticket Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
300
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "const crypto = require('crypto');\nconst item = $input.first();\nconst secret = $env.ORDER_INTAKE_HMAC_SECRET || '';\nconst raw = item.binary && item.binary.data\n ? Buffer.from(item.binary.data.data, 'base64').toString('utf8')\n : JSON.stringify(item.json.body || {});\nconst headers = item.json.headers || {};\nconst provided = String(headers['x-signature'] || '').replace(/^sha256=/, '').trim();\nconst expected = crypto.createHmac('sha256', secret).update(raw).digest('hex');\nlet authOk = false;\ntry {\n authOk = provided.length === expected.length &&\n crypto.timingSafeEqual(Buffer.from(provided, 'hex'), Buffer.from(expected, 'hex'));\n} catch (e) { authOk = false; }\nreturn [{ json: { authOk, body: item.json.body || {} } }];"
},
"id": "50000000-0000-4000-8000-000000000002",
"name": "Verify HMAC",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "50000000-0000-4000-8000-000000000003-cond",
"leftValue": "={{ $json.authOk }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "50000000-0000-4000-8000-000000000003",
"name": "Auth OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
440,
300
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ ({ status: 'unauthorized', error: 'invalid or missing signature' }) }}",
"options": {
"responseCode": 401
}
},
"id": "50000000-0000-4000-8000-000000000004",
"name": "Respond Unauthorized",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
660,
460
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Normalise the ticket once, so every downstream node \u2014 both provider\n// chains, both parsers, the human queue and the dead-letter row \u2014 is\n// looking at the same fields rather than re-deriving them.\nconst body = $json.body || {};\nconst ticketId = String(body.ticket_id || body.id || '').trim();\nconst subject = String(body.subject || '').trim();\nconst text = String(body.body || body.message || '').trim();\nconst errors = [];\nif (!ticketId) errors.push('ticket_id is required');\nif (!subject && !text) errors.push('the ticket needs a subject or a body');\nreturn [{ json: {\n ticket_id: ticketId,\n subject,\n body: text,\n valid_input: errors.length === 0,\n input_errors: errors,\n prompt: 'Triage this support ticket.\\n\\nSubject: ' + subject + '\\n\\nBody: ' + text,\n} }];"
},
"id": "50000000-0000-4000-8000-000000000005",
"name": "Build Ticket",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
220
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "50000000-0000-4000-8000-000000000006-cond",
"leftValue": "={{ $json.valid_input }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "50000000-0000-4000-8000-000000000006",
"name": "Ticket Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
880,
220
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ ({ status: 'invalid', errors: $json.input_errors }) }}",
"options": {
"responseCode": 400
}
},
"id": "50000000-0000-4000-8000-000000000007",
"name": "Respond Invalid",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1100,
400
]
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.prompt }}",
"hasOutputParser": false,
"needsFallback": false,
"options": {
"systemMessage": "You are a support-ticket triage assistant. Reply with a SINGLE JSON object and nothing else. Fields: category (billing|technical|account|other), urgency (low|normal|high|critical), summary (one line, max 280 characters), requires_human (boolean). Do not wrap the JSON in markdown fences and do not add commentary.",
"maxIterations": 3,
"returnIntermediateSteps": false
}
},
"id": "50000000-0000-4000-8000-000000000008",
"name": "Triage Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
1100,
160
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 1000,
"onError": "stopWorkflow"
},
{
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "mock-triage-1"
},
"responsesApiEnabled": false,
"options": {
"timeout": 20000,
"maxRetries": 1,
"temperature": 0
}
},
"id": "50000000-0000-4000-8000-000000000009",
"name": "Primary Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"position": [
1040,
400
],
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "const PROVIDER = 'primary';\n\n// -------------------------------------------------------------------------\n// DETERMINISTIC PARSER. No model is involved in deciding whether a model's\n// answer is usable \u2014 that decision is made here, by code, every time, the same\n// way. An \"auto-fixing\" parser that asks another model to repair the first\n// model's output turns one non-deterministic step into two.\n// -------------------------------------------------------------------------\n\nconst SCHEMA = {\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://example.invalid/schemas/triage-output.schema.json\",\n \"title\": \"triage-output\",\n \"description\": \"The ONLY shape the support-triage workflow will act on. A model response is not trusted because it parsed \u2014 it is trusted because it conforms to this. Kept as a file so the contract is reviewable and diffable on its own; the workflow's parser embeds a copy, and a test asserts the two never drift.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\n \"category\",\n \"urgency\",\n \"summary\",\n \"requires_human\"\n ],\n \"properties\": {\n \"category\": {\n \"type\": \"string\",\n \"description\": \"Routing destination. A closed set: an unknown category cannot be routed, so it must fail validation rather than be forwarded to a queue that does not exist.\",\n \"enum\": [\n \"billing\",\n \"technical\",\n \"account\",\n \"other\"\n ]\n },\n \"urgency\": {\n \"type\": \"string\",\n \"description\": \"Closed set. A model that returns 7 or \\\"very high\\\" here must be rejected, not coerced \u2014 coercion is how a critical ticket silently becomes a normal one.\",\n \"enum\": [\n \"low\",\n \"normal\",\n \"high\",\n \"critical\"\n ]\n },\n \"summary\": {\n \"type\": \"string\",\n \"description\": \"One-line summary shown to a human. Bounded so a runaway response cannot be written into a UI field verbatim.\",\n \"minLength\": 1,\n \"maxLength\": 280\n },\n \"requires_human\": {\n \"type\": \"boolean\",\n \"description\": \"Strictly boolean. \\\"probably\\\" is not a decision an automated router can act on.\"\n }\n }\n };\n\n// Strip the wrapping a chat model puts around JSON. Handles a fenced block, and\n// prose on either side of one, because those are what models actually return \u2014\n// not because the prompt asked for them.\nfunction extractJson(text) {\n let t = String(text === null || text === undefined ? '' : text).trim();\n const fenced = t.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n if (fenced) t = fenced[1].trim();\n const first = t.indexOf('{');\n const last = t.lastIndexOf('}');\n if (first !== -1 && last > first) t = t.slice(first, last + 1);\n return t.trim();\n}\n\n// A small, explicit validator for exactly the subset of JSON Schema this\n// contract uses. Explicit beats a dependency here: the rules a response is held\n// to are readable in the same file that applies them.\nfunction validate(value, schema, path) {\n const where = path ? path + ': ' : '';\n const errors = [];\n\n if (schema.type === 'object') {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return [where + 'expected an object'];\n }\n for (const key of schema.required || []) {\n if (!(key in value)) errors.push(where + 'missing required field \"' + key + '\"');\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in (schema.properties || {}))) {\n errors.push(where + 'unexpected field \"' + key + '\"');\n }\n }\n }\n for (const key of Object.keys(schema.properties || {})) {\n if (key in value) {\n errors.push(...validate(value[key], schema.properties[key], path ? path + '.' + key : key));\n }\n }\n return errors;\n }\n\n if (schema.type === 'string') {\n if (typeof value !== 'string') return [where + 'expected a string, got ' + typeof value];\n if (schema.enum && schema.enum.indexOf(value) === -1) {\n errors.push(where + 'must be one of [' + schema.enum.join(', ') + '], got \"' + value + '\"');\n }\n if (schema.minLength !== undefined && value.length < schema.minLength) {\n errors.push(where + 'must be at least ' + schema.minLength + ' character(s)');\n }\n if (schema.maxLength !== undefined && value.length > schema.maxLength) {\n errors.push(where + 'must be at most ' + schema.maxLength + ' characters');\n }\n return errors;\n }\n\n if (schema.type === 'boolean') {\n if (typeof value !== 'boolean') return [where + 'expected a boolean, got ' + typeof value];\n return errors;\n }\n\n return errors;\n}\n\nconst ticket = $('Build Ticket').first().json;\nconst j = $input.first().json || {};\n// The agent answers on `output`, the chain on `text`.\nconst raw = j.output !== undefined ? j.output : (j.text !== undefined ? j.text : '');\n\nlet triage = null;\nlet errors = [];\nconst candidate = extractJson(raw);\n\nif (!candidate) {\n errors = ['the model returned an empty response'];\n} else {\n try {\n triage = JSON.parse(candidate);\n } catch (e) {\n errors = ['response is not valid JSON: ' + e.message];\n }\n if (triage !== null) errors = validate(triage, SCHEMA, '');\n}\n\nreturn [{ json: {\n ticket_id: ticket.ticket_id,\n provider: PROVIDER,\n valid: errors.length === 0,\n errors,\n triage,\n raw: String(raw).slice(0, 2000),\n} }];"
},
"id": "50000000-0000-4000-8000-000000000010",
"name": "Parse Triage",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
160
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "50000000-0000-4000-8000-000000000011-cond",
"leftValue": "={{ $json.valid }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "50000000-0000-4000-8000-000000000011",
"name": "Triage Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1540,
160
]
},
{
"parameters": {
"promptType": "define",
"text": "={{ 'The previous attempt to triage this ticket produced an unusable response (' + $json.errors.join('; ') + '). Reply with ONLY the JSON object.\\n\\n' + $('Build Ticket').first().json.prompt }}",
"needsFallback": false,
"messages": {
"messageValues": [
{
"type": "SystemMessagePromptTemplate",
"message": "You are a support-ticket triage assistant. Reply with a SINGLE JSON object and nothing else. Fields: category (billing|technical|account|other), urgency (low|normal|high|critical), summary (one line, max 280 characters), requires_human (boolean). Do not wrap the JSON in markdown fences and do not add commentary."
}
]
}
},
"id": "50000000-0000-4000-8000-000000000012",
"name": "Fallback Triage",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.9,
"position": [
1760,
340
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 1000,
"onError": "continueErrorOutput"
},
{
"parameters": {
"model": "deepseek-chat",
"options": {
"timeout": 20000,
"maxRetries": 1,
"temperature": 0
}
},
"id": "50000000-0000-4000-8000-000000000013",
"name": "Fallback Model",
"type": "@n8n/n8n-nodes-langchain.lmChatDeepSeek",
"typeVersion": 1,
"position": [
1700,
560
],
"credentials": {
"deepSeekApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "const PROVIDER = 'fallback';\n\n// -------------------------------------------------------------------------\n// DETERMINISTIC PARSER. No model is involved in deciding whether a model's\n// answer is usable \u2014 that decision is made here, by code, every time, the same\n// way. An \"auto-fixing\" parser that asks another model to repair the first\n// model's output turns one non-deterministic step into two.\n// -------------------------------------------------------------------------\n\nconst SCHEMA = {\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://example.invalid/schemas/triage-output.schema.json\",\n \"title\": \"triage-output\",\n \"description\": \"The ONLY shape the support-triage workflow will act on. A model response is not trusted because it parsed \u2014 it is trusted because it conforms to this. Kept as a file so the contract is reviewable and diffable on its own; the workflow's parser embeds a copy, and a test asserts the two never drift.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\n \"category\",\n \"urgency\",\n \"summary\",\n \"requires_human\"\n ],\n \"properties\": {\n \"category\": {\n \"type\": \"string\",\n \"description\": \"Routing destination. A closed set: an unknown category cannot be routed, so it must fail validation rather than be forwarded to a queue that does not exist.\",\n \"enum\": [\n \"billing\",\n \"technical\",\n \"account\",\n \"other\"\n ]\n },\n \"urgency\": {\n \"type\": \"string\",\n \"description\": \"Closed set. A model that returns 7 or \\\"very high\\\" here must be rejected, not coerced \u2014 coercion is how a critical ticket silently becomes a normal one.\",\n \"enum\": [\n \"low\",\n \"normal\",\n \"high\",\n \"critical\"\n ]\n },\n \"summary\": {\n \"type\": \"string\",\n \"description\": \"One-line summary shown to a human. Bounded so a runaway response cannot be written into a UI field verbatim.\",\n \"minLength\": 1,\n \"maxLength\": 280\n },\n \"requires_human\": {\n \"type\": \"boolean\",\n \"description\": \"Strictly boolean. \\\"probably\\\" is not a decision an automated router can act on.\"\n }\n }\n };\n\n// Strip the wrapping a chat model puts around JSON. Handles a fenced block, and\n// prose on either side of one, because those are what models actually return \u2014\n// not because the prompt asked for them.\nfunction extractJson(text) {\n let t = String(text === null || text === undefined ? '' : text).trim();\n const fenced = t.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n if (fenced) t = fenced[1].trim();\n const first = t.indexOf('{');\n const last = t.lastIndexOf('}');\n if (first !== -1 && last > first) t = t.slice(first, last + 1);\n return t.trim();\n}\n\n// A small, explicit validator for exactly the subset of JSON Schema this\n// contract uses. Explicit beats a dependency here: the rules a response is held\n// to are readable in the same file that applies them.\nfunction validate(value, schema, path) {\n const where = path ? path + ': ' : '';\n const errors = [];\n\n if (schema.type === 'object') {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return [where + 'expected an object'];\n }\n for (const key of schema.required || []) {\n if (!(key in value)) errors.push(where + 'missing required field \"' + key + '\"');\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in (schema.properties || {}))) {\n errors.push(where + 'unexpected field \"' + key + '\"');\n }\n }\n }\n for (const key of Object.keys(schema.properties || {})) {\n if (key in value) {\n errors.push(...validate(value[key], schema.properties[key], path ? path + '.' + key : key));\n }\n }\n return errors;\n }\n\n if (schema.type === 'string') {\n if (typeof value !== 'string') return [where + 'expected a string, got ' + typeof value];\n if (schema.enum && schema.enum.indexOf(value) === -1) {\n errors.push(where + 'must be one of [' + schema.enum.join(', ') + '], got \"' + value + '\"');\n }\n if (schema.minLength !== undefined && value.length < schema.minLength) {\n errors.push(where + 'must be at least ' + schema.minLength + ' character(s)');\n }\n if (schema.maxLength !== undefined && value.length > schema.maxLength) {\n errors.push(where + 'must be at most ' + schema.maxLength + ' characters');\n }\n return errors;\n }\n\n if (schema.type === 'boolean') {\n if (typeof value !== 'boolean') return [where + 'expected a boolean, got ' + typeof value];\n return errors;\n }\n\n return errors;\n}\n\nconst ticket = $('Build Ticket').first().json;\nconst j = $input.first().json || {};\n// The agent answers on `output`, the chain on `text`.\nconst raw = j.output !== undefined ? j.output : (j.text !== undefined ? j.text : '');\n\nlet triage = null;\nlet errors = [];\nconst candidate = extractJson(raw);\n\nif (!candidate) {\n errors = ['the model returned an empty response'];\n} else {\n try {\n triage = JSON.parse(candidate);\n } catch (e) {\n errors = ['response is not valid JSON: ' + e.message];\n }\n if (triage !== null) errors = validate(triage, SCHEMA, '');\n}\n\nreturn [{ json: {\n ticket_id: ticket.ticket_id,\n provider: PROVIDER,\n valid: errors.length === 0,\n errors,\n triage,\n raw: String(raw).slice(0, 2000),\n} }];"
},
"id": "50000000-0000-4000-8000-000000000014",
"name": "Parse Fallback",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1980,
340
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "50000000-0000-4000-8000-000000000015-cond",
"leftValue": "={{ $json.valid }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "50000000-0000-4000-8000-000000000015",
"name": "Fallback Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2200,
340
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO triage_result (ticket_id, execution_id, category, urgency, summary, requires_human, provider) VALUES ($1, $2, $3, $4, $5, $6::boolean, $7) ON CONFLICT (ticket_id) DO UPDATE SET category = EXCLUDED.category, urgency = EXCLUDED.urgency, summary = EXCLUDED.summary, requires_human = EXCLUDED.requires_human, provider = EXCLUDED.provider, execution_id = EXCLUDED.execution_id, updated_at = now() RETURNING ticket_id, category, urgency, requires_human, provider;",
"options": {
"queryReplacement": "={{ (() => { const r = $json; return [r.ticket_id, $execution.id, r.triage.category, r.triage.urgency, r.triage.summary, r.triage.requires_human, r.provider]; })() }}"
}
},
"id": "50000000-0000-4000-8000-000000000016",
"name": "Record Triage",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.7,
"position": [
2420,
160
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ ({ status: 'triaged', ticket_id: $json.ticket_id, category: $json.category, urgency: $json.urgency, requires_human: $json.requires_human, provider: $json.provider }) }}",
"options": {
"responseCode": 200
}
},
"id": "50000000-0000-4000-8000-000000000017",
"name": "Respond Triaged",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
2640,
160
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Both ways of failing arrive here: the fallback returned something\n// unusable, or the fallback provider was unreachable. Normalising them\n// into one shape means the human queue, the dead-letter row and the\n// response cannot disagree about what happened.\nconst t = $('Build Ticket').first().json;\nconst primary = $('Parse Triage').first().json;\nconst j = $input.first().json || {};\n// A parsed result carries `valid`; an error item does not.\nconst providerFailed = j.valid === undefined;\nconst fallbackErrors = providerFailed\n ? ['fallback provider unavailable: ' + String((j.error && j.error.message) || 'unknown error')]\n : (j.errors || []);\nreturn [{ json: {\n ticket_id: t.ticket_id,\n reason: (providerFailed\n ? 'the fallback provider was unavailable'\n : 'no provider returned a usable triage') + ': ' + fallbackErrors.join('; '),\n raw: providerFailed ? '' : String(j.raw || ''),\n primary_errors: primary.errors || [],\n fallback_errors: fallbackErrors,\n subject: t.subject,\n body: t.body,\n} }];"
},
"id": "50000000-0000-4000-8000-000000000021",
"name": "Escalate To Human",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2200,
520
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO human_review_queue (ticket_id, execution_id, reason, raw_output, payload) VALUES ($1, $2, $3, $4, $5::jsonb) RETURNING id;",
"options": {
"queryReplacement": "={{ (() => { const r = $json; return [r.ticket_id, $execution.id, r.reason, r.raw, JSON.stringify({ subject: r.subject, body: r.body })]; })() }}"
}
},
"id": "50000000-0000-4000-8000-000000000018",
"name": "Queue For Human",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.7,
"position": [
2420,
520
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO dead_letter (workflow, execution_id, order_id, reason, payload) VALUES ($1, $2, $3, $4, $5::jsonb) RETURNING id;",
"options": {
"queryReplacement": "={{ (() => { const r = $('Escalate To Human').first().json; return ['03-support-triage', $execution.id, r.ticket_id, 'automated triage produced no usable result', JSON.stringify({ subject: r.subject, body: r.body, primary_errors: r.primary_errors, fallback_errors: r.fallback_errors })]; })() }}"
}
},
"id": "50000000-0000-4000-8000-000000000019",
"name": "Dead-letter Ticket",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.7,
"position": [
2640,
520
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ ({ status: 'queued_for_human', ticket_id: $('Escalate To Human').first().json.ticket_id, detail: 'automated triage could not produce a usable result; a person will review it' }) }}",
"options": {
"responseCode": 202
}
},
"id": "50000000-0000-4000-8000-000000000020",
"name": "Respond Needs Human",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
2860,
520
]
}
],
"connections": {
"Ticket Webhook": {
"main": [
[
{
"node": "Verify HMAC",
"type": "main",
"index": 0
}
]
]
},
"Verify HMAC": {
"main": [
[
{
"node": "Auth OK?",
"type": "main",
"index": 0
}
]
]
},
"Auth OK?": {
"main": [
[
{
"node": "Build Ticket",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Unauthorized",
"type": "main",
"index": 0
}
]
]
},
"Build Ticket": {
"main": [
[
{
"node": "Ticket Valid?",
"type": "main",
"index": 0
}
]
]
},
"Ticket Valid?": {
"main": [
[
{
"node": "Triage Agent",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Invalid",
"type": "main",
"index": 0
}
]
]
},
"Triage Agent": {
"main": [
[
{
"node": "Parse Triage",
"type": "main",
"index": 0
}
]
]
},
"Parse Triage": {
"main": [
[
{
"node": "Triage Valid?",
"type": "main",
"index": 0
}
]
]
},
"Triage Valid?": {
"main": [
[
{
"node": "Record Triage",
"type": "main",
"index": 0
}
],
[
{
"node": "Fallback Triage",
"type": "main",
"index": 0
}
]
]
},
"Fallback Triage": {
"main": [
[
{
"node": "Parse Fallback",
"type": "main",
"index": 0
}
],
[
{
"node": "Escalate To Human",
"type": "main",
"index": 0
}
]
]
},
"Parse Fallback": {
"main": [
[
{
"node": "Fallback Valid?",
"type": "main",
"index": 0
}
]
]
},
"Fallback Valid?": {
"main": [
[
{
"node": "Record Triage",
"type": "main",
"index": 0
}
],
[
{
"node": "Escalate To Human",
"type": "main",
"index": 0
}
]
]
},
"Record Triage": {
"main": [
[
{
"node": "Respond Triaged",
"type": "main",
"index": 0
}
]
]
},
"Escalate To Human": {
"main": [
[
{
"node": "Queue For Human",
"type": "main",
"index": 0
}
]
]
},
"Queue For Human": {
"main": [
[
{
"node": "Dead-letter Ticket",
"type": "main",
"index": 0
}
]
]
},
"Dead-letter Ticket": {
"main": [
[
{
"node": "Respond Needs Human",
"type": "main",
"index": 0
}
]
]
},
"Primary Model": {
"ai_languageModel": [
[
{
"node": "Triage Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Fallback Model": {
"ai_languageModel": [
[
{
"node": "Fallback Triage",
"type": "ai_languageModel",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"errorWorkflow": "globalerrhandler",
"timezone": "Australia/Brisbane"
},
"active": false
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
deepSeekApihttpHeaderAuthopenAiApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
03-support-triage. Uses agent, lmChatOpenAi, chainLlm, lmChatDeepSeek. Webhook trigger; 21 nodes.
Source: https://github.com/jarrod-wright/n8n-workflow-hardening/blob/main/03-support-triage/workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.
Bitlab-Chatbot. Uses telegramTrigger, telegram, snowflake, httpRequest. Event-driven trigger; 87 nodes.
my-secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 86 nodes.
Secretaria_IA. Uses postgres, httpRequest, openAi, agent. Webhook trigger; 60 nodes.
My workflow 3. Uses agent, memoryManager, memoryBufferWindow, postgres. Webhook trigger; 40 nodes.