AutomationFlowsAI & RAG › Automate Monthly Merchant Risk Reviews with Supabase, Perplexity and Claude

Automate Monthly Merchant Risk Reviews with Supabase, Perplexity and Claude

ByRyan Nolan @ryanandmattdatascience on n8n.io

This consists 3 different workflows. Each one should be saved into individual workflows.

Cron / scheduled trigger★★★★★ complexityAI-powered69 nodesSupabasePerplexityAgentAnthropic ChatOutput Parser StructuredHTTP Request@Apify/N8N Nodes Apify@Tavily/N8N Nodes Tavily
AI & RAG Trigger: Cron / scheduled Nodes: 69 Complexity: ★★★★★ AI nodes: yes Added:

This workflow corresponds to n8n.io template #12472 — we link there as the canonical source.

This workflow follows the Agent → Documentdefaultdataloader recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "qVcggoinRQjDDDO3",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true,
    "templateCredsSetupCompleted": true
  },
  "name": "Risk n8n Automation (All 3 Worklows) v2",
  "tags": [],
  "nodes": [
    {
      "id": "99aad377-8351-4ffd-a86f-b5f39d084789",
      "name": "Monthly on the 1st",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -432,
        624
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "months",
              "triggerAtHour": 6
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f1ebcd71-dcb8-4283-9a03-066ee21ea22c",
      "name": "Initialize Run",
      "type": "n8n-nodes-base.set",
      "position": [
        -224,
        624
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "run-month",
              "name": "run_month",
              "type": "string",
              "value": "={{ $now.toFormat(\"yyyy-MM\") }}"
            },
            {
              "id": "run-id",
              "name": "run_id",
              "type": "string",
              "value": "={{ $execution.id }}"
            },
            {
              "id": "triggered-at",
              "name": "triggered_at",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "4ae67652-17e5-479a-8a2d-90ee6f32d6b0",
      "name": "Supabase: Get Active Merchants",
      "type": "n8n-nodes-base.supabase",
      "position": [
        800,
        -192
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "status",
              "keyValue": "active",
              "condition": "eq"
            }
          ]
        },
        "tableId": "merchants",
        "matchType": "allFilters",
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5c44d915-9fa0-46bb-b756-8def8b2f2881",
      "name": "Code: Distinct MCCs + Top Accounts",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        -192
      ],
      "parameters": {
        "jsCode": "const merchants = $input.all().map(i => i.json);\nconst counts = {};\nconst samples = {};\nfor (const m of merchants) {\n  if (!m.mcc_code) continue;\n  counts[m.mcc_code] = (counts[m.mcc_code] || 0) + 1;\n  samples[m.mcc_code] = samples[m.mcc_code] || [];\n  if (samples[m.mcc_code].length < 5) samples[m.mcc_code].push(m.name);\n}\nreturn Object.entries(counts).map(([mcc, count]) => ({\n  mcc,\n  account_count: count,\n  top_accounts: samples[mcc]\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "0001a998-675a-4500-be7b-0f35d881b3b3",
      "name": "Merge: Merchants + MCC Codes",
      "type": "n8n-nodes-base.merge",
      "position": [
        1312,
        -96
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "advanced": true,
        "mergeByFields": {
          "values": [
            {
              "field1": "mcc",
              "field2": "code"
            }
          ]
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "ad5a0ba0-98bf-415a-8594-6ed9c715b52f",
      "name": "Supabase: Get MCC Codes",
      "type": "n8n-nodes-base.supabase",
      "position": [
        960,
        0
      ],
      "parameters": {
        "tableId": "mcc_codes",
        "matchType": "allFilters",
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d4f675e5-271a-41b5-accd-9c6beb30eaaf",
      "name": "Perplexity: MCC News (30d)",
      "type": "n8n-nodes-base.perplexity",
      "position": [
        1600,
        -96
      ],
      "parameters": {
        "query": "=What significant developments happened in the {{ $json.industry }} industry (MCC Code {{ $json.code }})\nin the last 30 days that would matter to a payment-processing risk team?\n\nCover, with specific names, dates, and sources:\n\n1. Federal or state government actions \u2014 regulatory rulings, agency\n   enforcement, new legislation, proposed rules. Specify the agency\n   (DOT, FAA, FTC, CFPB, SEC, FDA, DEA, etc.).\n\n2. Industry health \u2014 bankruptcies, Chapter 11 filings, major company\n   distress, consolidation, mass layoffs of meaningful players.\n\n3. Demand-side trends \u2014 consumer behavior shifts, declining usage,\n   pricing pressure, macro factors affecting this industry.\n\n4. Card-brand actions or chargeback monitoring program changes\n   (Visa VDMP, Mastercard ECP) for this MCC.\n\n5. Material litigation against the industry or key players.\n\nBe specific and source everything. If there's no news in a category,\nsay so explicitly.",
        "options": {
          "searchAfterDate": "={{ $now.minus({ days: 30 }).toFormat(\"MM/dd/yyyy\") }}"
        },
        "resource": "search",
        "requestOptions": {}
      },
      "credentials": {
        "perplexityApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "fa7a0f20-86ab-40d5-aaa2-b0f1aa022cd2",
      "name": "AI Agent: MCC Risk Rating",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1824,
        -96
      ],
      "parameters": {
        "text": "=You are a senior risk analyst at Sentinel Payments evaluating the {{ $('Merge: Merchants + MCC Codes').item.json.industry }} industry (MCC {{ $('Merge: Merchants + MCC Codes').item.json.code }}) for this month's risk review.\n\nVisa's baseline risk rating for this MCC: {{ $('Merge: Merchants + MCC Codes').item.json.mcc_risk_score }} out of 5.\n\nRecent industry research:\n{{ $json.results.map(r => '[' + r.date + '] ' + r.title + '\\nSource: ' + r.url + '\\n' + r.snippet).join('\\n\\n---\\n\\n') }}\n\nYour task: assess the current risk for this industry based on the research above. Consider both the baseline MCC risk AND the direction the industry is moving right now.\n\nProduce:\n- rating: 1 (very low) to 5 (very high)\n- direction: improving, stable, or deteriorating\n- summary: a TWO-SENTENCE summary, maximum 400 characters total, of what is moving and why Sentinel should care\n- key_flags: exactly 3 flags, each with category (govt_action, industry_health, demand, card_brand, litigation), a one-line headline (maximum 110 characters - this is rendered as a single bullet on a slide), and a source\n- rec_action: monitor, document_request, reserve_review, or escalate\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nSLIDE OUTPUT CONSTRAINTS (HARD LIMITS - the Risk Committee deck has fixed slide real estate)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n- summary: \u2264 400 characters, ending with a period. Two short sentences.\n- key_flags[].headline: \u2264 110 characters, one line, no trailing ellipsis.\n- Exactly 3 key flags - no more, no fewer.\n\nBe specific. Tie findings to the research provided. Do not invent details. Write tight - every word must earn its place.",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3.1
    },
    {
      "id": "4a2cfe9f-6f0b-4d2c-8186-c4d375fe3a44",
      "name": "Claude: MCC Agent",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        1824,
        128
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "97b426d4-e1b0-473c-8de1-8bcc1bba9b65",
      "name": "Parser: MCC Snapshot",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1984,
        112
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"rating\": 3,\n  \"direction\": \"stable\",\n  \"summary\": \"Two sentences max. \u2264 400 chars total. Covers what is moving and why Sentinel should care.\",\n  \"key_flags\": [\n    {\n      \"category\": \"govt_action\",\n      \"headline\": \"One-line description, \u2264 110 characters - reads cleanly on a slide bullet\",\n      \"source\": \"URL or publication name\"\n    }\n  ],\n  \"rec_action\": \"monitor\"\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "5485ab1e-5147-40f0-bbb4-3d25b6e53595",
      "name": "Supabase: Upsert MCC Snapshot",
      "type": "n8n-nodes-base.supabase",
      "position": [
        2304,
        -96
      ],
      "parameters": {
        "tableId": "mcc_risk_snapshots",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "month",
              "fieldValue": "={{ $now.toFormat(\"yyyy-MM\") }}"
            },
            {
              "fieldId": "mcc",
              "fieldValue": "={{ $('Merge: Merchants + MCC Codes').item.json.code }}"
            },
            {
              "fieldId": "account_count",
              "fieldValue": "={{ $('Merge: Merchants + MCC Codes').item.json.account_count }}"
            },
            {
              "fieldId": "risk_rating",
              "fieldValue": "={{ $json.output.rating }}"
            },
            {
              "fieldId": "news_summary",
              "fieldValue": "={{ $json.output.summary }}"
            },
            {
              "fieldId": "key_flags",
              "fieldValue": "={{ JSON.stringify($json.output.key_flags) }}"
            },
            {
              "fieldId": "top_accounts",
              "fieldValue": "={{ JSON.stringify($('Merge: Merchants + MCC Codes').item.json.top_accounts) }}"
            },
            {
              "fieldId": "direction",
              "fieldValue": "={{ $json.output.direction }}"
            },
            {
              "fieldId": "rec_action",
              "fieldValue": "={{ $json.output.rec_action }}"
            }
          ]
        }
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "8e5c8d36-28be-4a00-9d3d-dbd205c6947f",
      "name": "Aggregate MCC Snapshots",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2704,
        -96
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "mcc_snapshots"
      },
      "typeVersion": 1
    },
    {
      "id": "b0e00ad5-05c3-438e-a882-c8db609cde98",
      "name": "Merge: All Branches",
      "type": "n8n-nodes-base.merge",
      "position": [
        3552,
        944
      ],
      "parameters": {
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "93ba5c34-97b9-4579-8ac7-52cfd25ec606",
      "name": "Supabase: Get High-Risk Merchants",
      "type": "n8n-nodes-base.supabase",
      "position": [
        448,
        976
      ],
      "parameters": {
        "tableId": "monthly_metrics",
        "operation": "getAll",
        "returnAll": true,
        "filterType": "string",
        "filterString": "=month=eq.{{ $now.toFormat(\"yyyy-MM\") }}&or=(cb_rate.gte.0.009,refund_rate.gte.0.10,exposure_score.gte.4)"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7eb85560-2418-4b02-994c-947a232c00e1",
      "name": "OpenSanctions: Company Screen",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        976,
        416
      ],
      "parameters": {
        "url": "https://api.opensanctions.org/search/default",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ '\"' + $json.name + '\"' }}"
            },
            {
              "name": "limit",
              "value": "5"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "c3243dd7-2dd5-4058-bedd-e0c34f4fffa9",
      "name": "Merge: Merchant Findings",
      "type": "n8n-nodes-base.merge",
      "position": [
        1456,
        864
      ],
      "parameters": {
        "numberInputs": 7
      },
      "typeVersion": 3.2
    },
    {
      "id": "55c73934-c490-4db0-a8ad-783ed6459e28",
      "name": "OpenSanctions: Owner Screen",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        592
      ],
      "parameters": {
        "url": "https://api.opensanctions.org/search/default",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ '\"' + $json.owner_name + '\"' }}"
            },
            {
              "name": "limit",
              "value": "5"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "3a81263d-9e4d-4a93-8446-d15f439bfa1f",
      "name": "CourtListener: Company Cases",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        784
      ],
      "parameters": {
        "url": "https://www.courtlistener.com/api/rest/v4/search/",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 4,
              "batchInterval": 65000
            }
          },
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ '\"' + $json.name + '\"' }}"
            },
            {
              "name": "type",
              "value": "o"
            },
            {
              "name": "filed_after",
              "value": "={{ $now.minus({ days: 30 }).toFormat(\"yyyy-MM-dd\") }}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "ecd6b08a-e57e-4068-b803-c97b22d70f95",
      "name": "CourtListener: Owner Cases",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        976
      ],
      "parameters": {
        "url": "https://www.courtlistener.com/api/rest/v4/search/",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 4,
              "batchInterval": 65000
            }
          },
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ '\"' + $json.owner_name + '\"' }}"
            },
            {
              "name": "type",
              "value": "o"
            },
            {
              "name": "filed_after",
              "value": "={{ $now.minus({ days: 30 }).toFormat(\"yyyy-MM-dd\") }}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "701c2aaa-5bb8-48fd-9653-d2b501e7b478",
      "name": "Apify: Google Reviews",
      "type": "@apify/n8n-nodes-apify.apify",
      "position": [
        960,
        1168
      ],
      "parameters": {
        "actorId": {
          "__rl": true,
          "mode": "url",
          "value": "https://console.apify.com/actors/nwua9Gu5YrADL7ZDj"
        },
        "timeout": {},
        "operation": "Run actor and get dataset",
        "customBody": "={\n  \"searchStringsArray\": [\"{{ $json.name }}\"],\n  \"maxCrawledPlacesPerSearch\": 1,\n  \"maxReviews\": 50,\n  \"language\": \"en\",\n  \"reviewsSort\": \"newest\",\n  \"reviewsStartDate\": \"{{ $now.minus({ days: 30 }).toFormat('yyyy-MM-dd') }}\"\n}"
      },
      "credentials": {
        "apifyApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "89e21f0b-a9de-47bd-8fc8-72ce9930234a",
      "name": "Tavily: Adverse Media",
      "type": "@tavily/n8n-nodes-tavily.tavily",
      "position": [
        960,
        1360
      ],
      "parameters": {
        "query": "=Research the owner of {{ $json.name }}, whose listed beneficial owner is {{ $json.owner_name }}.\n\nAny negative news for either the name of the business or owner. ",
        "options": {
          "topic": "news",
          "time_range": "month"
        }
      },
      "credentials": {
        "tavilyApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "78fe14cb-b309-4ec2-9f46-4197b10cfcc8",
      "name": "Firecrawl: Scrape Merchant Site",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "position": [
        768,
        1584
      ],
      "parameters": {
        "url": "={{ $json.website }}",
        "operation": "scrape",
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "463d6add-d4fb-4afd-a9eb-eb587ec2ffd9",
      "name": "AI Agent: Classify Website",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        992,
        1584
      ],
      "parameters": {
        "text": "=Analyze this merchant website content. Identify restricted verticals (marijuana, CBD, kratom, GLP-1, gambling, payday lending, firearms, adult content, supplements making unsupported claims). Identify primary business vertical and red flags. Site content (markdown): {{ ($json.data && $json.data.markdown) ? $json.data.markdown.slice(0, 8000) : \"\" }}. Respond as JSON: {\"primary_vertical\": \"...\", \"restricted_verticals_found\": [], \"red_flags\": [], \"notes\": \"...\"}",
        "options": {
          "systemMessage": "You are a payments-risk analyst classifying merchant websites for restricted content. Output only valid JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "e8062a94-7c9d-4e14-9955-09da4aac18e0",
      "name": "Claude: Website Classifier",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        992,
        1776
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "0d883dca-875b-4db0-ad5b-348f65eb9e61",
      "name": "Aggregate Merchant Findings",
      "type": "n8n-nodes-base.code",
      "position": [
        1696,
        944
      ],
      "parameters": {
        "jsCode": "// Loop over each high-risk merchant and stitch parallel branch findings together.\nconst merchants = $('Supabase: Get High-Risk Merchants').all().map(i => i.json);\n\nreturn merchants.map((merchant, idx) => {\n  const ofacByCompany = $('OpenSanctions: Company Screen').all()[idx]?.json    || {};\n  const ofacByOwner   = $('OpenSanctions: Owner Screen').all()[idx]?.json   || {};\n  const courtCompany  = $('CourtListener: Company Cases').all()[idx]?.json || {};\n  const courtOwner    = $('CourtListener: Owner Cases').all()[idx]?.json   || {};\n  const apifyRaw      = $('Apify: Google Reviews').all()[idx]?.json  || {};\n  const adverseRaw    = $('Tavily: Adverse Media').all()[idx]?.json                         || {};\n  const firecrawl     = $('Firecrawl: Scrape Merchant Site').all()[idx]?.json || {};\n  const classify      = $('AI Agent: Classify Website').all()[idx]?.json?.output             || {};\n\n  const allOfacResults = [\n    ...(ofacByCompany.results || []).map(r => ({ ...r, queried_for: 'company' })),\n    ...(ofacByOwner.results   || []).map(r => ({ ...r, queried_for: 'owner'   }))\n  ];\n\n  const place = apifyRaw.title\n    ? apifyRaw\n    : (Array.isArray(apifyRaw) ? apifyRaw[0] : {}) || {};\n  const allReviews = place.reviews || [];\n  const lowReviews = allReviews.filter(r => (r.stars || 5) <= 2);\n\n  return {\n    merchant_id: merchant.merchant_id,\n    name: merchant.name,\n    owner_name: merchant.owner_name,\n    mcc: merchant.mcc,\n    website: merchant.website,\n    metrics: {\n      cb_rate: merchant.cb_rate,\n      refund_rate: merchant.refund_rate,\n      exposure_score: merchant.exposure_score\n    },\n    ofac_pep_hits: allOfacResults.map(r => ({\n      name: r.caption,\n      datasets: r.datasets,\n      score: r.score,\n      queried_for: r.queried_for\n    })),\n    site_classification: classify,\n    litigation_company: {\n      count: courtCompany.count || 0,\n      cases: (courtCompany.results || []).slice(0, 5).map(r => ({\n        caseName: r.caseName, court: r.court, dateFiled: r.dateFiled\n      }))\n    },\n    litigation_owner: {\n      count: courtOwner.count || 0,\n      cases: (courtOwner.results || []).slice(0, 5).map(r => ({\n        caseName: r.caseName, court: r.court, dateFiled: r.dateFiled\n      }))\n    },\n    google_reviews: {\n      place_name: place.title,\n      address: place.address,\n      rating: place.totalScore,\n      review_count: place.reviewsCount,\n      negative_reviews: lowReviews.slice(0, 5).map(r => ({\n        stars: r.stars, text: r.text, date: r.publishedAtDate\n      }))\n    },\n    adverse_media: (adverseRaw.results || []).map(r => ({\n      title: r.title, snippet: r.snippet, url: r.url, date: r.date\n    })),\n    findings_summary:\n      `Merchant: ${merchant.name} (owner ${merchant.owner_name || 'unknown'}). ` +\n      `MCC ${merchant.mcc}. CB rate ${merchant.cb_rate}. ` +\n      `Risk score ${merchant.exposure_score}. ` +\n      `Site vertical: ${classify.primary_vertical || 'unknown'}. ` +\n      `Restricted verticals found: ${(classify.restricted_verticals_found || []).join(', ') || 'none'}. ` +\n      `OFAC/PEP hits: ${allOfacResults.length} (company query: ${(ofacByCompany.results || []).length}, owner query: ${(ofacByOwner.results || []).length}). ` +\n      `Litigation (company): ${courtCompany.count || 0}. ` +\n      `Litigation (owner): ${courtOwner.count || 0}. ` +\n      `Adverse media hits: ${(adverseRaw.results || []).length}.`\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "1ff17484-d8e6-4264-a055-33cb287953bd",
      "name": "AI Agent: Risk Recommendation",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2064,
        944
      ],
      "parameters": {
        "text": "=You are a senior risk analyst at Sentinel Payments \u2014 a mid-market US payment processor \u2014 evaluating a merchant in our portfolio for the monthly risk review. Your job is to recommend ONE action for this merchant, grounded in Sentinel policy and our history of prior Risk Committee decisions. You recommend; the Risk Committee decides.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nMERCHANT UNDER REVIEW\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nMerchant: {{ $json.name }} (ID {{ $json.merchant_id }})\nOwner: {{ $json.owner_name }}\nMCC: {{ $json.mcc }}\nCurrent month metrics:\n  - Chargeback rate: {{ $json.metrics.cb_rate }}\n  - Refund rate: {{ $json.metrics.refund_rate }}\n  - Internal risk score: {{ $json.metrics.exposure_score }} / 5\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nEVIDENCE GATHERED THIS MONTH (from external sources)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nOFAC / PEP screening (OpenSanctions):\n{{ JSON.stringify($json.ofac_pep_hits) }}\n\nWebsite content classification (Firecrawl + Claude):\n  - Primary vertical: {{ $json.site_classification.primary_vertical }}\n  - Restricted verticals found: {{ JSON.stringify($json.site_classification.restricted_verticals_found) }}\n  - Red flags: {{ JSON.stringify($json.site_classification.red_flags) }}\n  - Notes: {{ $json.site_classification.notes }}\n\nLitigation searches (CourtListener):\n  - By company name: {{ $json.litigation_company.count }} cases \u2014 {{ JSON.stringify($json.litigation_company.cases) }}\n  - By owner name: {{ $json.litigation_owner.count }} cases \u2014 {{ JSON.stringify($json.litigation_owner.cases) }}\n\nPublic reviews (Google via Apify):\n  - Rating: {{ $json.google_reviews.rating }} ({{ $json.google_reviews.review_count }} reviews)\n  - Negative review sample: {{ JSON.stringify($json.google_reviews.negative_reviews) }}\n\nAdverse media (Tavily \u2014 negative news on business + owner):\n{{ JSON.stringify($json.adverse_media) }}\n\nFindings summary: {{ $json.findings_summary }}\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nSENTINEL RISK POLICY (these ARE the rules \u2014 apply them)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\u25b8 RESERVE POLICY BY MCC TIER\n  Base rolling reserve by MCC risk tier (1-5):\n    Tier 1 (very low):  no reserve required if CB rate < 0.3%\n    Tier 2 (low):       3% rolling reserve, 30-day\n    Tier 3 (moderate):  6% rolling reserve, 60-day\n    Tier 4 (elevated): 10% rolling reserve, 90-day, monthly Risk review\n    Tier 5 (high):     15% reserve floor, case-by-case ceiling, quarterly Risk Committee review\n\n\u25b8 CHARGEBACK THRESHOLD PLAYBOOK\n  Two consecutive months at these bands trigger action:\n    < 0.3%        \u2014 no action\n    0.3%-0.5%     \u2014 informational monitoring; flag for next-month review\n    0.5%-0.7%     \u2014 formal documentation request via Risk Analyst; reserve review\n    0.7%-0.9%     \u2014 mandatory reserve increase per MCC tier; consider rolling_hold_30d\n    0.9%-1.0%     \u2014 Visa VDMP threshold reached; rolling_hold_30d + reserve at MCC tier ceiling; Risk Analyst review\n    \u2265 1.0%        \u2014 escalate to Risk Committee for offboarding decision\n\n\u25b8 REFUND RATE MONITORING STANDARDS\n  Standard bands (two consecutive months):\n    < 2%             \u2014 normal; no action\n    2%-4%            \u2014 informational monitoring\n    4%-6%            \u2014 formal documentation request via Risk Analyst\n    6%-8%            \u2014 reserve review + service-quality investigation\n    8%-12%           \u2014 escalate to Risk Analyst for service-quality / fraud investigation\n    > 12% sustained  \u2014 escalate to Risk Committee for offboarding decision\n    > 15% any single month \u2014 immediate investigation + processing hold pending Risk Committee review\n\n  Vertical-specific refund modifier:\n    For MCC 5968 (Continuity / Subscription Merchants) and MCC 5499 (Misc Food Stores\n    \u2014 supplements, wellness, specialty), refund-rate thresholds shift up by 3 percentage\n    points before triggering action.\n\n\u25b8 RESTRICTED BUSINESS TYPES POLICY (STRICT LIABILITY)\n  Sentinel will NOT process for: marijuana (THC), kratom, unlicensed GLP-1 prescribing,\n  unlicensed payday lending, FFL-noncompliant firearms, supplements making unsupported\n  FDA claims, illegal gambling, non-consent adult content. CBD is permitted ONLY under\n  MCC 5912 with proper pharmacy licensing \u2014 never under MCC 5462, 5499, 5814, or any\n  non-pharmacy MCC.\n\n  CRITICAL: Restricted product found via site classification under a non-matching MCC\n  is grounds for IMMEDIATE escalate_offboarding regardless of other metrics.\n\n\u25b8 BENEFICIAL OWNER RE-SCREENING POLICY (\u00a74.2)\n  All beneficial owners are re-screened monthly against OFAC SDN, PEP lists, and\n  Sentinel's internal offboarded-entity registry. ANY name match against a BO who\n  appears in a prior Sentinel offboarding decision triggers MANDATORY escalation.\n\n\u25b8 OFAC / PEP HIT HANDLING\n  Positive OFAC SDN match (exact or fuzzy confidence \u22650.85) = immediate processing\n  hold + escalate to Sentinel Compliance.\n\n\u25b8 WEBSITE VERTICAL DRIFT POLICY\n  Drift into restricted territory = treated as misclassification and grounds for\n  escalate_offboarding per Restricted Business Types Policy.\n\n\u25b8 LITIGATION DISCLOSURE POLICY\n  Material pending litigation involving fraud, consumer protection violations, or\n  breach of fiduciary duty triggers documentation request via Risk Analyst.\n\n\u25b8 ACTION RECOMMENDATION FRAMEWORK\n  Recommend exactly ONE action per merchant per cycle:\n    no_action            \u2014 monitor only\n    request_docs         \u2014 formal request for updated processing volumes\n    raise_reserve        \u2014 increase rolling reserve % per MCC tier ceiling\n    rolling_hold_30d     \u2014 apply 30-day rolling settlement hold\n    escalate_offboarding \u2014 refer to Risk Committee for offboarding decision\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nPRIOR DECISIONS TOOL (Pinecone)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nUse the Pinecone tool to retrieve Sentinel's prior Risk Committee decisions on\nmerchants with comparable characteristics.\n\nTOOL CALL DISCIPLINE:\n- Make at most TWO Pinecone queries. After two queries, you MUST stop calling\n  tools and produce your final JSON answer.\n- Do not output tool-call syntax as part of your text response.\n- Your final response must be valid JSON matching the schema.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nTASK\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nRecommend ONE action. Your rationale MUST cite specific evidence findings,\nspecific Sentinel policies by name, and specific prior Risk Committee decisions.\n\nTwo HARD ESCALATION triggers \u2014 these override everything else:\n  1. Restricted product found via site classification under a non-matching MCC.\n  2. Beneficial owner name appears in a prior Sentinel offboarding decision.\n\nIn all other cases, be calibrated \u2014 not reflexively conservative.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nSLIDE OUTPUT CONSTRAINTS (HARD LIMITS - the Risk Committee deck has fixed slide real estate)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nThese constraints are NON-NEGOTIABLE. Write tight or your output gets truncated downstream.\n\n- primary_drivers:   EXACTLY 3 items, each \u2264 120 characters. Short bullet phrases. No trailing periods.\n- policy_citations:  EXACTLY 3 items, each \u2264 120 characters. Format: 'Policy name - one-line reason invoked'.\n- proposed_actions:  EXACTLY 3 items, each \u2264 120 characters. Imperative voice ('Place rolling 30-day hold').\n- prior_precedent.case_summary: 2-3 short sentences, \u2264 300 characters TOTAL. Name the prior merchant + outcome.\n- prior_precedent.outcome:      Single phrase, \u2264 140 characters.\n- rationale:         3-4 sentences, \u2264 500 characters TOTAL. End with a period. Plain English for the Risk Committee deck.\n\nPick the 3 STRONGEST drivers / policies / actions. Don't pad lists. Don't restate the same point twice. The Risk Committee will read this on a slide \u2014 make every word count.",
        "options": {
          "maxIterations": 10
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3.1
    },
    {
      "id": "af0084bd-f929-4e3e-944c-652f76b342f3",
      "name": "Claude: Recommendation Agent",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        1936,
        1168
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "03a3910b-62aa-497e-af52-9061efa72e6b",
      "name": "Pinecone: Prior Risk Decisions",
      "type": "@n8n/n8n-nodes-langchain.vectorStorePinecone",
      "position": [
        2080,
        1216
      ],
      "parameters": {
        "mode": "retrieve-as-tool",
        "options": {},
        "pineconeIndex": {
          "__rl": true,
          "mode": "list",
          "value": "n8nrisk",
          "cachedResultName": "n8nrisk"
        },
        "toolDescription": "Retrieves prior Sentinel Risk Committee decisions for analogous merchant situations.\nCall AT MOST TWICE per merchant: once with the company name + risk type, optionally\nonce with the owner name + risk type. After your second call (or first if sufficient),\nyou MUST call format_final_json_response immediately. Do not call this tool more than\ntwice under any circumstances."
      },
      "credentials": {
        "pineconeApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "aa4a365f-6cba-44a8-b558-21140cace97d",
      "name": "OpenAI: Pinecone Embeddings",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "position": [
        2080,
        1392
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "3fa5c986-08fd-402c-b4f7-dda5dcbb8c64",
      "name": "Parser: Recommended Action",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        2400,
        1248
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"merchant_id\": { \"type\": \"string\", \"description\": \"Sentinel merchant ID, e.g. M00042\" },\n    \"merchant_name\": { \"type\": \"string\" },\n    \"mcc\": { \"type\": \"string\" },\n    \"owner_name\": { \"type\": \"string\" },\n    \"recommendation\": {\n      \"type\": \"string\",\n      \"enum\": [\"no_action\", \"request_docs\", \"raise_reserve\", \"rolling_hold_30d\", \"escalate_offboarding\"],\n      \"description\": \"Single recommended action per Sentinel Risk policy\"\n    },\n    \"severity\": { \"type\": \"string\", \"enum\": [\"low\", \"medium\", \"high\", \"critical\"] },\n    \"primary_drivers\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\", \"maxLength\": 120 },\n      \"minItems\": 3,\n      \"maxItems\": 3,\n      \"description\": \"EXACTLY 3 short bullet phrases. Each \u2264 120 characters. No trailing periods. Each must read as one clean slide bullet.\"\n    },\n    \"policy_citations\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\", \"maxLength\": 120 },\n      \"minItems\": 3,\n      \"maxItems\": 3,\n      \"description\": \"EXACTLY 3 specific Sentinel policies invoked. Each \u2264 120 characters. Format: 'Policy name - one-line reason invoked'.\"\n    },\n    \"prior_precedent\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"found\": { \"type\": \"boolean\" },\n        \"case_summary\": { \"type\": \"string\", \"maxLength\": 300, \"description\": \"2-3 sentences, \u2264 300 chars. Names the prior merchant + outcome.\" },\n        \"outcome\": { \"type\": \"string\", \"maxLength\": 140, \"description\": \"Single phrase \u2264 140 chars\" }\n      },\n      \"required\": [\"found\", \"case_summary\", \"outcome\"]\n    },\n    \"proposed_actions\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\", \"maxLength\": 120 },\n      \"minItems\": 3,\n      \"maxItems\": 3,\n      \"description\": \"EXACTLY 3 concrete next steps. Each \u2264 120 characters. Imperative voice ('Place rolling 30-day hold').\"\n    },\n    \"reserve_change_bps\": { \"type\": \"integer\" },\n    \"confidence\": { \"type\": \"string\", \"enum\": [\"low\", \"medium\", \"high\"] },\n    \"rationale\": {\n      \"type\": \"string\",\n      \"maxLength\": 500,\n      \"description\": \"3-4 sentence narrative explaining the recommendation. MAXIMUM 500 CHARACTERS. End with a period. Plain English for the Risk Committee deck.\"\n    }\n  },\n  \"required\": [\"merchant_id\", \"merchant_name\", \"mcc\", \"owner_name\", \"recommendation\", \"severity\", \"primary_drivers\", \"policy_citations\", \"prior_precedent\", \"proposed_actions\", \"reserve_change_bps\", \"confidence\", \"rationale\"],\n  \"additionalProperties\": false\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "7c2e5862-9775-4483-a20c-d4af8bf7b239",
      "name": "Supabase: Insert recommended_action",
      "type": "n8n-nodes-base.supabase",
      "position": [
        2560,
        944
      ],
      "parameters": {
        "tableId": "recommended_actions",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "month",
              "fieldValue": "={{ $now.toFormat(\"yyyy-MM\") }}"
            },
            {
              "fieldId": "merchant_id",
              "fieldValue": "={{ $json.output.merchant_id }}"
            },
            {
              "fieldId": "merchant_name",
              "fieldValue": "={{ $json.output.merchant_name }}"
            },
            {
              "fieldId": "mcc",
              "fieldValue": "={{ $json.output.mcc }}"
            },
            {
              "fieldId": "owner_name",
              "fieldValue": "={{ $json.output.owner_name }}"
            },
            {
              "fieldId": "recommendation",
              "fieldValue": "={{ $json.output.recommendation }}"
            },
            {
              "fieldId": "severity",
              "fieldValue": "={{ $json.output.severity }}"
            },
            {
              "fieldId": "primary_drivers",
              "fieldValue": "={{ $json.output.primary_drivers }}"
            },
            {
              "fieldId": "policy_citations",
              "fieldValue": "={{ $json.output.policy_citations }}"
            },
            {
              "fieldId": "prior_precedent",
              "fieldValue": "={{ $json.output.prior_precedent }}"
            },
            {
              "fieldId": "proposed_actions",
              "fieldValue": "={{ $json.output.proposed_actions }}"
            },
            {
              "fieldId": "reserve_change_bps",
              "fieldValue": "={{ $json.output.reserve_change_bps }}"
            },
            {
              "fieldId": "confidence",
              "fieldValue": "={{ $json.output.confidence }}"
            },
            {
              "fieldId": "rationale",
              "fieldValue": "={{ $json.output.rationale }}"
            }
          ]
        }
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7d2c1c08-7ad7-4366-bcb3-0fd24ab6d59d",
      "name": "Aggregate Account Findings",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2848,
        944
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "recommended_actions"
      },
      "typeVersion": 1
    },
    {
      "id": "ea7799e6-221d-4e4d-96a7-65ba4d582ecd",
      "name": "Supabase: Portfolio Metrics (12 mo)",
      "type": "n8n-nodes-base.supabase",
      "position": [
        576,
        2704
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "month",
              "keyValue": "={{ $now.minus({ months: 12 }).toFormat(\"yyyy-MM\") }}",
              "condition": "gte"
            }
          ]
        },
        "tableId": "monthly_metrics",
        "matchType": "allFilters",
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9f492136-82d0-4ac8-8a55-afc4e673f555",
      "name": "Compute Portfolio Trends",
      "type": "n8n-nodes-base.code",
      "position": [
        992,
        2704
      ],
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst byMonth = {};\nfor (const r of rows) {\n  const m = r.month;\n  if (!byMonth[m]) byMonth[m] = { vol: 0, cbCount: 0, refCount: 0, revCount: 0, merchantVol: 0 };\n  byMonth[m].vol += (r.processing_vol || 0);\n  byMonth[m].cbCount += ((r.cb_rate || 0) * (r.processing_vol || 0));\n  byMonth[m].refCount += ((r.refund_rate || 0) * (r.processing_vol || 0));\n  byMonth[m].revCount += ((r.reversal_rate || 0) * (r.processing_vol || 0));\n  byMonth[m].merchantVol += (r.processing_vol || 0);\n}\nconst series = Object.entries(byMonth).sort(([a],[b]) => a.localeCompare(b)).map(([m, v]) => ({\n  month: m,\n  total_processing_vol: v.vol,\n  weighted_cb_rate: v.merchantVol > 0 ? v.cbCount / v.merchantVol : 0,\n  weighted_refund_rate: v.merchantVol > 0 ? v.refCount / v.merchantVol : 0,\n  weighted_reversal_rate: v.merchantVol > 0 ? v.revCount / v.merchantVol : 0\n}));\nconst last = series[series.length - 1] || {};\nconst prev = series[series.length - 2] || {};\nreturn [{\n  series,\n  current: last,\n  prior: prev,\n  deltas: {\n    cb_rate: (last.weighted_cb_rate || 0) - (prev.weighted_cb_rate || 0),\n    refund_rate: (last.weighted_refund_rate || 0) - (prev.weighted_refund_rate || 0),\n    reversal_rate: (last.weighted_reversal_rate || 0) - (prev.weighted_reversal_rate || 0),\n    processing_vol: (last.total_processing_vol || 0) - (prev.total_processing_vol || 0)\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "dc84d833-48a7-48ba-a365-312da6c21d4f",
      "name": "AI Agent: Portfolio Narrative",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1328,
        2704
      ],
      "parameters": {
        "text": "=Write a portfolio-level risk narrative for the Sentinel Payments monthly risk review, maximum 500 characters total. 2-3 short paragraphs. 12-month series, current vs prior month, deltas: {{ JSON.stringify($json) }}. Speak in internal risk-analyst voice. Identify what is moving and why it matters. Hard limit: 500 characters.",
        "options": {
          "systemMessage": "You are a senior risk analyst at Sentinel Payments writing for the internal risk committee. Be candid, dense, operational. Plain prose. HARD LIMIT: 500 characters total."
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "42bb4660-3c01-447a-b113-0220be263f58",
      "name": "Claude: Portfolio Agent",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        1328,
        2944
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "19380699-6407-4b13-af5b-d12a006e45a3",
      "name": "Supabase: Insert portfolio_snapshot",
      "type": "n8n-nodes-base.supabase",
      "position": [
        1680,
        2704
      ],
      "parameters": {
        "tableId": "portfolio_snapshots",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "month",
              "fieldValue": "={{ $now.toFormat(\"yyyy-MM\") }}"
            },
            {
              "fieldId": "series",
              "fieldValue": "={{ JSON.stringify($('Compute Portfolio Trends').item.json.series) }}"
            },
            {
              "fieldId": "current",
              "fieldValue": "={{ JSON.stringify($('Compute Portfolio Trends').item.json.current) }}"
            },
            {
              "fieldId": "deltas",
              "fieldValue": "={{ JSON.stringify($('Compute Portfolio Trends').item.json.deltas) }}"
            },
            {
              "fieldId": "narrative",
              "fieldValue": "={{ $json.output }}"
            }
          ]
        }
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "e9dd409e-e879-4e38-8810-1ccb4a7dbbbc",
      "name": "Build Monthly Manifest",
      "type": "n8n-nodes-base.code",
      "position": [
        3776,
        960
      ],
      "parameters": {
        "jsCode": "const mccAgg       = $('Aggregate MCC Snapshots').first()?.json || {};\nconst accountAgg   = $('Aggregate Account Findings').first()?.json || {};\nconst portfolio    = $('Supabase: Insert portfolio_snapshot').first()?.json || {};\n\nconst mccSnapshots       = mccAgg.mcc_snapshots || [];\nconst recommendedActions = accountAgg.recommended_actions || [];\n\nreturn [{\n  run_month:                $now.toFormat(\"yyyy-MM\"),\n  run_id:                   $execution.id,\n  triggered_at:             $now.toISO(),\n  generated_at:             new Date().toISOString(),\n  status:                   'complete',\n  mcc_snapshot_count:       mccSnapshots.length,\n  recommended_action_count: recommendedActions.length,\n  portfolio_snapshot_id:    portfolio.id\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "082c9968-22f0-4fab-9b2c-61a4baa0f688",
      "name": "Supabase: Insert monthly_manifest",
      "type": "n8n-nodes-base.supabase",
      "position": [
        4000,
        960
      ],
      "parameters": {
        "tableId": "monthly_manifest",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "run_month",
              "fieldValue": "={{ $json.run_month }}"
            },
            {
              "fieldId": "run_id",
              "fieldValue": "={{ $json.run_id }}"
            },
            {
              "fieldId": "triggered_at",
              "fieldValue": "={{ $json.triggered_at }}"
            },
            {
              "fieldId": "generated_at",
              "fieldValue": "={{ $json.generated_at }}"
            },
            {
              "fieldId": "status",
              "fieldValue": "={{ $json.status }}"
            },
            {
              "fieldId": "mcc_snapshot_count",
              "fieldValue": "={{ $json.mcc_snapshot_count }}"
            },
            {
              "fieldId": "recommended_action_count",
              "fieldValue": "={{ $json.recommended_action_count }}"
            },
            {
              "fieldId": "portfolio_snapshot_id",
              "fieldValue": "={{ $json.portfolio_snapshot_id }}"
            }
          ]
        }
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ade5dfc7-3d5a-4db9-8325-9116d1d6876e",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        128,
        -1280
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "874526e6-f789-46e5-b8bd-749e3fdbbdf2",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1488,
        592
      ],
      "parameters": {
        "width": 976,
        "height": 192,
        "content": "# Part 2 Main Workflow\n\nIn this section of the workflow, we will get the updates on industry MCC codes, updates on the high risk or watch accounts, and get the data from our portfolio. We want to format this data and save it back into the DB\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "4acaa071-e471-4939-9801-ddccda0b8275",
      "name": "Pinecone: Insert Prior Decisions",
      "type": "@n8n/n8n-nodes-langchain.vectorStorePinecone",
      "position": [
        608,
        -1280
      ],
      "parameters": {
        "mode": "insert",
        "options": {
          "clearNamespace": true
        },
        "pineconeIndex": {
          "__rl": true,
          "mode": "list",
          "value": "n8nrisk",
          "cachedResultName": "n8nrisk"
        },
        "embeddingBatchSize": 50
      },
      "credentials": {
        "pineconeApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "e625f792-bd67-46ee-80df-79a1139199b7",
      "name": "OpenAI Embeddings (text-embedding-3-small)",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "position": [
        528,
        -1040
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "457a7d51-991b-4d34-a24b-e998f33499d1",
      "name": "Prior Decision Document Loader",
      "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
      "position": [
        752,
        -1056
      ],
      "parameters": {
        "options": {
          "metadata": {
            "metadataValues": [
              {
                "name": "decision_id",
                "value": "={{ $json.id }}"
              },
              {
                "name": "decision_date",
                "value": "={{ $json.decision_date }}"
              },
              {
                "name": "merchant_name",
                "value": "={{ $json.merchant_name }}"
              },
              {
                "name": "merchant_id_internal",
                "value": "={{ $json.merchant_id_internal }}"
              },
              {
                "name": "mcc",
                "value": "={{ $json.mcc }}"
              },
              {
                "name": "action_taken",
                "value": "={{ $json.action_taken }}"
              },
              {
                "name": "bo_names",
                "value": "={{ $json.bo_names }}"
              }
            ]
          }
        },
        "jsonData": "=Date: {{ $json.decision_date }}\nMerchant: {{ $json.merchant_name }} (internal ID: {{ $json.merchant_id_internal }})\nMCC: {{ $json.mcc }} ({{ $json.mcc_industry }})\nBeneficial Owners: {{ $json.bo_names }}\nMetric Profile: {{ $json.metric_profile }}\nSignals Gathered: {{ $json.signals_gathered }}\nAction Taken: {{ $json.action_taken }}\nRationale: {{ $json.rationale }}\nOutcome: {{ $json.outcome }}",
        "jsonMode": "expressionData",
        "textSplittingMode": "custom"
      },
      "typeVersion": 1.1
    },
    {
      "id": "972fa3d2-6f5d-4d48-9333-b55c17214d5c",
      "name": "Character Text Splitter",
      "type": "@n8n/n8n-nodes-langchain.textSplitterCharacterTextSplitter",
      "position": [
        768,
        -880
      ],
      "parameters": {
        "chunkSize": 2000
      },
      "typeVersion": 1
    },
    {
      "id": "557dee26-6859-4013-9bc9-76ee8c0c5eaa",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        320,
        -1408
      ],
      "parameters": {
        "height": 96,
        "content": "Fake Example Decisions based on merchant risk are stored in here"
      },
      "typeVersion": 1
    },
    {
      "id": "e4273ca9-d02c-462a-b165-a7fc72ec8343",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -1312
      ],
      "parameters": {
        "width": 480,
        "content": "# Part 1 Simple RAG Ingestion\nEach past decision is stored as a chunk in the Vector DB\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c611457a-616a-4169-8ced-d8a58709f694",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        4096
      ],
      "parameters": {
        "width": 592,
        "content": "# Part 3 Cowork Workflow\nThis workflow will run from cowork, grab the exact data we need, and send the data back to cowork to build the slide deck"
      },
      "typeVersion": 1
    },
    {
      "id": "b9122b63-33e4-4e6f-b940-ddbb41c7e36e",
      "name": "Get Monthly Data Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        304,
        4128
      ],
      "parameters": {
        "path": "sentinel-risk-monthly-data",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "50560461-e6ee-4a19-8e92-dcf453b0ddc1",
      "name": "Resolve Month",
      "type": "n8n-nodes-base.set",
      "position": [
        560,
        4128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "month",
              "name": "month",
              "type": "string",
              "value": "={{ ($json.query && $json.query.month) ? $json.query.month : $now.toFormat(\"yyyy-MM\") }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "3ea3204a-4e4e-4680-a146-ea37e25dfc7e",
      "name": "Supabase: monthly_manifest",
      "type": "n8n-nodes-base.supabase",
      "position": [
        928,
        3728
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "run_month",
              "keyValue": "={{ $json.month }}",
              "condition": "eq"
            }
          ]
        },
        "tableId": "monthly_manifest",
        "matchType": "allFilters",
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This consists 3 different workflows. Each one should be saved into individual workflows.

Source: https://n8n.io/workflows/12472/ — 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

WooriFisa. Uses agent, httpRequest, documentDefaultDataLoader, vectorStorePinecone. Scheduled trigger; 86 nodes.

Agent, HTTP Request, Document Default Data Loader +14
AI & RAG

Search Worflow Docker Complete. Uses documentDefaultDataLoader, textSplitterCharacterTextSplitter, vectorStoreSupabase, embeddingsOllama. Scheduled trigger; 71 nodes.

Document Default Data Loader, Text Splitter Character Text Splitter, Supabase Vector Store +14
AI & RAG

WooriFisa 최종. Uses memoryMongoDbChat, agent, httpRequest, documentDefaultDataLoader. Scheduled trigger; 68 nodes.

Memory Mongo Db Chat, Agent, HTTP Request +14
AI & RAG

crawl4 ai. Uses documentDefaultDataLoader, textSplitterCharacterTextSplitter, vectorStoreSupabase, embeddingsOllama. Scheduled trigger; 65 nodes.

Document Default Data Loader, Text Splitter Character Text Splitter, Supabase Vector Store +12
AI & RAG

Search Worflow Docker. Uses documentDefaultDataLoader, textSplitterCharacterTextSplitter, vectorStoreSupabase, embeddingsOllama. Scheduled trigger; 65 nodes.

Document Default Data Loader, Text Splitter Character Text Splitter, Supabase Vector Store +12