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": "aml-alert-triage-v1",
"name": "AML Alert Triage",
"active": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "aml-alert",
"responseMode": "responseNode",
"options": {}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "AML Alert Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-2220,
0
]
},
{
"parameters": {
"jsCode": "const checked = $input.first().json;\nconst body = checked.body ?? {};\nconst transaction = body.transaction ?? body;\nreturn [{\n json: {\n transaction,\n request_id: String(checked.request_id ?? body.request_id ?? `n8n-${$execution.id}`),\n workflow_execution_id: $execution.id\n }\n}];"
},
"id": "00000000-0000-4000-8000-000000000002",
"name": "Normalize Webhook",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1560,
0
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/score' }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.transaction) }}",
"options": {
"timeout": 10000
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
}
},
"id": "00000000-0000-4000-8000-000000000003",
"name": "Score Transaction",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-1340,
0
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"onError": "continueRegularOutput"
},
{
"parameters": {
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/entity/' + encodeURIComponent($('Normalize Webhook').first().json.transaction.entity_id) + '/context' }}",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "as_of",
"value": "={{ $('Normalize Webhook').first().json.transaction.timestamp }}"
}
]
},
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
}
},
"id": "00000000-0000-4000-8000-000000000034",
"name": "Get Entity Context",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-1000,
100
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst status = Number(raw.statusCode ?? (body?.entity_id ? 200 : 0));\nconst success = status >= 200 && status < 300 && body && typeof body === 'object' && !body.error;\nreturn [{ json: { context: body, context_success: success, http_status: status || null, context_error: success ? null : (body?.error?.message ?? raw?.error?.message ?? 'Entity context could not be retrieved') } }];"
},
"id": "00000000-0000-4000-8000-000000000035",
"name": "Inspect Entity Context",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-840,
100
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000361",
"leftValue": "={{ $json.context_success }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000036",
"name": "Context Available?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-680,
100
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => ({ json: { ...item.json, technical_reason: item.json.context_error ?? 'entity_context_unavailable' } }));"
},
"id": "00000000-0000-4000-8000-000000000037",
"name": "Mark Context Failure",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-420,
380
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000041",
"leftValue": "={{ $json.score }}",
"rightValue": 40,
"operator": {
"type": "number",
"operation": "lt"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "below_40"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000042",
"leftValue": "={{ $json.score }}",
"rightValue": 40,
"operator": {
"type": "number",
"operation": "gte"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "at_or_above_40"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "00000000-0000-4000-8000-000000000004",
"name": "Score Threshold (40)",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-1120,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000051",
"leftValue": "={{ $json.rule_auto_close_eligible }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000005",
"name": "Rule Auto-Close Eligible?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-880,
-240
]
},
{
"parameters": {
"jsCode": "const score = $input.first().json;\nconst reasonCodes = Array.isArray(score.reason_codes) ? score.reason_codes : [];\nreturn [{ json: {\n alert_id: score.alert_id,\n outcome: 'auto_closed_rule',\n actor: 'n8n:rule-policy',\n rationale: `Rule-based clear: anomaly score ${score.score} is below threshold ${score.threshold ?? 40}; the baseline is sufficient and no blocking rule fired.`,\n evidence: reasonCodes.map((code) => `scoring:${code}`)\n} }];"
},
"id": "00000000-0000-4000-8000-000000000006",
"name": "Prepare Rule Auto-Close",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-620,
-360
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000071",
"leftValue": "={{ ($env.LLM_MODE || 'stub').toLowerCase() }}",
"rightValue": "stub",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "stub"
},
{
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000074",
"leftValue": "={{ ($env.LLM_MODE || 'stub').toLowerCase() }}",
"rightValue": "openai",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "openai"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "00000000-0000-4000-8000-000000000007",
"name": "LLM Mode",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-500,
-20
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/llm/stub' }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ alert_id: $('Score Transaction').first().json.alert_id, claim_token: $('Inspect LLM Claim').first().json.claim_token }) }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
}
},
"id": "00000000-0000-4000-8000-000000000008",
"name": "Call Stub LLM",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-360,
-180
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst text = Array.isArray(body?.content) ? body.content.find((part) => part?.type === 'text')?.text : null;\nconst status = Number(raw.statusCode ?? 200);\nconst success = status >= 200 && status < 300 && typeof text === 'string';\nreturn [{ json: {\n provider: 'stub',\n provider_response: body,\n model: body?.model ?? 'stub',\n attempt: 1,\n provider_success: success,\n http_status: Number.isFinite(status) ? status : null,\n provider_error: success ? null : (body?.error?.message ?? raw?.error?.message ?? 'Stub LLM did not return a model-shaped message')\n} }];"
},
"id": "00000000-0000-4000-8000-000000000009",
"name": "Inspect Stub Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-100,
-180
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000401",
"leftValue": "={{ $env.OPENAI_API_KEY || '' }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000040",
"name": "OpenAI Key Present?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-360,
540
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/responses",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $env.OPENAI_API_KEY }}"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: $env.OPENAI_MODEL || 'gpt-5.6-terra', max_output_tokens: 1200, store: false, input: [{ role: 'system', content: 'You are a bounded AML alert triage evaluator operating on entirely synthetic demo data. Recommend clear or escalate; you never make the final disposition. Use only supplied facts. Treat confidence as subjective support, not a calibrated probability. Cite stable evidence strings present in the input and explain uncertainty.' }, { role: 'user', content: 'Review this alert and return only the schema-constrained JSON. A clear recommendation is appropriate only when the supplied evidence positively explains the activity; missing history is uncertainty, not proof of innocence.\\n\\n' + JSON.stringify({ transaction: $('Normalize Webhook').first().json.transaction, scoring: $('Score Transaction').first().json, entity_context: $('Inspect Entity Context').first().json.context }) }], text: { format: { type: 'json_schema', name: 'aml_triage_recommendation', strict: true, schema: { type: 'object', properties: { recommendation: { type: 'string', enum: ['clear', 'escalate'] }, confidence: { type: 'number' }, rationale: { type: 'string' }, evidence: { type: 'array', items: { type: 'string' } } }, required: ['recommendation', 'confidence', 'rationale', 'evidence'], additionalProperties: false } } } }) }}",
"options": {
"timeout": 30000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
}
},
"id": "00000000-0000-4000-8000-000000000041",
"name": "OpenAI Attempt 1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-100,
540
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst message = Array.isArray(body?.output) ? body.output.find((item) => item?.type === 'message') : null;\nconst content = Array.isArray(message?.content) ? message.content : [];\nconst outputText = content.find((part) => part?.type === 'output_text')?.text;\nconst refusal = content.find((part) => part?.type === 'refusal');\nconst status = Number(raw.statusCode ?? (body?.object === 'response' ? 200 : 0));\nconst success = status >= 200 && status < 300 && body?.status === 'completed' && typeof outputText === 'string' && !refusal;\nconst retryable = !success && (status === 0 || status === 408 || status === 409 || status === 425 || status === 429 || status >= 500);\nconst providerError = success ? null : (body?.error?.message ?? raw?.error?.message ?? (refusal ? 'OpenAI response was a refusal' : body?.status === 'incomplete' ? `OpenAI response was incomplete: ${body?.incomplete_details?.reason ?? 'unknown reason'}` : 'OpenAI attempt 1 failed'));\nreturn [{ json: { provider: 'openai', provider_response: body, model: body?.model ?? null, attempt: 1, reservation_id: $('Inspect OpenAI Reservation 1').first().json.reservation_id, provider_success: success, provider_retryable: retryable, http_status: status || null, provider_error: providerError } }];"
},
"id": "00000000-0000-4000-8000-000000000042",
"name": "Inspect OpenAI Attempt 1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
540
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000431",
"leftValue": "={{ $json.provider_success }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000043",
"name": "OpenAI Attempt 1 Succeeded?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
420,
540
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000441",
"leftValue": "={{ $json.provider_retryable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000044",
"name": "OpenAI Attempt 1 Retryable?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
660,
600
]
},
{
"parameters": {
"amount": 2,
"unit": "seconds"
},
"id": "00000000-0000-4000-8000-000000000045",
"name": "OpenAI Backoff 2 Seconds",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
660,
660
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/responses",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $env.OPENAI_API_KEY }}"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: $env.OPENAI_MODEL || 'gpt-5.6-terra', max_output_tokens: 1200, store: false, input: [{ role: 'system', content: 'You are a bounded AML alert triage evaluator operating on entirely synthetic demo data. Recommend clear or escalate; you never make the final disposition. Use only supplied facts. Treat confidence as subjective support, not a calibrated probability. Cite stable evidence strings present in the input and explain uncertainty.' }, { role: 'user', content: 'Review this alert and return only the schema-constrained JSON. A clear recommendation is appropriate only when the supplied evidence positively explains the activity; missing history is uncertainty, not proof of innocence.\\n\\n' + JSON.stringify({ transaction: $('Normalize Webhook').first().json.transaction, scoring: $('Score Transaction').first().json, entity_context: $('Inspect Entity Context').first().json.context }) }], text: { format: { type: 'json_schema', name: 'aml_triage_recommendation', strict: true, schema: { type: 'object', properties: { recommendation: { type: 'string', enum: ['clear', 'escalate'] }, confidence: { type: 'number' }, rationale: { type: 'string' }, evidence: { type: 'array', items: { type: 'string' } } }, required: ['recommendation', 'confidence', 'rationale', 'evidence'], additionalProperties: false } } } }) }}",
"options": {
"timeout": 30000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
}
},
"id": "00000000-0000-4000-8000-000000000046",
"name": "OpenAI Attempt 2",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
660
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst message = Array.isArray(body?.output) ? body.output.find((item) => item?.type === 'message') : null;\nconst content = Array.isArray(message?.content) ? message.content : [];\nconst outputText = content.find((part) => part?.type === 'output_text')?.text;\nconst refusal = content.find((part) => part?.type === 'refusal');\nconst status = Number(raw.statusCode ?? (body?.object === 'response' ? 200 : 0));\nconst success = status >= 200 && status < 300 && body?.status === 'completed' && typeof outputText === 'string' && !refusal;\nconst retryable = !success && (status === 0 || status === 408 || status === 409 || status === 425 || status === 429 || status >= 500);\nconst providerError = success ? null : (body?.error?.message ?? raw?.error?.message ?? (refusal ? 'OpenAI response was a refusal' : body?.status === 'incomplete' ? `OpenAI response was incomplete: ${body?.incomplete_details?.reason ?? 'unknown reason'}` : 'OpenAI attempt 2 failed'));\nreturn [{ json: { provider: 'openai', provider_response: body, model: body?.model ?? null, attempt: 2, reservation_id: $('Inspect OpenAI Reservation 2').first().json.reservation_id, provider_success: success, provider_retryable: retryable, http_status: status || null, provider_error: providerError } }];"
},
"id": "00000000-0000-4000-8000-000000000047",
"name": "Inspect OpenAI Attempt 2",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1160,
660
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000481",
"leftValue": "={{ $json.provider_success }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000048",
"name": "OpenAI Attempt 2 Succeeded?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1420,
660
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000491",
"leftValue": "={{ $json.provider_retryable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000049",
"name": "OpenAI Attempt 2 Retryable?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1660,
720
]
},
{
"parameters": {
"amount": 4,
"unit": "seconds"
},
"id": "00000000-0000-4000-8000-000000000050",
"name": "OpenAI Backoff 4 Seconds",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
1660,
780
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/responses",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $env.OPENAI_API_KEY }}"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: $env.OPENAI_MODEL || 'gpt-5.6-terra', max_output_tokens: 1200, store: false, input: [{ role: 'system', content: 'You are a bounded AML alert triage evaluator operating on entirely synthetic demo data. Recommend clear or escalate; you never make the final disposition. Use only supplied facts. Treat confidence as subjective support, not a calibrated probability. Cite stable evidence strings present in the input and explain uncertainty.' }, { role: 'user', content: 'Review this alert and return only the schema-constrained JSON. A clear recommendation is appropriate only when the supplied evidence positively explains the activity; missing history is uncertainty, not proof of innocence.\\n\\n' + JSON.stringify({ transaction: $('Normalize Webhook').first().json.transaction, scoring: $('Score Transaction').first().json, entity_context: $('Inspect Entity Context').first().json.context }) }], text: { format: { type: 'json_schema', name: 'aml_triage_recommendation', strict: true, schema: { type: 'object', properties: { recommendation: { type: 'string', enum: ['clear', 'escalate'] }, confidence: { type: 'number' }, rationale: { type: 'string' }, evidence: { type: 'array', items: { type: 'string' } } }, required: ['recommendation', 'confidence', 'rationale', 'evidence'], additionalProperties: false } } } }) }}",
"options": {
"timeout": 30000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
}
},
"id": "00000000-0000-4000-8000-000000000051",
"name": "OpenAI Attempt 3",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1900,
780
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst message = Array.isArray(body?.output) ? body.output.find((item) => item?.type === 'message') : null;\nconst content = Array.isArray(message?.content) ? message.content : [];\nconst outputText = content.find((part) => part?.type === 'output_text')?.text;\nconst refusal = content.find((part) => part?.type === 'refusal');\nconst status = Number(raw.statusCode ?? (body?.object === 'response' ? 200 : 0));\nconst success = status >= 200 && status < 300 && body?.status === 'completed' && typeof outputText === 'string' && !refusal;\nconst providerError = success ? null : (body?.error?.message ?? raw?.error?.message ?? (refusal ? 'OpenAI response was a refusal' : body?.status === 'incomplete' ? `OpenAI response was incomplete: ${body?.incomplete_details?.reason ?? 'unknown reason'}` : 'OpenAI attempt 3 failed'));\nreturn [{ json: { provider: 'openai', provider_response: body, model: body?.model ?? null, attempt: 3, reservation_id: $('Inspect OpenAI Reservation 3').first().json.reservation_id, provider_success: success, http_status: status || null, provider_error: providerError } }];"
},
"id": "00000000-0000-4000-8000-000000000052",
"name": "Inspect OpenAI Attempt 3",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
780
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => ({ json: { ...item.json, technical_reason: 'openai_api_key_missing' } }));"
},
"id": "00000000-0000-4000-8000-000000000053",
"name": "Mark Missing OpenAI Key",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-100,
880
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/llm/validate' }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ alert_id: $('Score Transaction').first().json.alert_id, provider_response: $json.provider_response, provider: $json.provider, model: $json.model || ($json.provider === 'openai' ? ($env.OPENAI_MODEL || 'gpt-5.6-terra') : 'stub'), attempt: $json.attempt || 1, claim_token: $('Inspect LLM Claim').first().json.claim_token, reservation_id: $json.provider === 'openai' ? $json.reservation_id : undefined, input_context: { transaction: $('Normalize Webhook').first().json.transaction, scoring: $('Score Transaction').first().json, entity_context: $('Inspect Entity Context').first().json.context } }) }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
}
},
"id": "00000000-0000-4000-8000-000000000021",
"name": "Validate LLM Output",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2420,
-80
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst status = Number(raw.statusCode ?? (typeof body?.valid === 'boolean' ? 200 : 0));\nconst transportOk = status >= 200 && status < 300 && typeof body?.valid === 'boolean';\nconst autoCloseEligible = transportOk && body.valid === true && body.route === 'auto_close' && body.eligibility?.eligible === true;\nreturn [{ json: {\n ...body,\n validation_transport_ok: transportOk,\n auto_close_eligible: autoCloseEligible,\n validation_error: body?.validation_error ?? (transportOk ? null : (body?.error?.message ?? raw?.error?.message ?? 'Validation service failed'))\n} }];"
},
"id": "00000000-0000-4000-8000-000000000022",
"name": "Normalize Validation Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2680,
-80
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000231",
"leftValue": "={{ $json.auto_close_eligible }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000023",
"name": "Deterministic Eligibility Gate",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2940,
-80
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000241",
"leftValue": "={{ $json.validation_transport_ok === true && $json.valid === true }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000024",
"name": "Valid But Ambiguous?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3180,
60
]
},
{
"parameters": {
"jsCode": "const validated = $input.first().json;\nconst decision = validated.decision ?? {};\nreturn [{ json: {\n alert_id: $('Score Transaction').first().json.alert_id,\n outcome: 'auto_closed_llm',\n actor: 'n8n:deterministic-eligibility-policy',\n rationale: decision.rationale,\n confidence: decision.confidence,\n evidence: Array.isArray(decision.evidence) ? decision.evidence : [],\n claim_token: $('Inspect LLM Claim').first().json.claim_token\n} }];"
},
"id": "00000000-0000-4000-8000-000000000025",
"name": "Prepare LLM Auto-Close",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3180,
-180
]
},
{
"parameters": {
"jsCode": "const validated = $input.first().json;\nconst decision = validated.decision ?? {};\nconst checks = validated.eligibility?.checks ?? {};\nconst failedChecks = Object.entries(checks).filter(([, passed]) => passed !== true).map(([name]) => name);\nconst gateReason = failedChecks.join(', ') || 'review requested';\nconst modelRationale = decision.rationale ?? 'The LLM returned a valid response but did not support an automated clear.';\nreturn [{ json: {\n alert_id: $('Score Transaction').first().json.alert_id,\n outcome: 'human_review',\n actor: 'n8n:deterministic-eligibility-policy',\n rationale: `${modelRationale} Deterministic routing requires human review because: ${gateReason}.`,\n confidence: typeof decision.confidence === 'number' ? decision.confidence : undefined,\n evidence: Array.isArray(decision.evidence) ? decision.evidence : [],\n claim_token: $('Inspect LLM Claim').first().json.claim_token\n} }];"
},
"id": "00000000-0000-4000-8000-000000000026",
"name": "Prepare Human Review",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3420,
0
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => ({ json: { ...item.json, technical_reason: 'scoring_failed_or_invalid_response' } }));"
},
"id": "00000000-0000-4000-8000-000000000027",
"name": "Mark Scoring Failure",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-860,
300
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => ({ json: { ...item.json, technical_reason: 'unsupported_llm_mode' } }));"
},
"id": "00000000-0000-4000-8000-000000000028",
"name": "Mark LLM Mode Error",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-360,
300
]
},
{
"parameters": {
"jsCode": "const current = $input.first().json;\nlet score = {};\nlet claimToken = current.claim_token ?? null;\ntry { score = $('Score Transaction').first().json; } catch (_) {}\ntry {\n const claim = $('Inspect LLM Claim').first().json;\n if (!claimToken && claim.claim_status === 'proceed') claimToken = claim.claim_token;\n} catch (_) {}\nconst reason = current.technical_reason ?? current.validation_error ?? current.provider_error ?? 'provider_or_validation_failure';\nconst decision = {\n alert_id: current.alert_id ?? score.alert_id ?? null,\n outcome: 'technical_review',\n actor: 'n8n:workflow-fail-safe',\n rationale: `Automation failed safely: ${reason}. No auto-close was attempted.`,\n evidence: []\n};\nif (claimToken) decision.claim_token = claimToken;\nreturn [{ json: decision }];"
},
"id": "00000000-0000-4000-8000-000000000030",
"name": "Prepare Technical Review",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3420,
240
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/decisions' }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"responseFormat": "json"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
}
},
"id": "00000000-0000-4000-8000-000000000031",
"name": "Record Decision",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3680,
0
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst status = Number(raw.statusCode ?? (raw.error ? 500 : 200));\nconst ok = status >= 200 && status < 300 && !raw.error;\nreturn [{ json: {\n ok,\n workflow_execution_id: $execution.id,\n request_id: $('Normalize Webhook').first().json.request_id,\n result: ok ? body : null,\n error: ok ? null : (body?.message ?? raw?.error?.message ?? 'Decision could not be persisted'),\n response_status: ok ? 200 : 500\n} }];"
},
"id": "00000000-0000-4000-8000-000000000032",
"name": "Format Webhook Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3940,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": "={{ $json.response_status }}"
}
},
"id": "00000000-0000-4000-8000-000000000033",
"name": "Return Decision",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
4200,
0
]
},
{
"parameters": {
"jsCode": "const envelope = $input.first().json ?? {};\nconst headers = envelope.headers && typeof envelope.headers === 'object' ? envelope.headers : {};\nconst suppliedEntry = Object.entries(headers).find(([name]) => name.toLowerCase() === 'x-demo-webhook-key');\nconst supplied = suppliedEntry ? String(suppliedEntry[1]) : '';\nconst expected = String($env.DEMO_WEBHOOK_KEY || '');\nconst body = envelope.body && typeof envelope.body === 'object' ? envelope.body : {};\nconst requestHeader = Object.entries(headers).find(([name]) => name.toLowerCase() === 'x-request-id');\nreturn [{ json: {\n body,\n request_id: String(body.request_id ?? requestHeader?.[1] ?? `n8n-${$execution.id}`),\n webhook_authorized: expected.length > 0 && supplied === expected,\n workflow_execution_id: $execution.id\n} }];"
},
"id": "00000000-0000-4000-8000-000000000054",
"name": "Check Webhook Key",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2000,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000551",
"leftValue": "={{ $json.webhook_authorized }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000055",
"name": "Webhook Authorized?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-1780,
0
]
},
{
"parameters": {
"jsCode": "const checked = $input.first().json;\nreturn [{ json: {\n ok: false,\n workflow_execution_id: $execution.id,\n request_id: checked.request_id ?? `n8n-${$execution.id}`,\n result: null,\n error: 'Unauthorized webhook request',\n response_status: 401\n} }];"
},
"id": "00000000-0000-4000-8000-000000000056",
"name": "Format Unauthorized Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1560,
220
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/llm/claim' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ alert_id: $('Score Transaction').first().json.alert_id, request_id: $('Normalize Webhook').first().json.request_id }) }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
}
},
"id": "00000000-0000-4000-8000-000000000057",
"name": "Claim LLM Work",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-880,
100
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst statusCode = Number(raw.statusCode ?? (['proceed', 'replay', 'in_progress'].includes(body?.status) ? 200 : 0));\nconst known = ['proceed', 'replay', 'in_progress'].includes(body?.status);\nconst success = statusCode >= 200 && statusCode < 300 && known && body?.alert_id === $('Score Transaction').first().json.alert_id;\nreturn [{ json: {\n ...body,\n claim_status: success ? body.status : 'error',\n claim_success: success,\n http_status: statusCode || null,\n technical_reason: success ? null : (body?.detail ?? body?.error?.message ?? raw?.error?.message ?? 'LLM claim could not be acquired')\n} }];"
},
"id": "00000000-0000-4000-8000-000000000058",
"name": "Inspect LLM Claim",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-660,
100
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000591",
"leftValue": "={{ $json.claim_status }}",
"rightValue": "proceed",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "proceed"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000592",
"leftValue": "={{ $json.claim_status }}",
"rightValue": "replay",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "replay"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000593",
"leftValue": "={{ $json.claim_status }}",
"rightValue": "in_progress",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "in_progress"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "00000000-0000-4000-8000-000000000059",
"name": "Claim Status",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
-420,
100
]
},
{
"parameters": {
"jsCode": "const claim = $input.first().json;\nreturn [{ json: {\n ok: true,\n workflow_execution_id: $execution.id,\n request_id: $('Normalize Webhook').first().json.request_id,\n result: { claim_status: 'replay', alert_id: claim.alert_id, ...(claim.decision ?? {}), idempotent_replay: true },\n error: null,\n response_status: 200\n} }];"
},
"id": "00000000-0000-4000-8000-000000000060",
"name": "Format Claim Replay Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-180,
260
]
},
{
"parameters": {
"jsCode": "const claim = $input.first().json;\nreturn [{ json: {\n ok: false,\n workflow_execution_id: $execution.id,\n request_id: $('Normalize Webhook').first().json.request_id,\n result: { claim_status: 'in_progress', alert_id: claim.alert_id, lease_expires_at: claim.lease_expires_at, retry_after_seconds: claim.retry_after_seconds },\n error: 'Alert triage is already in progress',\n response_status: 202\n} }];"
},
"id": "00000000-0000-4000-8000-000000000061",
"name": "Format Claim In Progress Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-180,
380
]
},
{
"parameters": {
"jsCode": "const claim = $input.first().json;\nreturn [{ json: {\n alert_id: claim.alert_id ?? $('Score Transaction').first().json.alert_id ?? null,\n technical_reason: claim.technical_reason ?? 'Unknown LLM claim status'\n} }];"
},
"id": "00000000-0000-4000-8000-000000000062",
"name": "Mark Claim Failure",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-180,
500
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/llm/live/reservations' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ alert_id: $('Score Transaction').first().json.alert_id, claim_token: $('Inspect LLM Claim').first().json.claim_token, attempt: 1 }) }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
}
},
"id": "00000000-0000-4000-8000-000000000065",
"name": "Reserve OpenAI Attempt 1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-100,
420
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst statusCode = Number(raw.statusCode ?? (body?.status ? 200 : 0));\nconst allowed = statusCode >= 200 && statusCode < 300 && body?.status === 'reserved' && body?.allowed === true && typeof body?.reservation_id === 'string';\nconst denied = statusCode >= 200 && statusCode < 300 && body?.status === 'denied' && body?.allowed === false;\nconst reason = denied ? (body?.reason ?? 'reservation_denied') : (body?.detail ?? body?.error?.message ?? raw?.error?.message ?? 'reservation_service_failure');\nreturn [{ json: {\n reservation_allowed: allowed,\n reservation_id: allowed ? body.reservation_id : null,\n attempt: 1,\n alert_id: body?.alert_id ?? $('Score Transaction').first().json.alert_id,\n technical_reason: allowed ? null : `OpenAI attempt 1 was not authorized: ${reason}`,\n reservation_status: allowed ? 'reserved' : (denied ? 'denied' : 'error'),\n http_status: statusCode || null\n} }];"
},
"id": "00000000-0000-4000-8000-000000000066",
"name": "Inspect OpenAI Reservation 1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
100,
420
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000671",
"leftValue": "={{ $json.reservation_allowed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000067",
"name": "OpenAI Reservation 1 Allowed?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
300,
420
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/llm/live/reservations' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ alert_id: $('Score Transaction').first().json.alert_id, claim_token: $('Inspect LLM Claim').first().json.claim_token, attempt: 2 }) }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
}
},
"id": "00000000-0000-4000-8000-000000000068",
"name": "Reserve OpenAI Attempt 2",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
900,
540
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst statusCode = Number(raw.statusCode ?? (body?.status ? 200 : 0));\nconst allowed = statusCode >= 200 && statusCode < 300 && body?.status === 'reserved' && body?.allowed === true && typeof body?.reservation_id === 'string';\nconst denied = statusCode >= 200 && statusCode < 300 && body?.status === 'denied' && body?.allowed === false;\nconst reason = denied ? (body?.reason ?? 'reservation_denied') : (body?.detail ?? body?.error?.message ?? raw?.error?.message ?? 'reservation_service_failure');\nreturn [{ json: {\n reservation_allowed: allowed,\n reservation_id: allowed ? body.reservation_id : null,\n attempt: 2,\n alert_id: body?.alert_id ?? $('Score Transaction').first().json.alert_id,\n technical_reason: allowed ? null : `OpenAI attempt 2 was not authorized: ${reason}`,\n reservation_status: allowed ? 'reserved' : (denied ? 'denied' : 'error'),\n http_status: statusCode || null\n} }];"
},
"id": "00000000-0000-4000-8000-000000000069",
"name": "Inspect OpenAI Reservation 2",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
540
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000701",
"leftValue": "={{ $json.reservation_allowed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000070",
"name": "OpenAI Reservation 2 Allowed?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1300,
540
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($env.API_BASE_URL || 'http://api:8000').replace(/\\/$/, '') + '/llm/live/reservations' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Triage-API-Key",
"value": "={{ $env.TRIAGE_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ alert_id: $('Score Transaction').first().json.alert_id, claim_token: $('Inspect LLM Claim').first().json.claim_token, attempt: 3 }) }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
}
}
},
"id": "00000000-0000-4000-8000-000000000071",
"name": "Reserve OpenAI Attempt 3",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1900,
660
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst statusCode = Number(raw.statusCode ?? (body?.status ? 200 : 0));\nconst allowed = statusCode >= 200 && statusCode < 300 && body?.status === 'reserved' && body?.allowed === true && typeof body?.reservation_id === 'string';\nconst denied = statusCode >= 200 && statusCode < 300 && body?.status === 'denied' && body?.allowed === false;\nconst reason = denied ? (body?.reason ?? 'reservation_denied') : (body?.detail ?? body?.error?.message ?? raw?.error?.message ?? 'reservation_service_failure');\nreturn [{ json: {\n reservation_allowed: allowed,\n reservation_id: allowed ? body.reservation_id : null,\n attempt: 3,\n alert_id: body?.alert_id ?? $('Score Transaction').first().json.alert_id,\n technical_reason: allowed ? null : `OpenAI attempt 3 was not authorized: ${reason}`,\n reservation_status: allowed ? 'reserved' : (denied ? 'denied' : 'error'),\n http_status: statusCode || null\n} }];"
},
"id": "00000000-0000-4000-8000-000000000072",
"name": "Inspect OpenAI Reservation 3",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2100,
660
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "00000000-0000-4000-8000-000000000731",
"leftValue": "={{ $json.reservation_allowed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "00000000-0000-4000-8000-000000000073",
"name": "OpenAI Reservation 3 Allowed?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2300,
660
]
}
],
"connections": {
"AML Alert Webhook": {
"main": [
[
{
"node": "Check Webhook Key",
"type": "main",
"index": 0
}
]
]
},
"Normalize Webhook": {
"main": [
[
{
"node": "Score Transaction",
"type": "main",
"index": 0
}
]
]
},
"Score Transaction": {
"main": [
[
{
"node": "Score Threshold (40)",
"type": "main",
"index": 0
}
]
]
},
"Score Threshold (40)": {
"main": [
[
{
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
AML Alert Triage. Uses httpRequest. Webhook trigger; 57 nodes.
Source: https://github.com/AmrAdwan/AML_alert_triage_agent/blob/main/n8n/workflows/aml-alert-triage.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.
Jigsaw API key for image processing, I use this as a gatekeeper/second pair of eyes. LINK to their website https://jigsawstack.com/ SECOND A postgress DATABASE (I use Supabase) LlamaCloud for the pars
Onsite Photos to Jobs (SMS Agent). Uses dataTable, twilio, httpRequest, airtable. Webhook trigger; 62 nodes.
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 50 nodes.
W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 48 nodes.
WF-OB-2: Post-Call Handler. Uses httpRequest, slack. Webhook trigger; 48 nodes.