| Level of Care |
IOP"
template_name TEXT,
sections JSONB NOT NULL, -- Array of sections (above)
created_at TIMESTAMPTZ DEFAULT NOW()
);
API
router.get('/api/appeal-builder/narrative-template', tenant, async (req, res) => {
const { cluster_id } = req.query; // "Cigna | Frequency Limit |
IOP"
const template = await db.query(
'SELECT sections FROM narrative_templates WHERE cluster_id = $1',
[cluster_id]
);
return res.json(template.sections); // Return sections for UI to render
});
UI Flow
User clicks "Build Appeal" for Cigna IOP denial
↓
Query cluster_id: "Cigna | Level of Care |
IOP"
↓
Fetch narrative template (4 sections)
↓
Render form with sections:
Clinical Justification (required)
Evidence of Functional Impact (required)
Contradicting Payer Policy (optional)
Evidence Summary (optional)
↓
User fills in each section
↓
Save as draft (version 1)
Example: Completed Narrative
{
"clinical_justification": "Patient with GAF score 35 demonstrates significant functional impairment requiring intensive monitoring...",
"evidence_of_functional_impact": "Patient would likely deteriorate without intensive support, risking crisis hospitalization...",
"contradicting_payer_policy": "Cigna's guidelines (Section 2.3.1) require review based on functional status...",
"evidence_summary": "Three independent psychiatric evaluations over 90 days document consistent need..."
}
2. Evidence Kit Assembly
Dynamic Evidence Checklist
Data Source: precedent_objects.evidence_kit + marketplace_clusters.evidence_kit_composite
{
"required": [
{
"description": "Clinical progress notes (past 30 days)",
"frequency": "85% of successful appeals included this",
"completed": false,
"provided_by_user": null
},
{
"description": "Psychiatric evaluation documenting ongoing need",
"frequency": "78% of successful appeals",
"completed": false,
"provided_by_user": null
},
{
"description": "Functional assessment (GAF or equivalent)",
"frequency": "92% of successful appeals",
"completed": false,
"provided_by_user": null
}
],
"recommended": [
{
"description": "Collateral contact documentation",
"frequency": "45% of successful appeals",
"completed": false
},
{
"description": "Prior treatment records or prior auth history",
"frequency": "38% of successful appeals",
"completed": false
}
],
"optional": [
{
"description": "Research articles on treatment efficacy",
"frequency": "12% of appeals",
"completed": false
}
]
}
Frequency Calculation
-- Generate evidence_kit_composite from all precedents in cluster
SELECT
JSON_BUILD_OBJECT(
'required', JSON_AGG(DISTINCT required_items),
'recommended', JSON_AGG(DISTINCT recommended_items),
'optional', JSON_AGG(DISTINCT optional_items)
) AS evidence_kit_composite,
-- Frequency: percentage of successful (Won) precedents that used this evidence
JSON_OBJECT_AGG(
evidence_description,
COUNT() FILTER(WHERE outcome='Won') 100.0 / COUNT(*) FILTER(WHERE outcome='Won')
) AS frequency_stats
FROM precedent_objects
WHERE cluster_id = $1
GROUP BY cluster_id;
UI Implementation
// Evidence checklist with progress tracking
<div className="evidence-kit">
<h3>Required Evidence</h3>
{template.required.map(item => (
<div className="evidence-item">
<input type="checkbox" onChange={(e) => updateCompletion(item.id, e.target.checked)} />
<label>{item.description}</label>
<span className="frequency">{item.frequency}</span>
</div>
))}
<ProgressBar
completed={completedCount}
total={template.required.length}
label={${completedCount}/${template.required.length} required evidence gathered}
/>
</div>
3. Precedent Selection
Finding Relevant Precedents
User browses marketplace or library to find precedents:
// User clicks "Find Similar Cases"
GET /api/precedents?cluster_id=Cigna | Frequency Limit |
IOP
// Returns list of precedents in that cluster
[
{
id: 'prec-uuid-1',
precedent_name: 'Cigna IOP Frequency Limit Victory (GAF 35)',
outcome: 'Won',
evidence_score: 'Strong',
win_reason: 'Evidence Combination'
},
{
id: 'prec-uuid-2',
precedent_name: 'Cigna IOP Appeal Overturned (Functional Impact)',
outcome: 'Won',
evidence_score: 'Strong'
}
]
Evidence References
User selects precedents:
// Appeal state
const appeal = {
selected_precedent_ids: ['prec-uuid-1', 'prec-uuid-2'],
// Narrative references precedents by position
narrative: "...similar to precedents in our library (#1, #2) which were successful...",
// Evidence integration
evidence_elements: {
"clinical_notes": "From selected precedents: GAF assessment methods, clinical documentation examples",
"gaf_score": "Precedent #1 had GAF 35, patient currently 35 — similar functional level"
}
};
Evidence Synthesis
// When user selects precedents, extract evidence patterns
const synthesizeEvidence = (selectedPrecedentIds: string[]): EvidenceSynthesis => {
const precedents = await db.query(
'SELECT evidence_kit, narrative FROM precedent_objects WHERE id = ANY($1)',
[selectedPrecedentIds]
);
return {
required_patterns: mergeEvidenceKits(precedents.map(p => p.evidence_kit)),
narrative_patterns: extractNarrativeStrategies(precedents.map(p => p.narrative)),
successful_evidence_types: extractEvidenceTypes(precedents)
};
};
4. Quality Gates (Validation)
Run before allowing submit:
Gate 1: Narrative Completeness
Required sections all filled:
Clinical Justification: length >= 100 chars
Evidence of Functional Impact: length >= 100 chars
Gate 2: Evidence Gathered
At least 50% of required evidence marked completed:
required.filter(e => e.completed).length / required.length >= 0.5
Gate 3: Precedent Relevance
Selected precedents match appeal:
Same payer (or similar regional carrier)
Same denial_type or related
Successful outcome (Won or Partial) preferred
Gate 4: Minimal Length
Narrative >= 200 words (substantial argument)
Error if < 50 words (too brief)
Warning if 50-200 words (consider expanding)
Appeal Builder Flow (End-to-End)
1. User navigates to Appeals → "Build New"
↓
Select denial (payer, denial_type, LOC)
↓
System determines cluster_id: "Cigna | Frequency Limit |
IOP"
↓