AutomationFlowsData & Sheets › Complaint Triage Orchestrator (s2.3)

Complaint Triage Orchestrator (s2.3)

Complaint Triage Orchestrator (S2.3). Uses httpRequest, googleSheets. Scheduled trigger; 65 nodes.

Cron / scheduled trigger★★★★★ complexity65 nodesHTTP RequestGoogle Sheets
Data & Sheets Trigger: Cron / scheduled Nodes: 65 Complexity: ★★★★★ Added:
Complaint Triage Orchestrator (s2.3) — n8n workflow card showing HTTP Request, Google Sheets integration

This workflow follows the Google Sheets → HTTP Request 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
{
  "name": "Complaint Triage Orchestrator (S2.3)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "id": "a1e6c2f0-1a2b-4c3d-8e4f-000000000001",
      "name": "Schedule Trigger (15 min)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -400,
        -100
      ]
    },
    {
      "parameters": {},
      "id": "a1e6c2f0-1a2b-4c3d-8e4f-000000000002",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -400,
        100
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Watermark-based fetch (spec Section 5) -- read the last date_received we\n// successfully advanced past, or fall back to an initial lookback window on\n// the very first run.\n//\n// Why a lookback window and not \"since yesterday\": the CFPB Consumer Complaint\n// Database only publishes a complaint once the company has responded or 15\n// calendar days have passed (spec Section 3b, the CFPB 15-day rule). A poll\n// watermarked to \"yesterday\" would mostly return nothing. INITIAL_LOOKBACK_DAYS\n// gives the first run a realistic chance of hits while still behaving like a\n// genuine incremental poller from the second run onward.\nconst INITIAL_LOOKBACK_DAYS = 30;\n\nconst staticData = $getWorkflowStaticData('global');\n\nlet watermarkDate = staticData.lastWatermarkDate;\nlet isFirstRun = false;\n\nif (!watermarkDate) {\n  isFirstRun = true;\n  const fallback = new Date();\n  fallback.setUTCDate(fallback.getUTCDate() - INITIAL_LOOKBACK_DAYS);\n  watermarkDate = fallback.toISOString().slice(0, 10); // YYYY-MM-DD\n}\n\nreturn [\n  {\n    json: {\n      watermarkDate,\n      isFirstRun,\n    },\n  },\n];\n"
      },
      "id": "a1e6c2f0-1a2b-4c3d-8e4f-000000000003",
      "name": "Get Watermark",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -160,
        0
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://www.consumerfinance.gov/data-research/consumer-complaints/search/api/v1/",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "product",
              "value": "Debt collection"
            },
            {
              "name": "product",
              "value": "Credit card"
            },
            {
              "name": "date_received_min",
              "value": "={{ $json.watermarkDate }}"
            },
            {
              "name": "size",
              "value": "25"
            },
            {
              "name": "sort",
              "value": "created_date_asc"
            },
            {
              "name": "no_aggs",
              "value": "true"
            }
          ]
        },
        "options": {}
      },
      "id": "a1e6c2f0-1a2b-4c3d-8e4f-000000000004",
      "name": "CFPB Complaint Search",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        60,
        0
      ],
      "notesInFlow": true,
      "notes": "Pilot scope (spec Section 4): Debt collection + Credit card only. product= repeated as two query params is a real, verified OR filter against the live API (confirmed by build-time testing, not assumed from docs). date_received_min accepts YYYY-MM-DD only -- the live API rejects sub-day timestamps, confirmed by testing during Phase 1 build."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Cap to 25/run (spec Section 5 -- cost predictability, not a rate-limit\n// necessity; size=25 is already set on the request, this is a defensive\n// second cap), flatten each ES hit down to the fields the rest of the\n// pipeline needs (spec Section 3a), and advance the watermark to the latest\n// date_received actually seen in this batch.\n//\n// Watermark precision note: CFPB's date_received_min filter is date-level\n// (YYYY-MM-DD), not timestamp-level -- confirmed live during Phase 1 build\n// (a sub-day timestamp is rejected by the API). That means a same-day poll\n// can re-return complaints already fetched earlier that day. This workflow\n// does not dedup here by design -- spec Section 11 assigns dedup to the\n// storage layer (\"Google Sheets Append-or-Update, dedup via complaint ID\"),\n// wired in a later phase. Downstream nodes must not assume every item here\n// is new.\nconst MAX_PER_RUN = 25;\n\nconst response = $input.first().json;\nconst hits = (response.hits && response.hits.hits) || [];\nconst capped = hits.slice(0, MAX_PER_RUN);\n\nconst staticData = $getWorkflowStaticData('global');\nlet maxDate = staticData.lastWatermarkDate || null;\n\nconst out = capped.map((hit) => {\n  const src = hit._source;\n  const received = (src.date_received || '').slice(0, 10);\n  if (received && (!maxDate || received > maxDate)) {\n    maxDate = received;\n  }\n  return {\n    json: {\n      complaint_id: src.complaint_id,\n      product: src.product,\n      sub_product: src.sub_product,\n      issue: src.issue,\n      sub_issue: src.sub_issue,\n      company: src.company,\n      state: src.state,\n      tags: src.tags,\n      date_received: src.date_received,\n      timely: src.timely,\n      company_response: src.company_response,\n      complaint_what_happened: src.complaint_what_happened,\n    },\n  };\n});\n\nif (maxDate) {\n  staticData.lastWatermarkDate = maxDate;\n}\n\nreturn out;\n"
      },
      "id": "a1e6c2f0-1a2b-4c3d-8e4f-000000000005",
      "name": "Cap Batch & Advance Watermark",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        280,
        0
      ],
      "notesInFlow": true,
      "notes": "End of Phase 1: capped, flattened batch of new-since-watermark tickets."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Synthetic customer/account record generator (spec Section 3c).\n//\n// Structured like a real bank CRM export -- tenure, tier, holdings, balance,\n// contact history -- but every field is synthetic EXCEPT linked_complaint_id\n// (the real CFPB complaint ID) and the two flags below, which carry forward\n// CFPB's own real `tags` field where it exists. That carry-forward is the\n// deliberately-real part of an otherwise-synthetic record (spec Section 12) --\n// never blur it with the rest, and never let this record be mistaken for a\n// real CRM export in any downstream dashboard/report (Section 14).\n//\n// Deterministic, not random-per-run: seeded from complaint_id (mulberry32 PRNG)\n// so re-processing the same ticket -- a retry, a re-run, a dedup pass hitting\n// the same complaint_id twice because of the date-level watermark overlap\n// noted upstream -- always yields the same synthetic record instead of a new\n// random one each time.\n\nfunction mulberry32(seed) {\n  return function () {\n    seed |= 0; seed = (seed + 0x6D2B79F5) | 0;\n    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction seedFromString(str) {\n  let h = 2166136261;\n  for (let i = 0; i < str.length; i++) {\n    h ^= str.charCodeAt(i);\n    h = Math.imul(h, 16777619);\n  }\n  return h >>> 0;\n}\n\nfunction weightedPick(rng, weighted) {\n  const total = weighted.reduce((s, [, w]) => s + w, 0);\n  let r = rng() * total;\n  for (const [value, weight] of weighted) {\n    if (r < weight) return value;\n    r -= weight;\n  }\n  return weighted[weighted.length - 1][0];\n}\n\nconst PRODUCT_POOL = [\n  'Checking Account', 'Savings Account', 'Credit Card', 'Personal Loan',\n  'Auto Loan', 'Mortgage', 'Home Equity Line of Credit', 'Certificate of Deposit',\n];\n\nconst items = $input.all();\n\nconst out = items.map((item) => {\n  const ticket = item.json;\n  const rng = mulberry32(seedFromString(String(ticket.complaint_id)));\n\n  const tenure_years = weightedPick(rng, [[0,3],[1,4],[2,4],[3,4],[4,3],[5,3],[6,2],[7,2],[8,1],[9,1],[10,1],[12,1],[15,1]]);\n  const received = new Date(ticket.date_received);\n  const since = new Date(received);\n  since.setUTCFullYear(since.getUTCFullYear() - tenure_years);\n  since.setUTCMonth(Math.floor(rng() * 12));\n  since.setUTCDate(1 + Math.floor(rng() * 28));\n  const customer_since = since.toISOString().slice(0, 10);\n\n  const account_tier = weightedPick(rng, [['Standard', 7], ['Preferred', 2], ['Premier', 1]]);\n\n  const holdingsCount = weightedPick(rng, [[1, 5], [2, 3], [3, 1], [4, 1]]);\n  const shuffled = [...PRODUCT_POOL].sort(() => rng() - 0.5);\n  const product_holdings = shuffled.slice(0, holdingsCount);\n\n  const outstanding_balance_usd = weightedPick(rng, [[0, 3], [1, 1]]) === 0\n    ? 0\n    : Math.round(rng() * 15000 * 100) / 100;\n\n  const prior_complaints_12mo = weightedPick(rng, [[0, 6], [1, 2], [2, 1], [3, 1]]);\n  const prior_contacts_90d = weightedPick(rng, [[0, 4], [1, 3], [2, 2], [3, 1], [4, 1]]);\n  const preferred_channel = weightedPick(rng, [['Web', 5], ['Phone', 3], ['Mail', 1]]);\n\n  // Real-tag carry-forward (spec Section 3c/12). CFPB's own `tags` field is\n  // real government data, not synthetic. Values seen in the wild: null,\n  // \"Servicemember\", \"Older American\", or \"Older American, Servicemember\".\n  // special_population_flag is the broader of the two -- CFPB itself treats\n  // servicemembers and older Americans together as populations warranting\n  // extra-care handling, so a Servicemember tag sets both flags true.\n  const tags = ticket.tags || '';\n  const servicemember_flag = tags.includes('Servicemember');\n  const special_population_flag = tags.includes('Servicemember') || tags.includes('Older American');\n\n  return {\n    json: {\n      ...ticket,\n      crm: {\n        account_id: `SYN-${seedFromString(String(ticket.complaint_id)).toString(36).toUpperCase()}`,\n        linked_complaint_id: String(ticket.complaint_id),\n        customer_since,\n        tenure_years,\n        account_tier,\n        product_holdings,\n        outstanding_balance_usd,\n        prior_complaints_12mo,\n        prior_contacts_90d,\n        preferred_channel,\n        servicemember_flag,\n        special_population_flag,\n      },\n    },\n  };\n});\n\nreturn out;\n"
      },
      "id": "a1e6c2f0-1a2b-4c3d-8e4f-000000000006",
      "name": "Generate Synthetic CRM Record",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        0
      ],
      "notesInFlow": true,
      "notes": "Phase 2. Adds a synthetic CRM record (spec Section 3c) per ticket under `crm`. Only linked_complaint_id, servicemember_flag, and special_population_flag are real/hybrid -- everything else in `crm` is synthetic (Section 12) and must be labelled as such wherever it surfaces downstream (dashboard, README -- Section 14). End of Phase 2: agents, escalation gate, and storage/dedup are not yet wired."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Fixture test path (spec Section 15 Phase 3): the literal fixture tickets\n// + synthetic CRM records from spec Section 3a/3c, used to test the\n// orchestration, tool-use branching, and escalation gate against known-good\n// data before any real Claude API call exists (Phase 7). This node is its\n// own entry point -- run it directly via n8n's \"Execute step\" -- rather\n// than sitting behind a dedicated trigger node. A real n8n instance\n// silently drops a workflow's second n8n-nodes-base.manualTrigger node\n// (confirmed by live import: 31 of 32 nodes survived, the missing one was\n// a second Manual Trigger); the custom simulator never caught this since\n// it just executes the committed JSON directly and doesn't enforce n8n's\n// own editor-level constraints. Since this node needs no real input\n// (it returns literal fixture data regardless), dropping the redundant\n// trigger and executing it directly is a genuine simplification, not a\n// workaround.\nconst FIXTURE_TICKETS = [\n  {\n    \"complaint_id\": \"9999970\",\n    \"product\": \"Debt collection\",\n    \"sub_product\": \"Other debt\",\n    \"issue\": \"Written notification about debt\",\n    \"sub_issue\": \"Didn't receive notice of right to dispute\",\n    \"company\": \"Aargon Agency, Inc.\",\n    \"state\": \"MI\",\n    \"tags\": \"Servicemember\",\n    \"date_received\": \"2024-09-03T22:24:41.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"Requested debt validation after the 30-day window; says the collector never sent it; disputes dates in the collector's own CFPB reply; mentions an attorney and the FTC\",\n    \"crm\": {\n      \"account_id\": \"SYN-FIXTURE-A\",\n      \"linked_complaint_id\": \"9999970\",\n      \"customer_since\": \"2018-09-03\",\n      \"tenure_years\": 6,\n      \"account_tier\": \"Standard\",\n      \"product_holdings\": [\n        \"Personal Loan\"\n      ],\n      \"outstanding_balance_usd\": 2340,\n      \"prior_complaints_12mo\": 1,\n      \"prior_contacts_90d\": 1,\n      \"preferred_channel\": \"Phone\",\n      \"servicemember_flag\": true,\n      \"special_population_flag\": true\n    }\n  },\n  {\n    \"complaint_id\": \"9999975\",\n    \"product\": \"Debt collection\",\n    \"sub_product\": \"I do not know\",\n    \"issue\": \"Attempts to collect debt not owed\",\n    \"sub_issue\": \"Debt is not yours\",\n    \"company\": \"EQUIFAX, INC.\",\n    \"state\": \"SC\",\n    \"tags\": null,\n    \"date_received\": \"2024-09-03T22:28:25.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"Reviewed credit report and found accounts believed fraudulent, opened without consent\",\n    \"crm\": {\n      \"account_id\": \"SYN-FIXTURE-B\",\n      \"linked_complaint_id\": \"9999975\",\n      \"customer_since\": \"2023-09-03\",\n      \"tenure_years\": 1,\n      \"account_tier\": \"Standard\",\n      \"product_holdings\": [\n        \"Checking Account\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 0,\n      \"prior_contacts_90d\": 0,\n      \"preferred_channel\": \"Web\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  },\n  {\n    \"complaint_id\": \"9999983\",\n    \"product\": \"Credit card\",\n    \"sub_product\": \"General-purpose credit card or charge card\",\n    \"issue\": \"Getting a credit card\",\n    \"sub_issue\": \"Card opened without my consent or knowledge\",\n    \"company\": \"JPMORGAN CHASE & CO.\",\n    \"state\": \"MA\",\n    \"tags\": null,\n    \"date_received\": \"2024-09-03T22:07:34.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"Describes a call about a bank-flagged fraud case, being transferred, then disconnected with no follow-up received\",\n    \"crm\": {\n      \"account_id\": \"SYN-FIXTURE-C\",\n      \"linked_complaint_id\": \"9999983\",\n      \"customer_since\": \"2021-09-03\",\n      \"tenure_years\": 3,\n      \"account_tier\": \"Standard\",\n      \"product_holdings\": [\n        \"Checking Account\",\n        \"Credit Card\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 0,\n      \"prior_contacts_90d\": 0,\n      \"preferred_channel\": \"Web\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  },\n  {\n    \"complaint_id\": \"24158082\",\n    \"product\": \"Debt collection\",\n    \"sub_product\": \"Other debt\",\n    \"issue\": \"Written notification about debt\",\n    \"sub_issue\": \"Didn't receive notice of right to dispute\",\n    \"company\": \"American Profit Recovery, Inc., Marlborough, MA Branch\",\n    \"state\": \"TX\",\n    \"tags\": null,\n    \"date_received\": \"2026-07-14T00:13:58.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"Debt Collection Complaint I am disputing this alleged debt because I do not believe I owe the amount being claimed. I enrolled in a payment plan for my XXXX XXXX (formerly referred to as XXXX or XXXX XXXX XXXX) equipment and made my required monthly payments. My understanding was that the agreement would end after approximately 12 months. When my payment term was complete, a technician came to retrieve the handheld equipment. Before surrendering the equipment, I specifically asked whether my account was paid in full. Although the technician mentioned he was new and could not verify every detail in the system, he proceeded to collect the equipment and provided me with a printed receipt showing a balance of $0.00. To my understanding, the company would not have been able to retrieve the equipment or close out that portion of my account if there had been an outstanding balance. Based on the receipt showing a XXXX balance and the fact that the equipment was accepted and removed from my possession, I reasonably believed my account was fully satisfied. Now, after a significant amount of time has passed, I have learned that a collection account for approximately $550.00 has been reported or is being collected. I was never made aware of this alleged balance because any notices were apparently sent to an address where I have not lived for over three years. As a result, I had no opportunity to address or dispute the alleged debt before it was sent to collections. I respectfully request that this account be investigated. If the creditor claims I owe this balance, I request complete validation of the debt, including: a detailed accounting showing how the alleged balance of approximately $550.00 was calculated, copies of any agreement or contract demonstrating that I remained responsible for this balance, records of all payments made on the account, and documentation explaining why I was issued a receipt showing a $0.00 balance when the equipment was collected. Because I possess documentation showing a XXXX balance at the time the equipment was returned, I dispute the accuracy of this debt and request that the collection activity and any credit reporting be corrected if the debt cannot be properly validated.\",\n    \"crm\": {\n      \"account_id\": \"SYN-1S8MVB7\",\n      \"linked_complaint_id\": \"24158082\",\n      \"customer_since\": \"2016-09-11\",\n      \"tenure_years\": 10,\n      \"account_tier\": \"Standard\",\n      \"product_holdings\": [\n        \"Certificate of Deposit\",\n        \"Auto Loan\",\n        \"Home Equity Line of Credit\",\n        \"Credit Card\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 3,\n      \"prior_contacts_90d\": 1,\n      \"preferred_channel\": \"Web\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  },\n  {\n    \"complaint_id\": \"24157871\",\n    \"product\": \"Debt collection\",\n    \"sub_product\": \"I do not know\",\n    \"issue\": \"Communication tactics\",\n    \"sub_issue\": \"Frequent or repeated calls\",\n    \"company\": \"Collections Acquisition Company, Inc.\",\n    \"state\": \"PA\",\n    \"tags\": null,\n    \"date_received\": \"2026-07-14T00:17:07.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"Called me 9 times and left 8 voicemails within 7 minutes. On the 9th straight call I picked up and told them I work nights and am trying to sleep and to not call me again and they hung up. After researching the number they called from I discovered it is for a debt that I have already paid.\",\n    \"crm\": {\n      \"account_id\": \"SYN-17YTGVQ\",\n      \"linked_complaint_id\": \"24157871\",\n      \"customer_since\": \"2023-10-06\",\n      \"tenure_years\": 3,\n      \"account_tier\": \"Standard\",\n      \"product_holdings\": [\n        \"Certificate of Deposit\",\n        \"Personal Loan\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 1,\n      \"prior_contacts_90d\": 0,\n      \"preferred_channel\": \"Web\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  },\n  {\n    \"complaint_id\": \"24157473\",\n    \"product\": \"Credit card\",\n    \"sub_product\": \"General-purpose credit card or charge card\",\n    \"issue\": \"Fees or interest\",\n    \"sub_issue\": \"Problem with fees\",\n    \"company\": \"U.S. BANCORP\",\n    \"state\": \"MA\",\n    \"tags\": null,\n    \"date_received\": \"2026-07-14T00:02:31.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"On XX/XX/year>, I closed my US Bank XXXX XXXX XXXX XXXX, which has a $400.00 annual fee. As a Massachusetts resident, I requested a pro-rated annual fee refund afforded to me by Massachusetts General Laws Chapter 140, Section 114C. I received a letter (image attached) dated XX/XX/XXXX, stating that they would not be refunding this fee. U.S. Bank is violating Massachusetts General Laws Chapter 140, Section 114C by denying a legally mandated two-thirds prorated annual fee refund upon account closure.\",\n    \"crm\": {\n      \"account_id\": \"SYN-HQZST8\",\n      \"linked_complaint_id\": \"24157473\",\n      \"customer_since\": \"2023-06-20\",\n      \"tenure_years\": 3,\n      \"account_tier\": \"Preferred\",\n      \"product_holdings\": [\n        \"Personal Loan\",\n        \"Home Equity Line of Credit\"\n      ],\n      \"outstanding_balance_usd\": 4545.59,\n      \"prior_complaints_12mo\": 0,\n      \"prior_contacts_90d\": 1,\n      \"preferred_channel\": \"Web\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  },\n  {\n    \"complaint_id\": \"24157200\",\n    \"product\": \"Credit card\",\n    \"sub_product\": \"General-purpose credit card or charge card\",\n    \"issue\": \"Trouble using your card\",\n    \"sub_issue\": \"Credit card company won't increase or decrease your credit limit\",\n    \"company\": \"WELLS FARGO & COMPANY\",\n    \"state\": \"NJ\",\n    \"tags\": null,\n    \"date_received\": \"2026-07-14T00:03:45.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"On XX/XX/year>, Wells Fargo denied my request for a credit limit increase on my credit card account ending XXXX, citing a single reason: unacceptable past credit history. The denial letter contains none of the disclosures required by FCRA (XXXX)(XXXX XXXX. XXXX (XXXX)) when adverse action is based in whole or in part on a consumer report: it does not identify any consumer reporting agency, does not provide the agency's contact information, does not disclose the credit score used or its range and key factors, and does not state my right to obtain a free copy of the report or to dispute its contents.\",\n    \"crm\": {\n      \"account_id\": \"SYN-FBM1I0\",\n      \"linked_complaint_id\": \"24157200\",\n      \"customer_since\": \"2021-04-06\",\n      \"tenure_years\": 5,\n      \"account_tier\": \"Standard\",\n      \"product_holdings\": [\n        \"Auto Loan\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 2,\n      \"prior_contacts_90d\": 2,\n      \"preferred_channel\": \"Phone\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  },\n  {\n    \"complaint_id\": \"24157609\",\n    \"product\": \"Debt collection\",\n    \"sub_product\": \"I do not know\",\n    \"issue\": \"Attempts to collect debt not owed\",\n    \"sub_issue\": \"Debt was result of identity theft\",\n    \"company\": \"CL Holdings LLC\",\n    \"state\": \"TX\",\n    \"tags\": \"Servicemember\",\n    \"date_received\": \"2026-07-14T00:11:22.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with non-monetary relief\",\n    \"complaint_what_happened\": \"\",\n    \"crm\": {\n      \"account_id\": \"SYN-TTYX3Z\",\n      \"linked_complaint_id\": \"24157609\",\n      \"customer_since\": \"2016-03-10\",\n      \"tenure_years\": 10,\n      \"account_tier\": \"Standard\",\n      \"product_holdings\": [\n        \"Credit Card\",\n        \"Home Equity Line of Credit\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 0,\n      \"prior_contacts_90d\": 0,\n      \"preferred_channel\": \"Web\",\n      \"servicemember_flag\": true,\n      \"special_population_flag\": true\n    }\n  },\n  {\n    \"complaint_id\": \"24157195\",\n    \"product\": \"Debt collection\",\n    \"sub_product\": \"I do not know\",\n    \"issue\": \"Attempts to collect debt not owed\",\n    \"sub_issue\": \"Debt is not yours\",\n    \"company\": \"ProCollect, Inc.\",\n    \"state\": \"NM\",\n    \"tags\": null,\n    \"date_received\": \"2026-07-14T00:04:51.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"\",\n    \"crm\": {\n      \"account_id\": \"SYN-TWYBB\",\n      \"linked_complaint_id\": \"24157195\",\n      \"customer_since\": \"2023-06-05\",\n      \"tenure_years\": 3,\n      \"account_tier\": \"Preferred\",\n      \"product_holdings\": [\n        \"Credit Card\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 0,\n      \"prior_contacts_90d\": 1,\n      \"preferred_channel\": \"Web\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  },\n  {\n    \"complaint_id\": \"24157240\",\n    \"product\": \"Debt collection\",\n    \"sub_product\": \"I do not know\",\n    \"issue\": \"Attempts to collect debt not owed\",\n    \"sub_issue\": \"Debt is not yours\",\n    \"company\": \"Security Credit Services, LLC\",\n    \"state\": \"NM\",\n    \"tags\": null,\n    \"date_received\": \"2026-07-14T00:06:48.000Z\",\n    \"timely\": \"Yes\",\n    \"company_response\": \"Closed with explanation\",\n    \"complaint_what_happened\": \"\",\n    \"crm\": {\n      \"account_id\": \"SYN-D441JW\",\n      \"linked_complaint_id\": \"24157240\",\n      \"customer_since\": \"2025-10-14\",\n      \"tenure_years\": 1,\n      \"account_tier\": \"Preferred\",\n      \"product_holdings\": [\n        \"Checking Account\"\n      ],\n      \"outstanding_balance_usd\": 0,\n      \"prior_complaints_12mo\": 0,\n      \"prior_contacts_90d\": 0,\n      \"preferred_channel\": \"Phone\",\n      \"servicemember_flag\": false,\n      \"special_population_flag\": false\n    }\n  }\n];\nreturn FIXTURE_TICKETS.map((t) => ({ json: t }));"
      },
      "id": "b2f7d3a1-0000-4000-8000-000000000002",
      "name": "Load Fixture Tickets",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -400,
        320
      ],
      "notesInFlow": true,
      "notes": "Phase 3 test harness: injects the literal Section 3a/3c Ticket A/B/C fixtures, bypassing the live CFPB fetch and random CRM generation entirely. Its own entry point -- no dedicated trigger node (see the code comment on jsLoadFixtureTickets); run it directly via n8n's \"Execute step\"."
    },
    {
      "parameters": {
        "mode": "append",
        "numberInputs": 2
      },
      "id": "b2f7d3a1-0000-4000-8000-00000000001b",
      "name": "Merge: Fixture or Live Tickets",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.1,
      "position": [
        580,
        60
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "// Both the live pipeline (Phase 1 fetch -> Phase 2 CRM) and the Fixture Test\n// Trigger converge here. Phase 3's mock agents only have known-good fixture\n// data for Tickets A/B/C -- route anything else to a clearly-labelled\n// \"awaiting Phase 7\" dead end instead of letting it fall through into agents\n// that would have to fabricate a result for it.\nconst FIXTURE_IDS = [\"9999970\",\"9999975\",\"9999983\",\"24158082\",\"24157871\",\"24157473\",\"24157200\",\"24157609\",\"24157195\",\"24157240\"];\nconst ticket = $input.item.json;\nreturn { json: { ...ticket, is_fixture_ticket: FIXTURE_IDS.includes(String(ticket.complaint_id)) } };"
      },
      "id": "b2f7d3a1-0000-4000-8000-000000000003",
      "name": "Route: Fixture or Live?",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "b2f7d3a1-0000-4000-8000-000000000004-cond",
              "leftValue": "={{ $json.is_fixture_ticket }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b2f7d3a1-0000-4000-8000-000000000004",
      "name": "IF: Is Fixture Ticket?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        960,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: \"claude-haiku-4-5-20251001\", max_tokens: 1536, system: \"You are Agent 1 (Classification) in a complaint-triage pipeline processing a real CFPB consumer complaint.\\n\\nTask: identify the substantive issue(s) the consumer is actually raising. When the narrative describes more than one issue, rank them by severity and substance -- NOT by how much narrative text each one occupies. (A known failure mode this pipeline was specifically built to avoid: an earlier draft classified a complaint by whichever issue had the most narrative text, a dropped phone call, instead of the issue the consumer had actually filed as substantive, an unauthorised account. Do not repeat that mistake.)\\n\\nDecide whether the taxonomy-lookup tool is needed: set tool_used=true ONLY when the narrative is genuinely ambiguous relative to the ticket's own filed category (its product/issue/sub_issue fields) -- not for clear, clean-match cases.\\n\\nRespond with ONLY this JSON shape, no other text:\\n{\\n  \\\"tool_used\\\": boolean,\\n  \\\"issues\\\": [{ \\\"issue\\\": string, \\\"severity\\\": \\\"Low\\\" | \\\"Medium\\\" | \\\"High\\\", \\\"confidence\\\": number (0-1), \\\"basis\\\": string }],\\n  \\\"primary_issue\\\": string\\n}\\n\\\"issues\\\" must have at least one entry. \\\"primary_issue\\\" must exactly match one entry's \\\"issue\\\" value -- the one you judge most substantive.\", messages: [{ role: \"user\", content: JSON.stringify($json) + \"\\n\\nRespond with ONLY valid JSON matching the schema in the system prompt -- no prose, no markdown code fences.\" }] }) }}",
        "options": {}
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000001",
      "name": "Real Agent 1: Classification",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1180,
        400
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "notes": "Real Claude API call (spec Section 15 Phase 7). Classifies the complaint's substantive issue(s) and decides whether the taxonomy tool is needed -- same contract as the mock Agent 1 node above."
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function parseAnthropicJson(apiResponse) {\r\n  const text = apiResponse.content?.[0]?.text || \"\";\r\n  const stripped = text.trim().replace(/^```(?:json)?\\n?/i, \"\").replace(/\\n?```$/, \"\");\r\n  return JSON.parse(stripped);\r\n}\n\n// n8n's HTTP Request node replaces the item's json with the raw API\n// response -- it does not merge the response with the original input. So\n// the original ticket (complaint_id, product, crm, etc.) must be pulled\n// back from the node that fed Real Agent 1, by name, rather than assumed\n// to still be present on $input.item.json (which is the API response).\nconst apiResponse = $input.item.json;\nconst originalTicket = $('IF: Is Fixture Ticket?').item.json;\nconst parsed = parseAnthropicJson(apiResponse);\nreturn { json: { ...originalTicket, agent1_tool_used: parsed.tool_used, agent1_output: { issues: parsed.issues, primary_issue: parsed.primary_issue } } };"
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000002",
      "name": "Parse: Real Agent 1 Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1300,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c3a8e4b2-0001-4000-8000-000000000003-cond",
              "leftValue": "={{ $json.agent1_tool_used }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000003",
      "name": "IF: Real Agent 1 Tool Used?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1420,
        400
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "// Tool 1 (real): CFPB product/issue/sub-issue taxonomy lookup -- the same\n// cached snapshot as reference_data/taxonomy/cfpb_taxonomy.json (Phase 1).\n// Searches both issue- and sub-issue-level names, since Agent 1's\n// classification may hand back either.\nconst TAXONOMY = {\"_meta\":{\"description\":\"CFPB Consumer Complaint Database product/issue/sub-issue taxonomy, scoped to the S2.3 pilot categories (Debt collection, Credit card). Backs Agent 1 (Classification)'s taxonomy lookup tool -- spec Section 6.\",\"source\":\"https://www.consumerfinance.gov/data-research/consumer-complaints/search/api/v1/ (live aggregations query, no auth required)\",\"retrieved_date\":\"2026-08-12\",\"scope_note\":\"Pulled once at build time per spec Section 6 / Section 15 Phase 1 -- cached static lookup, not re-fetched per ticket.\"},\"products\":[{\"product\":\"Debt collection\",\"total_complaints_in_scope\":1167638,\"sub_products\":[{\"name\":\"I do not know\",\"doc_count\":474297},{\"name\":\"Other debt\",\"doc_count\":195980},{\"name\":\"Credit card debt\",\"doc_count\":177098},{\"name\":\"Medical debt\",\"doc_count\":76913},{\"name\":\"Other (i.e. phone, health club, etc.)\",\"doc_count\":44436},{\"name\":\"Auto debt\",\"doc_count\":32099},{\"name\":\"Telecommunications debt\",\"doc_count\":30154},{\"name\":\"Credit card\",\"doc_count\":28626},{\"name\":\"Rental debt\",\"doc_count\":28622},{\"name\":\"Medical\",\"doc_count\":21143},{\"name\":\"Payday loan debt\",\"doc_count\":16325},{\"name\":\"Mortgage debt\",\"doc_count\":8496},{\"name\":\"Payday loan\",\"doc_count\":7493},{\"name\":\"Private student loan debt\",\"doc_count\":6058},{\"name\":\"Federal student loan debt\",\"doc_count\":5992},{\"name\":\"Mortgage\",\"doc_count\":4807},{\"name\":\"Auto\",\"doc_count\":3748},{\"name\":\"Non-federal student loan\",\"doc_count\":2878},{\"name\":\"Federal student loan\",\"doc_count\":2473}],\"issues\":[{\"name\":\"Attempts to collect debt not owed\",\"doc_count\":469041,\"sub_issues\":[{\"name\":\"Debt is not yours\",\"doc_count\":262452},{\"name\":\"Debt was result of identity theft\",\"doc_count\":139468},{\"name\":\"Debt was paid\",\"doc_count\":54398},{\"name\":\"Debt was already discharged in bankruptcy and is no longer owed\",\"doc_count\":12723}]},{\"name\":\"Written notification about debt\",\"doc_count\":226489,\"sub_issues\":[{\"name\":\"Didn't receive enough information to verify debt\",\"doc_count\":114695},{\"name\":\"Notification didn't disclose it was an attempt to collect a debt\",\"doc_count\":62578},{\"name\":\"Didn't receive notice of right to dispute\",\"doc_count\":49216}]},{\"name\":\"Took or threatened to take negative or legal action\",\"doc_count\":143350,\"sub_issues\":[{\"name\":\"Threatened or suggested your credit would be damaged\",\"doc_count\":111195},{\"name\":\"Threatened to sue you for very old debt\",\"doc_count\":11131},{\"name\":\"Sued you without properly notifying you of lawsuit\",\"doc_count\":7534},{\"name\":\"Seized or attempted to seize your property\",\"doc_count\":5239},{\"name\":\"Threatened to arrest you or take you to jail if you do not pay\",\"doc_count\":3496},{\"name\":\"Collected or attempted to collect exempt funds\",\"doc_count\":3037},{\"name\":\"Sued you in a state where you do not live or did not sign for the debt\",\"doc_count\":1462},{\"name\":\"Threatened to turn you in to immigration or deport you\",\"doc_count\":256}]},{\"name\":\"False statements or representation\",\"doc_count\":118634,\"sub_issues\":[{\"name\":\"Attempted to collect wrong amount\",\"doc_count\":104998},{\"name\":\"Impersonated attorney, law enforcement, or government official\",\"doc_count\":6304},{\"name\":\"Indicated you were committing crime by not paying debt\",\"doc_count\":3456},{\"name\":\"Impersonated an attorney or official\",\"doc_count\":1284},{\"name\":\"Told you not to respond to a lawsuit they filed against you\",\"doc_count\":1235},{\"name\":\"Indicated committed crime not paying\",\"doc_count\":1012},{\"name\":\"Indicated shouldn't respond to lawsuit\",\"doc_count\":345}]},{\"name\":\"Communication tactics\",\"doc_count\":77065,\"sub_issues\":[{\"name\":\"Frequent or repeated calls\",\"doc_count\":42329},{\"name\":\"You told them to stop contacting you, but they keep trying\",\"doc_count\":16978},{\"name\":\"Used obscene, profane, or other abusive language\",\"doc_count\":6111},{\"name\":\"Threatened to take legal action\",\"doc_count\":4669},{\"name\":\"Called before 8am or after 9pm\",\"doc_count\":2374},{\"name\":\"Called after sent written cease of comm\",\"doc_count\":1852},{\"name\":\"Used obscene/profane/abusive language\",\"doc_count\":1832},{\"name\":\"Called outside of 8am-9pm\",\"doc_count\":912},{\"name\":\"Frequent or repeated messages\",\"doc_count\":4},{\"name\":\"Contacted before 8am or after 9pm\",\"doc_count\":3}]},{\"name\":\"Cont'd attempts collect debt not owed\",\"doc_count\":60520,\"sub_issues\":[{\"name\":\"Debt is not mine\",\"doc_count\":36629},{\"name\":\"Debt was paid\",\"doc_count\":16582},{\"name\":\"Debt resulted from identity theft\",\"doc_count\":4899},{\"name\":\"Debt was discharged in bankruptcy\",\"doc_count\":2410}]},{\"name\":\"Disclosure verification of debt\",\"doc_count\":30728,\"sub_issues\":[{\"name\":\"Not given enough info to verify debt\",\"doc_count\":21757},{\"name\":\"Right to dispute notice not received\",\"doc_count\":7174},{\"name\":\"Not disclosed as an attempt to collect\",\"doc_count\":1797}]},{\"name\":\"Threatened to contact someone or share information improperly\",\"doc_count\":13300,\"sub_issues\":[{\"name\":\"Talked to a third-party about your debt\",\"doc_count\":7634},{\"name\":\"Contacted you after you asked them to stop\",\"doc_count\":2985},{\"name\":\"Contacted your employer\",\"doc_count\":2482},{\"name\":\"Contacted you instead of your attorney\",\"doc_count\":199}]},{\"name\":\"Improper contact or sharing of info\",\"doc_count\":10036,\"sub_issues\":[{\"name\":\"Talked to a third party about my debt\",\"doc_count\":5037},{\"name\":\"Contacted me after I asked not to\",\"doc_count\":2567},{\"name\":\"Contacted employer after asked not to\",\"doc_count\":2192},{\"name\":\"Contacted me instead of my attorney\",\"doc_count\":240}]},{\"name\":\"Electronic communications\",\"doc_count\":9682,\"sub_issues\":[{\"name\":\"Frequent or repeated messages\",\"doc_count\":5850},{\"name\":\"You told them to stop contacting you, but they keep trying\",\"doc_count\":2884},{\"name\":\"Contacted before 8am or after 9pm\",\"doc_count\":680},{\"name\":\"Used obscene, profane, or other abusive language\",\"doc_count\":268}]},{\"name\":\"Taking/threatening an illegal action\",\"doc_count\":8793,\"sub_issues\":[{\"name\":\"Threatened to sue on too old debt\",\"doc_count\":2690},{\"name\":\"Threatened arrest/jail if do not pay\",\"doc_count\":2253},{\"name\":\"Sued w/o proper notification of suit\",\"doc_count\":1387},{\"name\":\"Seized/Attempted to seize property\",\"doc_count\":1159},{\"name\":\"Attempted to/Collected exempt funds\",\"doc_count\":903},{\"name\":\"Sued where didn't live/sign for debt\",\"doc_count\":401}]}]},{\"product\":\"Credit card\",\"total_complaints_in_scope\":333515,\"sub_products\":[{\"name\":\"General-purpose credit card or charge card\",\"doc_count\":217513},{\"name\":\"Store credit card\",\"doc_count\":26817}],\"issues\":[{\"name\":\"Problem with a purchase shown on your statement\",\"doc_count\":57864,\"sub_issues\":[{\"name\":\"Credit card company isn't resolving a dispute about a purchase on your statement\",\"doc_count\":43435},{\"name\":\"Card was charged for something you did not purchase with the card\",\"doc_count\":12841},{\"name\":\"Overcharged for something you did purchase with the card\",\"doc_count\":1588}]},{\"name\":\"Incorrect information on your report\",\"doc_count\":31970,\"sub_issues\":[{\"name\":\"Account status incorrect\",\"doc_count\":23892},{\"name\":\"Account information incorrect\",\"doc_count\":3687},{\"name\":\"Information belongs to someone else\",\"doc_count\":3368},{\"name\":\"Old information reappears or never goes away\",\"doc_count\":401},{\"name\":\"Information is missing that should be on the report\",\"doc_count\":234},{\"name\":\"Personal information incorrect\",\"doc_count\":213},{\"name\":\"Public record information inaccurate\",\"doc_count\":175}]},{\"name\":\"Getting a credit card\",\"doc_count\":30200,\"sub_issues\":[{\"name\":\"Card opened without my consent or knowledge\",\"doc_count\":20460},{\"name\":\"Application denied\",\"doc_count\":6209},{\"name\":\"Sent card you never applied for\",\"doc_count\":1428},{\"name\":\"Delay in processing application\",\"doc_count\":1131},{\"name\":\"Problem getting a working replacement card\",\"doc_count\":972}]},{\"name\":\"Problem with a company's investigation into an existing problem\",\"doc_count\":26306,\"sub_issues\":[{\"name\":\"Was not notified of investigation status or results\",\"doc_count\":20284},{\"name\":\"Their investigation did not fix an error on your report\",\"doc_count\":4654},{\"name\":\"Problem with personal statement of dispute\",\"doc_count\":626},{\"name\":\"Difficulty submitting a dispute or getting information about a dispute over the phone\",\"doc_count\":507},{\"name\":\"Investigation took more than 30 days\",\"doc_count\":235}]},{\"name\":\"Other features, terms, or problems\",\"doc_count\":24417,\"sub_issues\":[{\"name\":\"Other problem\",\"doc_count\":12042},{\"name\":\"Problem with rewards from credit card\",\"doc_count\":5515},{\"name\":\"Problem with customer service\",\"doc_count\":2514},{\"name\":\"Problem with balance transfer\",\"doc_count\":1574},{\"name\":\"Privacy issues\",\"doc_count\":1558},{\"name\":\"Add-on products and services\",\"doc_count\":580},{\"name\":\"Credit card company forcing arbitration\",\"doc_count\":309},{\"name\":\"Problem with cash advances\",\"doc_count\":239},{\"name\":\"Problem with convenience check\",\"doc_count\":86}]},{\"name\":\"Fees or interest\",\"doc_count\":21692,\"sub_issues\":[{\"name\":\"Problem with fees\",\"doc_count\":13275},{\"name\":\"Charged too much interest\",\"doc_count\":6273},{\"name\":\"Unexpected increase in interest rate\",\"doc_count\":2144}]},{\"name\":\"Closing your account\",\"doc_count\":15397,\"sub_issues\":[{\"name\":\"Company closed your account\",\"doc_count\":11972},{\"name\":\"Can't close your account\",\"doc_count\":3425}]},{\"name\":\"Billing disputes\",\"doc_count\":15136,\"sub_issues\":[]},{\"name\":\"Problem when making payments\",\"doc_count\":12937,\"sub_issues\":[{\"name\":\"Problem during payment process\",\"doc_count\":11227},{\"name\":\"You never received your bill or did not know a payment was due\",\"doc_count\":1710}]},{\"name\":\"Other\",\"doc_count\":9350,\"sub_issues\":[]},{\"name\":\"Advertising and marketing, including promotional offers\",\"doc_count\":9134,\"sub_issues\":[{\"name\":\"Didn't receive advertised or promotional terms\",\"doc_count\":5112},{\"name\":\"Confusing or misleading advertising about the credit card\",\"doc_count\":4022}]},{\"name\":\"Identity theft / Fraud / Embezzlement\",\"doc_count\":8480,\"sub_issues\":[]},{\"name\":\"Trouble using your card\",\"doc_count\":8174,\"sub_issues\":[{\"name\":\"Can't use card to make purchases\",\"doc_count\":5530},{\"name\":\"Credit card company won't increase or decrease your credit limit\",\"doc_count\":2485},{\"name\":\"Account sold or transferred to another company\",\"doc_count\":159}]},{\"name\":\"Closing/Cancelling account\",\"doc_count\":6389,\"sub_issues\":[]},{\"name\":\"APR or interest rate\",\"doc_count\":5506,\"sub_issues\":[]},{\"name\":\"Late fee\",\"doc_count\":3639,\"sub_issues\":[]},{\"name\":\"Customer service / Customer relations\",\"doc_count\":3504,\"sub_issues\":[]},{\"name\":\"Delinquent account\",\"doc_count\":3218,\"sub_issues\":[]},{\"name\":\"Struggling to pay your bill\",\"doc_count\":3079,\"sub_issues\":[{\"name\":\"Credit card company won't work with you while you're going through financial hardship\",\"doc_count\":2681},{\"name\":\"Problem lowering your monthly payments\",\"doc_count\":248},{\"name\":\"Filed for bankruptcy\",\"doc_count\":150}]},{\"name\":\"Credit determination\",\"doc_count\":3057,\"sub_issues\":[]},{\"name\":\"Advertising and marketing\",\"doc_count\":2926,\"sub_issues\":[]},{\"name\":\"Rewards\",\"doc_count\":2916,\"sub_issues\":[]},{\"name\":\"Credit card protection / Debt protection\",\"doc_count\":2728,\"sub_issues\":[]},{\"name\":\"Transaction issue\",\"doc_count\":2700,\"sub_issues\":[]},{\"name\":\"Billing statement\",\"doc_count\":2619,\"sub_issues\":[]},{\"name\":\"Payoff process\",\"doc_count\":2315,\"sub_issues\":[]},{\"name\":\"Improper use of your report\",\"doc_count\":2263,\"sub_issues\":[{\"name\":\"Reporting company used your report improperly\",\"doc_count\":1379},{\"name\":\"Credit inquiries on your report that you don't recognize\",\"doc_count\":837},{\"name\":\"Received unsolicited financial product or insurance offers after opting out\",\"doc_count\":24},{\"name\":\"Report provided to employer without your written authorization\",\"doc_count\":23}]},{\"name\":\"Other fee\",\"doc_count\":2198,\"sub_issues\":[]},{\"name\":\"Credit line increase/decrease\",\"doc_count\":2185,\"sub_issues\":[]},{\"name\":\"Unsolicited issuance of credit card\",\"doc_count\":1853,\"sub_issues\":[]},{\"name\":\"Credit reporting\",\"doc_count\":1696,\"sub_issues\":[]},{\"name\":\"Balance transfer\",\"doc_count\":1117,\"sub_issues\":[]},{\"name\":\"Collection practices\",\"doc_count\":1001,\"sub_issues\":[]},{\"name\":\"Collection debt dispute\",\"doc_count\":901,\"sub_issues\":[]},{\"name\":\"Forbearance / Workout plans\",\"doc_count\":556,\"sub_issues\":[]},{\"name\":\"Credit monitoring or identity theft protection services\",\"doc_count\":547,\"sub_issues\":[{\"name\":\"Billing dispute for services\",\"doc_count\":245},{\"name\":\"Problem canceling credit monitoring or identify theft protection service\",\"doc_count\":165},{\"name\":\"Didn't receive services that were advertised\",\"doc_count\":63},{\"name\":\"Problem with product or service terms changing\",\"doc_count\":52},{\"name\":\"Received unwanted marketing or advertising\",\"doc_count\":22}]},{\"name\":\"Application processing delay\",\"doc_count\":540,\"sub_issues\":[]},{\"name\":\"Privacy\",\"doc_count\":489,\"sub_issues\":[]},{\"name\":\"Bankruptcy\",\"doc_count\":448,\"sub_issues\":[]},{\"name\":\"Arbitration\",\"doc_count\":348,\"sub_issues\":[]},{\"name\":\"Sale of account\",\"doc_count\":344,\"sub_issues\":[]},{\"name\":\"Cash advance\",\"doc_count\":245,\"sub_issues\":[]},{\"name\":\"Problem with fraud alerts or security freezes\",\"doc_count\":234,\"sub_issues\":[]},{\"name\":\"Balance transfer fee\",\"doc_count\":221,\"sub_issues\":[]},{\"name\":\"Overlimit fee\",\"doc_count\":215,\"sub_issues\":[]},{\"name\":\"Cash advance fee\",\"doc_count\":196,\"sub_issues\":[]},{\"name\":\"Convenience checks\",\"doc_count\":149,\"sub_issues\":[]},{\"name\":\"Unable to get your credit report or credit score\",\"doc_count\":116,\"sub_issues\":[{\"name\":\"Other problem getting your report or credit score\",\"doc_count\":113},{\"name\":\"Problem getting your free annual credit report\",\"doc_count\":3}]}]}]};\n\nfunction taxonomyLookup(taxonomy, product, categoryName) {\r\n  const productEntry = taxonomy.products.find((p) => p.product === product);\r\n  if (!productEntry) return { found: false, note: `Product '${product}' not in cached taxonomy scope.` };\r\n\r\n  const issueEntry = productEntry.issues.find((i) => i.name === categoryName);\r\n  if (issueEntry) {\r\n    return { found: true, level: \"issue\", issue: issueEntry.name, doc_count: issueEntry.doc_count, sibling_sub_issues: issueEntry.sub_issues.map((s) => s.name) };\r\n  }\r\n\r\n  for (const issue of productEntry.issues) {\r\n    const subIssueEntry = issue.sub_issues.find((s) => s.name === categoryName);\r\n    if (subIssueEntry) {\r\n      return {\r\n        found: true, level: \"sub_issue\", parent_issue: issue.name, sub_issue: subIssueEntry.name,\r\n        doc_count: subIssueEntry.doc_count,\r\n        sibling_sub_issues: issue.sub_issues.map((s) => s.name).filter((n) => n !== categoryName),\r\n      };\r\n    }\r\n  }\r\n\r\n  return { found: false, note: `'${categoryName}' not found as an issue or sub-issue under product '${product}'.` };\r\n}\n\nconst ticket = $input.item.json;\nconst category = (ticket.agent1_output && (ticket.agent1_output.primary_issue || ticket.agent1_output.issue)) || ticket.issue;\nconst result = taxonomyLookup(TAXONOMY, ticket.product, category);\nreturn { json: { ...ticket, agent1_tool_result: result } };"
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000004",
      "name": "Tool: Real CFPB Taxonomy Lookup",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        520
      ],
      "notesInFlow": true,
      "notes": "Same real, deterministic taxonomy lookup as the Test path's tool -- reused verbatim, not duplicated logic (see jsTaxonomyTool)."
    },
    {
      "parameters": {
        "mode": "append",
        "numberInputs": 2
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000005",
      "name": "Merge: Pre-Real Agent 2",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.1,
      "position": [
        1740,
        400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: \"claude-haiku-4-5-20251001\", max_tokens: 1536, system: \"You are Agent 2 (Research) in a complaint-triage pipeline. You receive a CFPB complaint ticket plus Agent 1's classification of it.\\n\\nTask: determine which federal regulation, if any, applies to this complaint and why, based on the narrative and the classified issue. Do not guess a citation you're not reasonably confident about -- it is genuinely fine to return null if nothing clearly applies; a downstream deterministic tool independently re-checks your claim against a real cached regulation corpus.\\n\\nDecide whether a broader CRM context pull (tenure, account tier, product holdings, balance, prior-complaint history) is warranted: set broader_crm_lookup_used=true when the customer relationship history seems relevant to responding appropriately (e.g. a repeat complainant, a high-value account, or the issue's nature calls for account context) -- this is discretionary, not automatic.\\n\\nRespond with ONLY this JSON shape, no other text:\\n{\\n  \\\"broader_crm_lookup_used\\\": boolean,\\n  \\\"applicable_regulation\\\": string or null,\\n  \\\"citation\\\": string or null,\\n  \\\"precedent_notes\\\": string\\n}\", messages: [{ role: \"user\", content: JSON.stringify($json) + \"\\n\\nRespond with ONLY valid JSON matching the schema in the system prompt -- no prose, no markdown code fences.\" }] }) }}",
        "options": {}
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000006",
      "name": "Real Agent 2: Research",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1860,
        400
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "notes": "Real Claude API call. Determines the applicable regulation (if any) and whether broader CRM context is warranted."
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "function parseAnthropicJson(apiResponse) {\r\n  const text = apiResponse.content?.[0]?.text || \"\";\r\n  const stripped = text.trim().replace(/^```(?:json)?\\n?/i, \"\").replace(/\\n?```$/, \"\");\r\n  return JSON.parse(stripped);\r\n}\n\n// See Parse: Real Agent 1 Response for why originalTicket is pulled from\n// the upstream node by name instead of $input.item.json.\nconst apiResponse = $input.item.json;\nconst originalTicket = $('Merge: Pre-Real Agent 2').item.json;\nconst parsed = parseAnthropicJson(apiResponse);\nreturn {\n  json: {\n    ...originalTicket,\n    agent2_broader_crm_lookup_used: parsed.broader_crm_lookup_used,\n    agent2_output: { applicable_regulation: parsed.applicable_regulation, citation: parsed.citation, precedent_notes: parsed.precedent_notes },\n  },\n};"
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000007",
      "name": "Parse: Real Agent 2 Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        400
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "// Tool 2, tier 1 (real, always runs -- spec v5): a deterministic yes/no read\n// off the CRM record, not a judgment call, so it is exempted from Agent 2's\n// usual discretion and checked on every ticket. Carried as its own structured\n// field so Agent 4 reads it as data, not buried in free-text customer_context.\nconst ticket = $input.item.json;\nconst special_population_flag = Boolean(ticket.crm.special_population_flag);\nreturn {\n  json: {\n    ...ticket,\n    agent2_special_population_flag: special_population_flag,\n    agent2_output: { ...ticket.agent2_output, special_population_flag },\n  },\n};"
      },
      "id": "c3a8e4b2-0001-4000-8000-000000000008",
      "name": "Tool: Real Special Population Check",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2100,
        400
      ],
      "notesInFlow": true,
      "notes": "Always runs, every ticket -- same deterministic CRM read as the Test path's tool."
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "language": "javaScript",
        "jsCode": "// Tool 2, tier 2 (real, always runs -- spec Section 6: \"(b) always, based on\n// classification\"). Keyword + light synonym search across the cached\n// regulation corpus' topics (reference_data/regulations/*.json _meta) to\n// surface candidate applicable regulations. This is deliberately a simple,\n// deterministic lookup, not semantic search -- see README for the honest\n// limitation this implies.\nconst REGULATION_META_INDEX = {\n  \"fdcpa_1692g\": {\n    \"regulation\": \"FDCPA\",\n    \"citation\": \"15 U.S.C. \u00a71692g\",\n    \"topic\": \"Debt validation notice\",\n    \"source_url\": \"https://www.law.cornell.edu/uscode/text/15/1692g\",\n    \"retrieved_date\": \"2026-08-12\",\n    \"pilot_relevance\": \"Ticket A (Aargon Agency) -- 30-day validation dispute.\",\n    \"sourcing_note\": \"Verbatim public-domain statutory/regulatory text as published by Cornell Legal Information Institute (law.cornell.edu). Cached once at build time per spec Section 3b -- stable statutory text, not re-fetched per ticket. Not legal advice; this is a technical demonstration (spec Section 14, disclosure 4).\"\n  },\n  \"fdcpa_1692e\": {\n    \"regulation\": \"FDCPA\",\n    \"citation\": \"15 U.S.C. \u00a71692e\",\n    \"topic\": \"False or misleading representations\",\n    \"source_url\": \"https://www.law.cornell.edu/uscode/text/15/1692e\",\n    \"retrieved_date\": \"2026-08-12\",\n    \"pilot_relevance\": \"Ticket B (Equifax) -- alongside FCRA \u00a7605B for a debt-not-owed / identity-theft-profile claim.\",\n    \"sourcing_note\": \"Verbatim public-domain statutory/regulatory text as published by Cornell Legal Information Institute (law.cornell.edu). Cached once at build time per spec Section 3b -- stable statutory text, not re-fetched per ticket. Not legal advice; this is a technical demonstration (spec Section 14, disclosure 4).\"\n  },\n  \"fcra_1681c-2\": {\n    \"regulation\": \"FCRA\",\n    \"citation\": \"15 U.S.C. \u00a71681c-2\",\n    \"topic\": \"Identity-theft block procedure\",\n    \"source_url\": \"https://www.law.cornell.edu/uscode/text/15/1681c-2\",\n    \"retrieved_date\": \"2026-08-12\",\n    \"pilot_relevance\": \"Ticket B (Equifax) and Ticket C (Chase) -- identity-theft / unauthorized-account block requests.\",\n    \"sourcing_note\": \"Verbatim public-domain statutory/regulatory text as published by Cornell Legal Information Institute (law.cornell.edu). Cached once at build time per spec Section 3b -- stable statutory text, not re-fetched per ticket. Not legal advice; this is a technical demonstration (spec Section 14, disclosure 4).\"\n  },\n  \"reg_z_1026_13\": {\n    \"regulation\": \"Regulation Z (FCBA)\",\n    \"citation\": \"12 CFR \u00a71026.13\",\n    \"topic\": \"Billing-error resolution procedure\",\n    \"source_url\": \"https://www.law.cornell.edu/cfr/text/12/1026.13\",\n    \"retrieved_date\": \"2026-08-12\",\n    \"pilot_relevance\": 

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

Complaint Triage Orchestrator (S2.3). Uses httpRequest, googleSheets. Scheduled trigger; 65 nodes.

Source: https://github.com/LeoTheGreatChan/complaint-triage-orchestrator/blob/master/n8n/workflows/complaint_triage_orchestrator.json — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This workflow automates video distribution to 9 social platforms simultaneously using Blotato's API. It includes both a scheduled publisher (checks Google Sheets for videos marked "Ready") and a subwo

Google Sheets, HTTP Request, Form Trigger +2
Data & Sheets

YogiAI. Uses googleSheets, googleSheetsTool, httpRequest, stopAndError. Scheduled trigger; 61 nodes.

Google Sheets, Google Sheets Tool, HTTP Request +1
Data & Sheets

This workflow monitors Google Calendar for events indicating that a customer will visit the company today or the next day, retrieves the required details, and sends reminder notifications to the relev

Google Calendar, Google Sheets, HTTP Request +1
Data & Sheets

ofn hook v0.24.0 beta. Uses start, httpRequest, functionItem, itemLists. Scheduled trigger; 42 nodes.

Start, HTTP Request, Function Item +3
Data & Sheets

Security teams, DevOps engineers, vulnerability analysts, and automation builders who want to eliminate repetitive Nessus scan parsing, AI-based risk triage, and manual reporting. Designed for orgs fo

Email Send, HTTP Request, Google Sheets +1