{
  "name": "Claim Denial Triage + Appeal Draft",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [{ "field": "cronExpression", "expression": "0 7 * * 1-5" }]
        }
      },
      "id": "c3000000-0000-4000-8000-000000000001",
      "name": "Weekday mornings at 07:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [-680, 0]
    },
    {
      "parameters": {
        "url": "={{ $env.BILLING_BASE_URL }}/denials?since={{ $now.minus(1, 'days').toISODate() }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "c3000000-0000-4000-8000-000000000002",
      "name": "Fetch new denials",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-460, 0],
      "notesInFlow": true,
      "notes": "From your clearinghouse or 835 remittance feed. Expected fields: claimId, patientRef, payer, cptCodes, icdCodes, chargeAmount, carcCode, rarcCode, denialText, serviceDate."
    },
    {
      "parameters": {
        "jsCode": "// Classifies each denial by CARC code into the category that decides what happens next.\n// Rule-based on purpose: categorisation drives money, so it must be explainable.\n\nconst CATEGORIES = {\n  eligibility:  ['26','27','31','32','200','201'],\n  registration: ['16','140','164','206'],\n  authorization:['15','197','198','199'],\n  coding:       ['4','11','107','181','182','236'],\n  bundling:     ['97','234','B15'],\n  timely:       ['29'],\n  duplicate:    ['18'],\n  medicalNecessity: ['50','55','56','167'],\n  nonCovered:   ['96','204','A1'],\n};\n\n// Appealing these rarely pays for the staff time it costs.\nconst LOW_YIELD = new Set(['duplicate', 'timely', 'nonCovered']);\n\nconst APPEAL_FLOOR = Number($env.APPEAL_FLOOR_USD || 25);\n\nreturn $input.all().map(item => {\n  const d = item.json;\n  const carc = String(d.carcCode || '').trim();\n\n  let category = 'other';\n  for (const [name, codes] of Object.entries(CATEGORIES)) {\n    if (codes.includes(carc)) { category = name; break; }\n  }\n\n  const amount = Number(d.chargeAmount || 0);\n  const preventable = ['eligibility', 'registration', 'coding', 'authorization'].includes(category);\n  const worthAppealing = !LOW_YIELD.has(category) && amount >= APPEAL_FLOOR;\n\n  return {\n    json: {\n      ...d,\n      category,\n      preventable,\n      worthAppealing,\n      priority: amount >= 500 ? 'high' : amount >= 150 ? 'medium' : 'low',\n    },\n  };\n});"
      },
      "id": "c3000000-0000-4000-8000-000000000003",
      "name": "Categorise the denial",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-240, 0]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "cond-worth-appealing",
              "leftValue": "={{ $json.worthAppealing }}",
              "rightValue": true,
              "operator": { "type": "boolean", "operation": "true", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "c3000000-0000-4000-8000-000000000004",
      "name": "Worth appealing?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [-20, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "x-api-key", "value": "={{ $env.ANTHROPIC_API_KEY }}" },
            { "name": "anthropic-version", "value": "2023-06-01" },
            { "name": "content-type", "value": "application/json" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  model: 'claude-sonnet-5',\n  max_tokens: 1200,\n  system: 'You draft appeal letters for a US medical billing team. Write only from the facts supplied — never invent clinical details, dates, or policy citations. If a required fact is missing, write [MISSING: what is needed] inline so a human can fill it. Output the letter only, no preamble. A certified coder reviews every letter before it is sent.',\n  messages: [{\n    role: 'user',\n    content: 'Draft a first-level appeal letter for this denial.\\n\\n' +\n      'Payer: ' + $json.payer + '\\n' +\n      'Claim ID: ' + $json.claimId + '\\n' +\n      'Date of service: ' + $json.serviceDate + '\\n' +\n      'CPT codes: ' + JSON.stringify($json.cptCodes) + '\\n' +\n      'ICD-10 codes: ' + JSON.stringify($json.icdCodes) + '\\n' +\n      'Billed amount: $' + $json.chargeAmount + '\\n' +\n      'Denial code: ' + $json.carcCode + ' / ' + ($json.rarcCode || 'n/a') + '\\n' +\n      'Denial reason given: ' + $json.denialText + '\\n' +\n      'Our category: ' + $json.category\n  }]\n}) }}",
        "options": {}
      },
      "id": "c3000000-0000-4000-8000-000000000005",
      "name": "Draft the appeal letter",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [220, -100],
      "notesInFlow": true,
      "notes": "Send de-identified claim data where your workflow allows it. The draft is always reviewed by a human before submission."
    },
    {
      "parameters": {
        "jsCode": "const claim = $('Categorise the denial').item.json;\nconst response = $input.first().json;\nconst letter = response?.content?.[0]?.text || '[Draft failed — write manually]';\n\nconst needsInfo = /\\[MISSING:/i.test(letter);\n\nreturn [{\n  json: {\n    claimId: claim.claimId,\n    payer: claim.payer,\n    amount: claim.chargeAmount,\n    category: claim.category,\n    priority: claim.priority,\n    needsInfo,\n    letter,\n  },\n}];"
      },
      "id": "c3000000-0000-4000-8000-000000000006",
      "name": "Package for review",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [460, -100]
    },
    {
      "parameters": {
        "sendTo": "={{ $env.BILLING_TEAM_EMAIL }}",
        "subject": "=[{{ $json.priority.toUpperCase() }}] Appeal draft ready — claim {{ $json.claimId }} ({{ $json.payer }}, ${{ $json.amount }})",
        "message": "=Category: {{ $json.category }}\nAmount: ${{ $json.amount }}\n{{ $json.needsInfo ? 'ACTION NEEDED: the draft contains [MISSING] markers — fill these before sending.' : 'Draft is complete pending your review.' }}\n\n--- DRAFT (review before sending) ---\n\n{{ $json.letter }}",
        "options": {}
      },
      "id": "c3000000-0000-4000-8000-000000000007",
      "name": "Send draft to a human",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [700, -100],
      "webhookId": "c3000000-0000-4000-8000-000000000007",
      "notesInFlow": true,
      "notes": "Nothing is submitted automatically. A biller reviews, edits, and sends."
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": { "__rl": true, "value": "={{ $env.DENIAL_LOG_SHEET_ID }}", "mode": "id" },
        "sheetName": { "__rl": true, "value": "Denials", "mode": "name" },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Date": "={{ $now.toISODate() }}",
            "Claim": "={{ $json.claimId }}",
            "Payer": "={{ $json.payer }}",
            "CARC": "={{ $json.carcCode }}",
            "Category": "={{ $json.category }}",
            "Preventable": "={{ $json.preventable ? 'Yes' : 'No' }}",
            "Amount": "={{ $json.chargeAmount }}",
            "Appealing": "={{ $json.worthAppealing ? 'Yes' : 'No' }}"
          }
        },
        "options": {}
      },
      "id": "c3000000-0000-4000-8000-000000000008",
      "name": "Log every denial",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [220, 140],
      "notesInFlow": true,
      "notes": "The real prize. After a month this sheet tells you which five payer rules cause most of your denials — that is where prevention work pays."
    }
  ],
  "connections": {
    "Weekday mornings at 07:00": {
      "main": [[{ "node": "Fetch new denials", "type": "main", "index": 0 }]]
    },
    "Fetch new denials": {
      "main": [[{ "node": "Categorise the denial", "type": "main", "index": 0 }]]
    },
    "Categorise the denial": {
      "main": [
        [
          { "node": "Worth appealing?", "type": "main", "index": 0 },
          { "node": "Log every denial", "type": "main", "index": 0 }
        ]
      ]
    },
    "Worth appealing?": {
      "main": [[{ "node": "Draft the appeal letter", "type": "main", "index": 0 }], []]
    },
    "Draft the appeal letter": {
      "main": [[{ "node": "Package for review", "type": "main", "index": 0 }]]
    },
    "Package for review": {
      "main": [[{ "node": "Send draft to a human", "type": "main", "index": 0 }]]
    }
  },
  "pinData": {},
  "settings": { "executionOrder": "v1" },
  "tags": [{ "name": "healthcare" }, { "name": "revenue cycle" }]
}
