Routing and triage
Requests must reach the right team the first time. A misroute costs handling time and delays the customer, and a request that fits two teams, or none, should go to a person who can place it rather than to whichever rule fired first. This example routes a loan servicer's borrower requests.
How this runs
Every result on this page is the recorded run of the rulebook below through the Helixor rule engine, case by case. In production a rulebook like this runs in a digital worker Available that you build and deploy in the console or with the agent command-line tools; console access is granted on request (email hello@helixor.ai). The rule language itself is described in Rules.
The policy#
- Routes.
A borrower who reports hardship goes to the hardship team. Payment help 30 or more days past due, with no hardship reported, goes to collections. A dispute goes to disputes. Payoff quotes, address changes and payment help under 30 days go to general servicing.
- Triage.
A request that matches two routes, such as a dispute from a borrower in hardship, or that no route places, goes to the triage desk.
- What follows.
A vulnerable customer's request is handled first, whichever team gets it. A collections case is never answered with an automated collection message.
What the rules decided#
Each row is one case run through the rulebook. The outcome is one of four; the deciding rule is the one whose recommendation won; derived facts show every criterion the rules established; and the last column lists the obligations and prohibitions that follow.
| Case | Outcome | Deciding rule | Derived facts | What follows |
|---|---|---|---|---|
| S1 A borrower asks for a payoff quote. | APPROVE | RDEC01: A request that matches exactly one route is sent there. | ROUTE_SERVICING ROUTED | none |
| S2 A borrower asks for payment help and reports losing their job. | APPROVE | RDEC01: A request that matches exactly one route is sent there. | ROUTE_HARDSHIP ROUTED | none |
| S3 A borrower 45 days past due asks for payment help, with no hardship reported. | APPROVE | RDEC01: A request that matches exactly one route is sent there. | ROUTE_COLLECTIONS ROUTED | PROHIBITION_AUTOMATED_COLLECTION_MESSAGE |
| S4 A borrower 10 days past due asks for payment help. | APPROVE | RDEC01: A request that matches exactly one route is sent there. | ROUTE_SERVICING ROUTED | none |
| S5 A borrower disputes a late fee. | APPROVE | RDEC01: A request that matches exactly one route is sent there. | ROUTE_DISPUTES ROUTED | none |
| S6 A borrower in hardship disputes a late fee: two routes match. | HOLD | RDEC02: A request that matches two routes goes to the triage desk. | ROUTE_HARDSHIP ROUTE_DISPUTES ROUTED CONFLICTING_ROUTES TRIAGE_NEEDED | OBLIGATION_SEND_TO_TRIAGE_DESK |
| S7 A request the routes do not cover. | HOLD | RDEC03: A request that no route places goes to the triage desk. | TRIAGE_NEEDED | OBLIGATION_SEND_TO_TRIAGE_DESK |
| S8 A vulnerable customer reports hardship. | APPROVE | RDEC01: A request that matches exactly one route is sent there. | ROUTE_HARDSHIP ROUTED | OBLIGATION_PRIORITY_HANDLING |
| S9 Days past due were not looked up. | NEED_DATA | No rule ran: missing days_past_due | none | none |
| Outcome | Means here |
|---|---|
APPROVE | Exactly one route matched; the derived ROUTE_* fact names the team. |
HOLD | Two routes or none matched; the request goes to the triage desk with the routes it matched. |
NEED_DATA | The request type, reported hardship or days past due is missing. |
The rule engine's outcomes are approve, hold, decline and need data, so the route itself is a derived fact: APPROVE means the route stands. Precedence is written into the rules where the policy states it (hardship before collections and servicing), and left to a person where it does not (hardship and a dispute together).
The rulebook#
Every rule names its owner, its authority, how hard it is, and what it means in plain English, and binds to a typed vocabulary so a misspelled fact fails at load instead of silently never matching.
# Example servicing request routing rulebook in the Helixor rule language. # Fictional domain for helixor.dev: a loan servicer routes each borrower request to one team, # and sends what the rules cannot place to a triage desk. # Classes and predicates: ontology.json. Each case fact <name> reaches the rules as HAS_<NAME>, # carrying its value, and KNOWN_<NAME>, true when it has one. # Recommendations are resolved by @authority, then @priority, then decline > hold > approve. # Here APPROVE means "the route stands" and HOLD means "send to the triage desk"; the route itself is a derived fact. # ---------------------------------------------------------------- required data @domain loan_servicing @ontology example_servicing_requests @ontology_ver 1 @authority policy @owner servicing-operations @severity hard @category data @priority 100 @required_fact_mode explicit @required_facts [HAS_REQUEST_TYPE, HAS_REPORTED_HARDSHIP, HAS_DAYS_PAST_DUE] @english The request can be routed when its type, any reported hardship and the days past due are known. @uses_predicates [hasRequestType, hasReportedHardship, hasDaysPastDue] @uses_classes [ServicingRequest, Borrower, LoanAccount] RDAT01: IF KNOWN_REQUEST_TYPE AND KNOWN_REPORTED_HARDSHIP AND KNOWN_DAYS_PAST_DUE THEN FILE_COMPLETE # ---------------------------------------------------------------- routes @domain loan_servicing @ontology example_servicing_requests @authority policy @owner servicing-operations @severity hard @category route @priority 100 @required_fact_mode explicit @required_facts [HAS_REPORTED_HARDSHIP] @english A borrower who reports financial hardship goes to the hardship team. @uses_predicates [hasReportedHardship] @uses_classes [Borrower] RRT01: IF FILE_COMPLETE AND HAS_REPORTED_HARDSHIP == 1 THEN ROUTE_HARDSHIP @domain loan_servicing @ontology example_servicing_requests @authority policy @owner servicing-operations @severity hard @category route @priority 100 @required_fact_mode explicit @required_facts [HAS_REQUEST_TYPE, HAS_DAYS_PAST_DUE] @english A request for payment help 30 or more days past due, with no reported hardship, goes to collections. @uses_predicates [hasRequestType, hasDaysPastDue] @uses_classes [ServicingRequest, LoanAccount] RRT02: IF FILE_COMPLETE AND HAS_REQUEST_TYPE == PAYMENT_HELP AND HAS_DAYS_PAST_DUE >= 30 AND NOT ROUTE_HARDSHIP THEN ROUTE_COLLECTIONS @domain loan_servicing @ontology example_servicing_requests @authority policy @owner servicing-operations @severity hard @category route @priority 100 @required_fact_mode explicit @required_facts [HAS_REQUEST_TYPE] @english A dispute goes to the disputes team. @uses_predicates [hasRequestType] @uses_classes [ServicingRequest] RRT03: IF FILE_COMPLETE AND HAS_REQUEST_TYPE == DISPUTE THEN ROUTE_DISPUTES @domain loan_servicing @ontology example_servicing_requests @authority policy @owner servicing-operations @severity hard @category route @priority 100 @required_fact_mode explicit @required_facts [HAS_REQUEST_TYPE] @english A payoff request goes to general servicing. @uses_predicates [hasRequestType] @uses_classes [ServicingRequest] RRT04: IF FILE_COMPLETE AND HAS_REQUEST_TYPE == PAYOFF AND NOT ROUTE_HARDSHIP THEN ROUTE_SERVICING @domain loan_servicing @ontology example_servicing_requests @authority policy @owner servicing-operations @severity hard @category route @priority 100 @required_fact_mode explicit @required_facts [HAS_REQUEST_TYPE] @english An address change goes to general servicing. @uses_predicates [hasRequestType] @uses_classes [ServicingRequest] RRT05: IF FILE_COMPLETE AND HAS_REQUEST_TYPE == ADDRESS_CHANGE AND NOT ROUTE_HARDSHIP THEN ROUTE_SERVICING @domain loan_servicing @ontology example_servicing_requests @authority policy @owner servicing-operations @severity hard @category route @priority 100 @required_fact_mode explicit @required_facts [HAS_REQUEST_TYPE, HAS_DAYS_PAST_DUE] @english A request for payment help under 30 days past due, with no reported hardship, goes to general servicing. @uses_predicates [hasRequestType, hasDaysPastDue] @uses_classes [ServicingRequest, LoanAccount] RRT06: IF FILE_COMPLETE AND HAS_REQUEST_TYPE == PAYMENT_HELP AND HAS_DAYS_PAST_DUE < 30 AND NOT ROUTE_HARDSHIP THEN ROUTE_SERVICING # ---------------------------------------------------------------- one route, two, or none @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category route @priority 100 @english A hardship route places the request. RRD01: IF ROUTE_HARDSHIP THEN ROUTED @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category route @priority 100 @english A collections route places the request. RRD02: IF ROUTE_COLLECTIONS THEN ROUTED @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category route @priority 100 @english A disputes route places the request. RRD03: IF ROUTE_DISPUTES THEN ROUTED @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category route @priority 100 @english A servicing route places the request. RRD04: IF ROUTE_SERVICING THEN ROUTED @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category triage @priority 100 @english A dispute from a borrower in hardship matches two teams. RRD05: IF ROUTE_HARDSHIP AND ROUTE_DISPUTES THEN CONFLICTING_ROUTES # ---------------------------------------------------------------- recommendations @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category decision @priority 100 @english A request that matches exactly one route is sent there. RDEC01: IF ROUTED AND NOT CONFLICTING_ROUTES THEN RECOMMEND_APPROVE @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category decision @priority 90 @english A request that matches two routes goes to the triage desk. RDEC02: IF CONFLICTING_ROUTES THEN RECOMMEND_HOLD AND TRIAGE_NEEDED @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category decision @priority 90 @english A request that no route places goes to the triage desk. RDEC03: IF FILE_COMPLETE AND NOT ROUTED THEN RECOMMEND_HOLD AND TRIAGE_NEEDED # ---------------------------------------------------------------- what follows from the decision @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category obligation @priority 100 @english A request the rules cannot place is sent to the triage desk with the routes it matched. ROBL01: IF TRIAGE_NEEDED THEN OBLIGATION(send_to_triage_desk) @domain loan_servicing @ontology example_servicing_requests @authority policy @owner servicing-operations @severity hard @category obligation @priority 100 @required_fact_mode explicit @required_facts [HAS_VULNERABLE_CUSTOMER] @english A request from a vulnerable customer is handled first, whichever team receives it. @uses_predicates [hasVulnerableCustomer] @uses_classes [Borrower] ROBL02: IF HAS_VULNERABLE_CUSTOMER == 1 THEN OBLIGATION(priority_handling) @domain loan_servicing @authority policy @owner servicing-operations @severity hard @category obligation @priority 100 @english A collections case is never answered with an automated collection message. ROBL03: IF ROUTE_COLLECTIONS THEN PROHIBITION(automated_collection_message)
The full recorded run
ontology example_servicing_requests v1: 3 classes, 4 properties rulebook servicing-routing@v1: 18 rules, bound to ontology example_servicing_requests: yes == S1-payoff: A borrower asks for a payoff quote. facts: request_type=PAYOFF, reported_hardship=0, days_past_due=0 outcome: APPROVE report: fired; fired 4 of 18 rules fired: RDAT01 RRT04 RRD04 RDEC01 derived facts: FILE_COMPLETE ROUTE_SERVICING ROUTED recommendations: RECOMMEND_APPROVE obligations: none deciding rule RDEC01 (policy, hard, priority 100): A request that matches exactly one route is sent there. resolution: only_candidate: RDEC01 recommends APPROVE == S2-hardship: A borrower asks for payment help and reports losing their job. facts: request_type=PAYMENT_HELP, reported_hardship=1, days_past_due=45 outcome: APPROVE report: fired; fired 4 of 18 rules fired: RDAT01 RRT01 RRD01 RDEC01 derived facts: FILE_COMPLETE ROUTE_HARDSHIP ROUTED recommendations: RECOMMEND_APPROVE obligations: none deciding rule RDEC01 (policy, hard, priority 100): A request that matches exactly one route is sent there. resolution: only_candidate: RDEC01 recommends APPROVE == S3-collections: A borrower 45 days past due asks for payment help, with no hardship reported. facts: request_type=PAYMENT_HELP, reported_hardship=0, days_past_due=45 outcome: APPROVE report: fired; fired 5 of 18 rules fired: RDAT01 RRT02 RRD02 RDEC01 ROBL03 derived facts: FILE_COMPLETE ROUTE_COLLECTIONS ROUTED recommendations: RECOMMEND_APPROVE obligations: PROHIBITION_AUTOMATED_COLLECTION_MESSAGE deciding rule RDEC01 (policy, hard, priority 100): A request that matches exactly one route is sent there. resolution: only_candidate: RDEC01 recommends APPROVE == S4-early-help: A borrower 10 days past due asks for payment help. facts: request_type=PAYMENT_HELP, reported_hardship=0, days_past_due=10 outcome: APPROVE report: fired; fired 4 of 18 rules fired: RDAT01 RRT06 RRD04 RDEC01 derived facts: FILE_COMPLETE ROUTE_SERVICING ROUTED recommendations: RECOMMEND_APPROVE obligations: none deciding rule RDEC01 (policy, hard, priority 100): A request that matches exactly one route is sent there. resolution: only_candidate: RDEC01 recommends APPROVE == S5-dispute: A borrower disputes a late fee. facts: request_type=DISPUTE, reported_hardship=0, days_past_due=0 outcome: APPROVE report: fired; fired 4 of 18 rules fired: RDAT01 RRT03 RRD03 RDEC01 derived facts: FILE_COMPLETE ROUTE_DISPUTES ROUTED recommendations: RECOMMEND_APPROVE obligations: none deciding rule RDEC01 (policy, hard, priority 100): A request that matches exactly one route is sent there. resolution: only_candidate: RDEC01 recommends APPROVE == S6-dispute-in-hardship: A borrower in hardship disputes a late fee: two routes match. facts: request_type=DISPUTE, reported_hardship=1, days_past_due=0 outcome: HOLD report: fired; fired 8 of 18 rules fired: RDAT01 RRT01 RRT03 RRD01 RRD03 RRD05 RDEC02 ROBL01 derived facts: FILE_COMPLETE ROUTE_HARDSHIP ROUTE_DISPUTES ROUTED CONFLICTING_ROUTES TRIAGE_NEEDED recommendations: RECOMMEND_HOLD obligations: OBLIGATION_SEND_TO_TRIAGE_DESK deciding rule RDEC02 (policy, hard, priority 90): A request that matches two routes goes to the triage desk. resolution: only_candidate: RDEC02 recommends HOLD == S7-unplaced: A request the routes do not cover. facts: request_type=OTHER, reported_hardship=0, days_past_due=0 outcome: HOLD report: fired; fired 3 of 18 rules fired: RDAT01 RDEC03 ROBL01 derived facts: FILE_COMPLETE TRIAGE_NEEDED recommendations: RECOMMEND_HOLD obligations: OBLIGATION_SEND_TO_TRIAGE_DESK deciding rule RDEC03 (policy, hard, priority 90): A request that no route places goes to the triage desk. resolution: only_candidate: RDEC03 recommends HOLD == S8-vulnerable-hardship: A vulnerable customer reports hardship. facts: request_type=PAYMENT_HELP, reported_hardship=1, days_past_due=20, vulnerable_customer=1 outcome: APPROVE report: fired; fired 5 of 18 rules fired: RDAT01 RRT01 RRD01 ROBL02 RDEC01 derived facts: FILE_COMPLETE ROUTE_HARDSHIP ROUTED recommendations: RECOMMEND_APPROVE obligations: OBLIGATION_PRIORITY_HANDLING deciding rule RDEC01 (policy, hard, priority 100): A request that matches exactly one route is sent there. resolution: only_candidate: RDEC01 recommends APPROVE == S9-missing-arrears: Days past due were not looked up. facts: request_type=PAYMENT_HELP, reported_hardship=0 outcome: NEED_DATA missing facts: days_past_due rulebook not run: the decision type's required facts are missing
Adapt it to your policy#
- Write one rule per route, and state precedence explicitly with
NOTwhere your policy has it. - Make "two routes" and "no route" decisions of their own, so nothing is placed by accident.
- Requests that arrive as free text need a reading before these rules: see Route a request and reject what you do not handle.