Submission routing rules
Outcome
Every outbound transaction (claim, eligibility, auth, status inquiry) routes to the correct trading partner deterministically — and when the routing produces an unexpected pick, you can use the precedence simulator to see exactly which rule fired and why.
Prerequisites
| Scope | What it lets you do |
|---|---|
edi.routing.read | View rules + run the simulator |
edi.routing.write | Add / edit / delete rules |
A trading partner registered with the relevant outbound capability (2.4 — Capabilities).
What a routing rule is
A routing rule says for transactions matching X conditions, send to
partner Y via companion guide Z over transport mode M. Conditions
narrow the rule's scope; every condition column is optional except
claim_type, tx_type, and dispatch_mode.
| Column | Example values | Notes |
|---|---|---|
tx_type | 837P, 837I, 270, 278, 276, 835 | Required. |
claim_type | PROFESSIONAL, INSTITUTIONAL, DENTAL, * | Required; * = any. |
dispatch_mode | BATCH_SFTP, BATCH_REST, BATCH_AS2, BATCH_MFT, REALTIME_REST | Required; must be one the target partner declares. |
state | OH, MI, or null | Optional dimension. |
payer_id | A specific registered payer, or null | Optional dimension. |
payer_type | MEDICAID, COMMERCIAL, MEDICARE_ADVANTAGE, or null | Optional dimension. |
service_line | A specific service line, or null | Optional dimension. |
priority | Integer (lower = higher precedence) | Explicit precedence; default 100. |
effective_from / effective_to | ISO dates, optional | Activation window — rule is ignored outside its window. |
trading_partner_id | FK to partner | Required. |
companion_guide_id | FK to guide | Optional. |
connection_config | JSON | Per-rule transport overrides. |
Precedence
The resolver in apps/edi-gateway/src/service/routing-resolver.ts runs
in this order:
Specificity counts how many of (state, payer_id, payer_type, service_line) the rule pins. Each non-null adds 1; no other fields
participate in scoring. Specificity only breaks ties between rules at
the same priority.
Capability filter (dispatch_mode) is applied before scoring. A
rule whose target partner has not declared the requested
(dispatch_mode, tx_type) pair is dropped with reason
CAPABILITY_MISSING and visible to the simulator.
Effective-date window is applied next. A rule outside its
[effective_from, effective_to] window is dropped with reason
OUT_OF_WINDOW.
Steps to add a routing rule
Open
/routingin the edi-app. The list shows every active rule grouped by transaction type.Click
+ New ruleor click an existing rule to edit. The RoutingRuleEditPage opens.Fill the form:
Field Notes Transaction type Pick from the partner's enabled capabilities only. Claim type PROFESSIONAL/INSTITUTIONAL/DENTAL/*.Dispatch mode BATCH_SFTP/BATCH_REST/BATCH_AS2/BATCH_MFT/REALTIME_REST. Restricted to modes the chosen partner declares.Conditions Pick optional dimensions (state, payer, payer type, service line); leave blank for "any". Trading partner Restricted to partners with the chosen (tx_type, dispatch_mode)capability.Companion guide Restricted to guides bound to the chosen partner + transaction. Priority Integer; lower is higher precedence. Default 100.Effective window Optional from/to dates. Active Toggle. Use the Precedence simulator before saving. The simulator asks for the same conditions you'd see at runtime (payer, program, state, submitter NPI) and shows which rule would fire — including the rule you are currently editing.
Save. The rule is live immediately for new outbound traffic.
The precedence simulator
The Precedence simulator at /routing is a backed-by-the-real-resolver
debugger. Source: POST /edi/routing/resolve on edi-gateway, which
delegates to routing-resolver.explainRoute().
The simulator returns:
| Field | Content |
|---|---|
selectedRuleId | The winner (or null if no rule survived). |
candidates[].decisionReason | One of SELECTED, LOWER_PRIORITY, LOWER_SPECIFICITY, OUT_OF_WINDOW, CAPABILITY_MISSING, SCOPE_MISMATCH, INACTIVE. |
candidates[].specificity | Count of non-null optional dimensions on that rule. |
candidates[].mismatchedDimensions | For SCOPE_MISMATCH, the dimensions that disagreed. |
candidates[].capabilityRejection | For CAPABILITY_MISSING, the requested mode + tx-type vs. the partner's declared modes. |
Common routing patterns
Single clearinghouse, all payers
rule:
tx_type: 837P
claim_type: PROFESSIONAL
dispatch_mode: BATCH_SFTP
trading_partner: change-healthcare
priority: 100
The simplest pattern — every 837P goes to a clearinghouse, which then sub-routes to the right payer.
Direct submission for one payer, clearinghouse for others
rule_a:
tx_type: 837P
claim_type: PROFESSIONAL
dispatch_mode: BATCH_SFTP
payer_id: ohio-medicaid
trading_partner: ohio-mits-direct
companion_guide: ohio-mits-2026q1
priority: 50 # lower = higher precedence
rule_b:
tx_type: 837P
claim_type: PROFESSIONAL
dispatch_mode: BATCH_SFTP
trading_partner: change-healthcare
priority: 100
Rule A wins for Ohio Medicaid via explicit lower priority. Rule B is the catch-all.
Per-state direct submission
rule:
tx_type: 837P
claim_type: PROFESSIONAL
dispatch_mode: BATCH_SFTP
state: OH
trading_partner: ohio-mits-direct
priority: 80
Wins for Ohio claims by priority; pairs with a priority: 100 catch-
all. Specificity (one non-null dimension: state) only matters if
another rule also has priority: 80.
Payer-type routing
rule:
tx_type: 837P
claim_type: PROFESSIONAL
dispatch_mode: BATCH_SFTP
payer_type: MEDICAID
trading_partner: medicaid-direct
priority: 70
Catches every Medicaid 837P regardless of state.
Steps to debug a misrouted transaction
A transaction that went to the wrong partner:
Open the transaction at
/transactions/:id. Note the partner that received it and the timestamp.Open
/routing→ Precedence simulator. Enter the same conditions the transaction had (transaction type, payer, program, state).The simulator shows the current winner. Compare to what the transaction actually used.
If the winner has changed, look at the rule edit history. Most common: someone added a new more-specific rule or activated a tag.
Fix the rule that should have fired (re-tighten conditions or deactivate the conflicting rule). New transactions route correctly immediately; the misrouted one needs replay (see 8.2 — Replay & retry).
Validation
| Check | Expected |
|---|---|
New rule visible at /routing | Yes. |
| Simulator picks the new rule when conditions match | Yes. |
| Outbound transactions route to the new rule's partner immediately | Yes; no service restart needed. |
Audit log carries routing.create row | Yes. |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Save returns "partner does not have this capability" | Partner's (tx_type, dispatch_mode) map excludes this combination | Enable on the partner (2.4 — Capabilities). |
| Two rules tie on priority + specificity and the wrong one wins | Insertion order tie-breaker | Adjust priority to make precedence explicit. |
| Outbound stalls "no routing rule" | No active rule covers the transaction's conditions, or every match was dropped for CAPABILITY_MISSING / OUT_OF_WINDOW | Run the simulator with the same inputs; the rejection reason on each candidate tells you what to fix. |
| Simulator says rule X wins but real traffic uses Y | Rule X was activated or had its window opened after the transaction was already in flight | Replay the transaction. |