Decisions as a Service
Sign Up Now →Sign Up Now

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.

AvailableWorked exampleRecorded run

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#

  1. 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.

  2. 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.

  3. 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.

CaseOutcomeDeciding ruleDerived factsWhat follows
S1 A borrower asks for a payoff quote.APPROVERDEC01: A request that matches exactly one route is sent there.ROUTE_SERVICING ROUTEDnone
S2 A borrower asks for payment help and reports losing their job.APPROVERDEC01: A request that matches exactly one route is sent there.ROUTE_HARDSHIP ROUTEDnone
S3 A borrower 45 days past due asks for payment help, with no hardship reported.APPROVERDEC01: A request that matches exactly one route is sent there.ROUTE_COLLECTIONS ROUTEDPROHIBITION_AUTOMATED_COLLECTION_MESSAGE
S4 A borrower 10 days past due asks for payment help.APPROVERDEC01: A request that matches exactly one route is sent there.ROUTE_SERVICING ROUTEDnone
S5 A borrower disputes a late fee.APPROVERDEC01: A request that matches exactly one route is sent there.ROUTE_DISPUTES ROUTEDnone
S6 A borrower in hardship disputes a late fee: two routes match.HOLDRDEC02: A request that matches two routes goes to the triage desk.ROUTE_HARDSHIP ROUTE_DISPUTES ROUTED CONFLICTING_ROUTES TRIAGE_NEEDEDOBLIGATION_SEND_TO_TRIAGE_DESK
S7 A request the routes do not cover.HOLDRDEC03: A request that no route places goes to the triage desk.TRIAGE_NEEDEDOBLIGATION_SEND_TO_TRIAGE_DESK
S8 A vulnerable customer reports hardship.APPROVERDEC01: A request that matches exactly one route is sent there.ROUTE_HARDSHIP ROUTEDOBLIGATION_PRIORITY_HANDLING
S9 Days past due were not looked up.NEED_DATANo rule ran: missing days_past_duenonenone
OutcomeMeans here
APPROVEExactly one route matched; the derived ROUTE_* fact names the team.
HOLDTwo routes or none matched; the request goes to the triage desk with the routes it matched.
NEED_DATAThe 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 NOT where 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.