Skip to main content

Denial auto-correction and rebill operator playbook

Outcome

Denials are auto-corrected via the registered handler chain and the resulting rebilled claim flows through the rest of the pipeline. Stuck or SKIPPED denials have a clear triage path.

Prerequisites

Handler coverage

CARC / PR codeHandlerTerminal actionStrategy
CO-4Co4ModifierHandlerRebill w/ missing modifier injectedMODIFIER_CORRECTION
CO-16Co16MissingInfoHandlerRebill w/ auth# (N479/M51/MA66) or SKIP for manual review (N350/M79/N381/M80)MISSING_INFO_REBILL
CO-18Co18DuplicateHandlerWrite off + claim_relationship(DUPLICATE)DUPLICATE_LINK
CO-29Co29TimelyFilingHandlerFile level-1 appeal with submission proofTIMELY_FILING_APPEAL
CO-97Co97AdjudicationHandlerDuplicate-match → write off, otherwise appealDUPLICATE_LINK / ADJUDICATION_APPEAL
PR-1 / PR-2 / PR-3PatientLiabilityHandlerResolve + emit patient.liability_transferredPATIENT_BILLING

Handlers that don't match simply skip. Handlers that match but can't complete (no submission evidence for CO-29, no remit anchor for PR, etc.) return SKIPPED — the attempt row is still written, which keeps the success-rate view honest.

Event ordering

Tenant/org/correlation IDs thread from the inbound envelope. PatientLiabilityHandler receives the event context via patientLiabilityHandler.withExtra({ eventContext }) so its liability-transfer events inherit the same tenantId / correlationId.

Idempotency guard

billing.auto_correction_attempt carries a partial unique index ux_auto_correction_denial_handler_live on (denial_id, handler_name) WHERE status <> 'FAILED'.

ImplicationWhy
FAILED attempts do not consume the slotRetrying after a transient error is safe
findByDenialAndHandler SKIPs on replayWe never rebill the same denial twice
A second SUCCESS row is rejectedThe data is already corrected; no manual override

Dashboard: success-rate view

billing.v_auto_correction_success_rate aggregates attempts per CARC code:

SELECT carc_code, attempts, successes, failures, skipped,
success_rate, recovered_amount_cents
FROM billing.v_auto_correction_success_rate
ORDER BY success_rate DESC, attempts DESC;

The dashboard surfaces this via GET /denials/auto-correction/stats?since=YYYY-MM-DD&until=YYYY-MM-DD. The response wraps each row in the standard money envelope and adds a rollup meta block (totalAttempts, overallSuccessRate, totalRecoveredAmount). Empty windows return an empty array.

Common triage

"Why didn't CO-4 auto-correct?"

Check that:

  1. The claim has a resolved_context_snapshot with rendering_credential
    • facility_address_state + payer_id.
  2. rcm_reference.modifier_injection_rule has a matching row (trigger_type, trigger_value, scope).
  3. The missing modifier is not already on the line.

The handler SKIPs (not FAILs) when there's nothing to inject.

"Why didn't CO-29 file an appeal?"

The handler needs either a billing.claim_submission row or a pre-denial billing.claim_status_history entry to produce a proof bundle. Without one, it SKIPs so the denial stays OPEN for manual review.

"Why didn't PR-2 emit a liability event?"

PatientLiabilityHandler pulls PR adjustments from billing.remittance_adjustment anchored to the denial's remit_claim_id (claim-level or any of its remit_line rows). If the denial has no remit anchor or the PR sum is zero, the handler SKIPs. Verify the 835 posted cleanly before debugging the handler.

"Replay-safe?"

Yes. Re-emitting a claim.denied event re-runs the registry but every matched handler sees a live auto_correction_attempt row and short-circuits to SKIP. To actually retry a stuck denial (after fixing the underlying data issue), delete the live attempt row or mark it FAILED. Or use the manual trigger UI.

Validation

CheckExpected
auto_correction_attempt rowsOne per (denial, handler) pair, status reflects outcome
claim.rebilled + claim.corrected eventsEmitted on SUCCESS with rebilledClaimId
Dashboard success rateVisible per CARC

Cross-references

Next

3.8 — Receivables (835) workspace