← Back to Platform Architecture
Appeals — Workflow & State Machine
Overview
The appeals table tracks insurance denial appeals from creation through submission and resolution. Appeals leverage precedents as evidence, follow a strict state machine, and are versioned immutably (like precedents).
Core Concept
An appeal is a structured request to overturn a denial using clinical evidence and precedent-based argument.
Lifecycle:
User drafts appeal (state: Draft) — selects denial, finds relevant precedents
User saves drafts (versions auto-snapshots)
System validates appeal (checks evidence, runs quality gates)
User submits (state: Submitted)
Payer reviews and responds (state: Under Review → Approved/Denied)
Each state transition is immutable: previous versions are preserved in appeal_versions.
Schema Definition
CREATE TABLE appeals (
id UUID PRIMARY KEY,
-- Claim/Denial Reference
denial_id TEXT NOT NULL, -- External claim/denial ID (e.g., "CLM-2024-001")
payer_id TEXT NOT NULL, -- Payer identifier (e.g., "cigna-v2024")
denial_code TEXT, -- X.12 denial code (UR-001, MN-007, etc.)
-- Appeal Narrative
narrative TEXT, -- Free-text appeal argument (markdown)
-- Evidence Selection (JSONB for flexibility)
evidence_elements JSONB,
-- Shape: {
-- "clinical_notes": ["date-range-1", "date-range-2"],
-- "gaf_score": "35",
-- "functional_impact": "description",
-- "treatment_plan": "url or summary"
-- }
-- Precedent Evidence (UUID references)
selected_precedent_ids UUID[] DEFAULT '{}',
-- State Machine
status TEXT NOT NULL DEFAULT 'Draft',
-- Valid: Draft, Submitted, Under Review, Approved, Denied, Withdrawn, Archived
-- Validation
validation_status TEXT, -- "clean", "warnings", "errors"
validation_result JSONB, -- { "checks_passed": [...], "warnings": [...], "errors": [...] }
-- Workflow
created_by UUID NOT NULL REFERENCES users(id),
submitted_by UUID REFERENCES users(id),
submitted_at TIMESTAMPTZ,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
notes TEXT,
-- Tenant Isolation
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Versioning
version INTEGER NOT NULL DEFAULT 1,
-- Indexes
UNIQUE(tenant_id, denial_id), -- One appeal per denial per tenant
INDEX idx_appeal_tenant (tenant_id, status),
INDEX idx_appeal_status (status),
INDEX idx_appeal_created_at (created_at DESC),
INDEX idx_appeal_submitted_at (submitted_at DESC),
INDEX idx_appeal_precedent (selected_precedent_ids)
);
State Machine
┌────────┐
│ Draft │ ← User is building appeal, saving drafts
└───┬────┘
│ (user clicks Submit)
↓
┌──────────┐
│Submitted │ ← Appeal sent to payer or internal queue
└───┬──────┘
│ (payer reviews)
↓
┌──────────────┐
│ Under Review │ ← Payer is evaluating
└─┬──────────┬─┘
│ │
│(approved)│(denied)
↓ ↓
┌────────┐ ┌────────┐
│Approved│ │ Denied │
└────────┘ └────────┘
Valid Transitions:
- Draft → Submitted (user action)
- Draft → Withdrawn (user cancels)
- Submitted → Under Review (payer acknowledges)
- Under Review → Approved (payer decision)
- Under Review → Denied (payer decision)
- Any state → Archived (admin cleanup)
Core Fields
Denial Reference
denial_id — External claim/denial identifier (from payer claim system)
payer_id — Payer code (used to lookup payer_plans)
denial_code — X.12 denial code (UR-001, MN-007, etc.)
Appeal Content
narrative — Free-text markdown appeal argument. Example:
## Clinical Justification
Patient meets medical necessity criteria for continued IOP:
Recent GAF score: 35 (indicates moderate impairment)
Clinical deterioration risk if discharged to less intensive care
## Evidence
Psychiatric evaluation (2024-06-10) documents ongoing need
Treatment progress demonstrates clinical benefit
evidence_elements — Structured metadata about evidence:
{
"clinical_notes": ["2024-06-01 to 2024-06-30"],
"gaf_score": "35",
"functional_impact": "Patient would be unable to manage symptoms independently",
"treatment_plan": "Continue IOP with psychiatric medication management"
}
selected_precedent_ids — UUID array of precedents cited as evidence. Example:
selected_precedent_ids = ARRAY[
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'f1e2d3c4-b5a6-7890-fedc-ba9876543210'
]
State & Validation
status — Current state (Draft, Submitted, Under Review, Approved, Denied, Withdrawn, Archived)
validation_status — Result of quality gates (clean, warnings, errors)
validation_result — Detailed validation output:
{
"checks_passed": ["has_narrative", "has_precedents", "precedents_relevant"],
"warnings": ["narrative_length_short"],
"errors": []
}
Query Patterns
List Appeals by Status
SELECT id, denial_id, status, created_at
FROM appeals
WHERE tenant_id = $1 AND status = 'Draft'
ORDER BY created_at DESC
LIMIT 25;
Get Appeal with Evidence
SELECT
a.id, a.denial_id, a.narrative, a.status,
jsonb_agg(p.precedent_name) AS precedent_names
FROM appeals a
LEFT JOIN precedent_objects p ON p.id = ANY(a.selected_precedent_ids)
WHERE a.id = $1 AND a.tenant_id = $2
GROUP BY a.id;
Find Appeals Pending Submission
SELECT id, denial_id, validation_status
FROM appeals
WHERE tenant_id = $1
AND status = 'Draft'
AND validation_status IS NOT NULL
ORDER BY updated_at DESC;
Versioning (appeals_versions table)
Same immutable pattern as precedents. Every save creates a snapshot:
CREATE TABLE appeal_versions (
id UUID PRIMARY KEY,
appeal_id UUID NOT NULL REFERENCES appeals(id) ON DELETE CASCADE,
version_number INTEGER NOT NULL,
snapshot JSONB NOT NULL, -- Full state at this version
saved_by UUID NOT NULL REFERENCES users(id),
saved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
version_reason TEXT, -- "saved_draft", "validation_run", "submitted"
tenant_id UUID NOT NULL REFERENCES tenants(id),
UNIQUE(appeal_id, version_number),
INDEX idx_appeal_version (appeal_id, version_number DESC)
);
When to snapshot:
User saves draft
System runs validation
User submits
Payer status changes
Audit Logging (appeals_audit_log)
CREATE TABLE appeals_audit_log (
id UUID PRIMARY KEY,
appeal_id UUID NOT NULL REFERENCES appeals(id) ON DELETE CASCADE,
event_type TEXT NOT NULL, -- "created", "saved_draft", "validation_run", "submitted", "status_changed"
event_data JSONB, -- Context (validation result, state change reason, etc.)
user_id UUID NOT NULL REFERENCES users(id),
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
INDEX idx_appeal_audit (appeal_id, timestamp DESC)
);
Validation Gates (Quality Checks)
Before submission, appeals pass validation:
| Gate | Rule | Error if |
| Has Narrative | Narrative is non-empty | narrative IS NULL OR narrative = '' |
| Has Evidence | At least one precedent selected | selected_precedent_ids = '{}' |
| Precedents Valid | All precedent IDs exist | Precedent not found in database |
| Narrative Length | Narrative > 50 words | Too short (shallow appeal) |
| Payer Known | Payer ID exists in payer_plans or registry | Unrecognized payer |
Run before allowing Submit:
-- Example: Check if appeal is ready to submit
SELECT
CASE
WHEN narrative IS NULL THEN 'error: missing_narrative'
WHEN array_length(selected_precedent_ids, 1) = 0 THEN 'error: no_precedents'
WHEN (SELECT COUNT(*) FROM precedent_objects WHERE id = ANY(a.selected_precedent_ids))
!= array_length(selected_precedent_ids, 1) THEN 'error: invalid_precedents'
ELSE 'clean'
END AS validation_status
FROM appeals a
WHERE a.id = $1;
Integration with Precedents
Appeals link to precedents as evidence. When a precedent is referenced:
-- Query: Get all appeals citing a precedent
SELECT appeal_id, denial_id, status
FROM appeals
WHERE tenant_id = $1 AND $2::uuid = ANY(selected_precedent_ids);
This enables:
"Show me all appeals using this precedent" (for quality assessment)
"Mark this precedent as reused" (for contribution scoring)
Compliance Notes
PHI Handling:
Narrative may contain de-identified clinical summary (symptoms, not patient name/DOB/MRN)
PHI validation middleware scans narrative for 18 identifiers
Reject submission if SSN, phone, email, MRN detected
42 CFR Part 2 (SUD):
If appeal cites SUD precedent, inherit sharing_restrictions from precedent
Log appeal creation/submission to audit trail
Never allow cross-tenant visibility of SUD appeals
See also
Precedent Objects — Evidence source
Versioning & Immutability — appeal_versions pattern
Audit Log — appeals_audit_log design
PHI Validation — Identifier detection
Code reference
src/models/appeal.ts — Query helpers (CRUD, validation, versioning)
src/routes/appeals.ts — API endpoints (GET, POST, validation, submit)
src/db/migrations/002_appeals_infrastructure.sql — Table definitions
src/services/validation.ts — Quality gate logic