AutomationFlowsAI & RAG › Aml Alert Triage

Aml Alert Triage

AML Alert Triage. Uses httpRequest. Webhook trigger; 57 nodes.

Webhook trigger★★★★★ complexity57 nodesHTTP Request
AI & RAG Trigger: Webhook Nodes: 57 Complexity: ★★★★★ Added:
Aml Alert Triage — n8n workflow card showing HTTP Request integration

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 →

Download .json
{
  "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": [
        [
          {
      
Pro

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 →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

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

HTTP Request, Postgres, Stop And Error +2
AI & RAG

Onsite Photos to Jobs (SMS Agent). Uses dataTable, twilio, httpRequest, airtable. Webhook trigger; 62 nodes.

Data Table, Twilio, HTTP Request +1
AI & RAG

W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 50 nodes.

Postgres, Redis, HTTP Request
AI & RAG

W1 - IN WhatsApp Adapter (Secure + Fast ACK). Uses postgres, redis, httpRequest. Webhook trigger; 48 nodes.

Postgres, Redis, HTTP Request
AI & RAG

WF-OB-2: Post-Call Handler. Uses httpRequest, slack. Webhook trigger; 48 nodes.

HTTP Request, Slack