Case Routing Rule Set

On this page

Status

Step Description Status

1

Save plan document and link in nav.adoc

Done (pre-ADR-030)

2

Design rule set inputs, outputs, and decision logic

Done (pre-ADR-030)

3

Create Georgia case routing JDM file

Done (pre-ADR-030)

4

Create Texas case routing JDM file

Done (pre-ADR-030)

5

Add event-triggered evaluation

Done (pre-ADR-030)

6

Integration tests

Done (pre-ADR-030)

7

Verification and documentation updates

Done (pre-ADR-030)

Branch: feature/case-routing-ruleset

Context

When a referral is screened in and becomes an investigation, the system must route it to an appropriate caseworker and assignment area. Today this is a manual process — supervisors assign cases by hand. Automated routing reduces assignment delays, balances caseloads, and ensures geographic coverage.

CRAIG already has 5 JDM rule set domains per jurisdiction (intake-screening, safety-assessment, ive-eligibility, placement-matching, timeliness). Case routing is the 6th domain, completing the automation of the referral-to-assignment pipeline.

The existing intake-screening rule set determines whether a report is screened in and assigns a priority. The case routing rule set takes the output of intake screening (priority, allegation type) plus geographic and caseload data to determine who handles it.

Scope

In scope:

  • Georgia case routing JDM file (rulesets/georgia/georgia-case-routing.json)

  • Texas case routing JDM file (rulesets/texas/texas-case-routing.json)

  • Event-triggered evaluation via case.referral_created event

  • Integration tests for routing evaluation

  • Rule set documentation in the file’s description field

Out of scope:

  • Caseload balancing API (requires real worker registry — use placeholder logic)

  • Supervisor override UI (web UI feature, separate plan)

  • Real-time reassignment when worker capacity changes

Design

Rule Set Architecture

The case routing rule set uses two chained decision tables (multi-node JDM graph):

  1. Assignment Area Table — maps admin_unit + allegation_type to an assignment_area (geographic/functional grouping)

  2. Worker Selection Table — maps assignment_area + priority + caseload_tier to a routing_decision and assignment_method

This two-stage approach separates geographic routing from caseload-based selection, matching how real child welfare agencies organize (regional offices → unit assignments).

Inputs

Field Type Description

admin_unit

string

County/administrative unit where the child is located (from referral)

allegation_type

string

Type of allegation: physical_abuse, sexual_abuse, neglect, emotional_abuse, abandonment

priority

string

Response priority from intake screening: Georgia uses IMMEDIATE/PRIORITY_24HR/STANDARD; Texas uses P1/P2

caseload_tier

string

Current caseload level of available workers: low (<15 cases), medium (15-25), high (>25)

icwa_flag

boolean

Whether Indian Child Welfare Act applies (requires specialized worker)

special_unit_needed

string

Special unit requirement: none, sexual_abuse_unit, infant_death_unit, institutional_abuse_unit

Outputs

Field Type Description

assignment_area

string

Geographic/functional area for routing (e.g., metro_north, rural_south, special_investigations)

routing_decision

string

How to assign: auto_assign, supervisor_queue, specialty_unit, overflow_pool

assignment_method

string

Selection method within the pool: round_robin, lowest_caseload, geographic_proximity, manual

max_response_hours

number

Maximum hours until first contact, based on priority

routing_notes

string

Plain-language explanation of why this routing was chosen

Node Graph

[Input] → [Assignment Area Table] → [Worker Selection Table] → [Output]

Three nodes connected by two edges, following the same pattern as existing rulesets like georgia-intake-screening.json.

Georgia-Specific Rules (GA DFCS Policy Chapter 4)

Georgia DFCS organizes investigations by region (14 regions covering 159 counties). Assignment areas map to DFCS regional offices:

  • Metro Atlanta counties (Fulton, DeKalb, Gwinnett, Cobb, Clayton) → metro_atlanta

  • Coastal counties (Chatham, Glynn, Camden, etc.) → coastal

  • All other counties → rural with sub-regions based on DFCS district

Special routing: * Sexual abuse allegations → special_investigations unit regardless of geography * ICWA-flagged cases → icwa_specialist queue * Immediate priority + high caseload → supervisor_queue (requires manual assignment)

Texas-Specific Rules (DFPS Organizational Structure)

Texas DFPS uses 11 regions. Key differences from Georgia: * Four priority tiers (P1 24hr, P2 72hr) vs Georgia’s three * institutional_abuse_unit handles daycare/residential facility allegations * Larger geographic regions — geographic_proximity assignment method used more often

Steps

Files: docs/modules/ROOT/pages/plans/case-routing-ruleset.adoc, docs/modules/ROOT/nav.adoc

Create this plan file and add nav entry under Planned.

Step 2: Design Rule Tables

Files: None (design work)

Finalize the decision table rules:

Assignment Area Table (Georgia) — 12 rules:

admin_unit pattern allegation_type assignment_area

Fulton/DeKalb/Gwinnett/Cobb/Clayton

any

metro_atlanta

Chatham/Glynn/Camden/Bryan/Liberty

any

coastal

any

sexual_abuse

special_investigations

(all other counties)

any

rural

Sexual abuse override must come before geographic rules (hitPolicy: first). This follows the same ordering principle as the intake-screening ruleset where specific rules precede catch-alls.

Worker Selection Table (Georgia) — 8 rules:

assignment_area priority caseload_tier routing_decision assignment_method

special_investigations

any

any

specialty_unit

round_robin

any

IMMEDIATE

high

supervisor_queue

manual

any

IMMEDIATE

any

auto_assign

lowest_caseload

any

PRIORITY_24HR

high

overflow_pool

round_robin

any

PRIORITY_24HR

any

auto_assign

lowest_caseload

any

STANDARD

high

overflow_pool

round_robin

any

STANDARD

any

auto_assign

round_robin

any

any

any

supervisor_queue

manual

Step 3: Create Georgia JDM File

Files: rulesets/georgia/georgia-case-routing.json

Follow the exact JDM structure from existing rulesets. Reference rulesets/georgia/georgia-intake-screening.json for the node/edge pattern:

{
  "name": "georgia-case-routing",
  "version": "v1.0",
  "description": "Georgia DFCS case routing decision per Policy Chapter 4. Determines assignment area and routing method based on geography, allegation type, priority, and caseload. Uses two-stage decision: geographic/functional area assignment, then worker selection method.",
  "nodes": [
    {
      "id": "input",
      "name": "Input",
      "type": "inputNode"
    },
    {
      "id": "assignment-area",
      "name": "Assignment Area Determination",
      "type": "decisionTableNode",
      "content": {
        "hitPolicy": "first",
        "inputs": [
          { "id": "i_admin", "name": "Administrative Unit", "field": "admin_unit" },
          { "id": "i_allege", "name": "Allegation Type", "field": "allegation_type" },
          { "id": "i_icwa", "name": "ICWA Flag", "field": "icwa_flag" },
          { "id": "i_special", "name": "Special Unit Needed", "field": "special_unit_needed" }
        ],
        "outputs": [
          { "id": "o_area", "name": "Assignment Area", "field": "assignment_area" }
        ],
        "rules": [...]
      }
    },
    {
      "id": "worker-selection",
      "name": "Worker Selection Method",
      "type": "decisionTableNode",
      "content": {
        "hitPolicy": "first",
        "inputs": [
          { "id": "i_area", "name": "Assignment Area", "field": "assignment_area" },
          { "id": "i_priority", "name": "Priority", "field": "priority" },
          { "id": "i_caseload", "name": "Caseload Tier", "field": "caseload_tier" }
        ],
        "outputs": [
          { "id": "o_decision", "name": "Routing Decision", "field": "routing_decision" },
          { "id": "o_method", "name": "Assignment Method", "field": "assignment_method" },
          { "id": "o_hours", "name": "Max Response Hours", "field": "max_response_hours" },
          { "id": "o_notes", "name": "Routing Notes", "field": "routing_notes" }
        ],
        "rules": [...]
      }
    },
    {
      "id": "output",
      "name": "Output",
      "type": "outputNode"
    }
  ],
  "edges": [
    { "id": "e1", "sourceId": "input", "targetId": "assignment-area", "type": "edge" },
    { "id": "e2", "sourceId": "assignment-area", "targetId": "worker-selection", "type": "edge" },
    { "id": "e3", "sourceId": "worker-selection", "targetId": "output", "type": "edge" }
  ]
}

Populate the rules arrays with the decision tables from Step 2. Use the exact JDM value syntax: * String comparisons: "\"sexual_abuse\"" (quoted string inside quoted value) * Boolean: "== true", "== false" * Wildcards: "" (empty string) * Number outputs: 24 (bare number)

Rule count: ~12 rules in assignment-area table + ~8 rules in worker-selection table = ~20 rules total.

Step 4: Create Texas JDM File

Files: rulesets/texas/texas-case-routing.json

Same structure as Georgia with Texas-specific adjustments:

  • 11 DFPS regions instead of Georgia’s county-to-area mapping

  • P1/P2 priorities instead of IMMEDIATE/PRIORITY_24HR/STANDARD

  • institutional_abuse_unit as an additional special unit type

  • max_response_hours: P1=24, P2=72 (vs Georgia’s IMMEDIATE=1, PRIORITY_24HR=24, STANDARD=72)

Rule count: ~10 assignment-area rules + ~10 worker-selection rules = ~20 rules total.

Step 5: Add Event-Triggered Evaluation

Files: services/craig-rules/src/main.rs

The rules service already handles domain events via handle_domain_event in main.rs. Add a new event trigger:

// In handle_domain_event match:
"case.referral_created" => {
    // Existing: evaluates {jurisdiction}-intake-screening
    // Add: also evaluate {jurisdiction}-case-routing with the referral data
    let routing_input = json!({
        "admin_unit": payload["admin_unit"],
        "allegation_type": payload["concern_type"],
        "priority": screening_result["priority"],  // from intake-screening output
        "caseload_tier": "medium",  // placeholder — real caseload data requires worker registry
        "icwa_flag": payload.get("icwa_flag").and_then(|v| v.as_bool()).unwrap_or(false),
        "special_unit_needed": "none",
    });
    evaluate_and_publish(&engine, &publisher, &format!("{jurisdiction}-case-routing"), routing_input).await;
}
The caseload_tier input is hardcoded to "medium" as a placeholder. Real caseload balancing requires a worker registry service (out of scope). The rule set architecture supports it — when the registry exists, just query actual caseloads and pass the tier.

Alternatively, if chaining two evaluations on a single event is complex, add a new event type "intake.screening_completed" that the intake-screening evaluation publishes, and trigger case-routing from that event.

The existing /v1/rules/evaluate endpoint also works for ad-hoc evaluation:

curl -X POST http://localhost:8001/v1/rules/evaluate \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"rule_set": "georgia-case-routing", "input": {"admin_unit": "Fulton", ...}}'

Step 6: Integration Tests

Files: services/craig-rules/tests/api/evaluate.rs

Add tests to the existing evaluation test file, following the pattern of existing rule evaluation tests:

// Test 1: georgia_case_routing_sexual_abuse_routes_to_special_investigations
// Input: admin_unit="Fulton", allegation_type="sexual_abuse", priority="STANDARD",
//        caseload_tier="low", icwa_flag=false, special_unit_needed="none"
// Expected: assignment_area="special_investigations", routing_decision="specialty_unit",
//           assignment_method="round_robin"

// Test 2: georgia_case_routing_immediate_high_caseload_goes_to_supervisor
// Input: admin_unit="Fulton", allegation_type="neglect", priority="IMMEDIATE",
//        caseload_tier="high", icwa_flag=false, special_unit_needed="none"
// Expected: routing_decision="supervisor_queue", assignment_method="manual"

// Test 3: georgia_case_routing_standard_low_caseload_auto_assigns
// Input: admin_unit="Chatham", allegation_type="neglect", priority="STANDARD",
//        caseload_tier="low", icwa_flag=false, special_unit_needed="none"
// Expected: assignment_area="coastal", routing_decision="auto_assign",
//           assignment_method="round_robin"

// Test 4: georgia_case_routing_icwa_flag_routes_to_icwa_specialist
// Input: admin_unit="Fulton", allegation_type="neglect", priority="STANDARD",
//        caseload_tier="low", icwa_flag=true, special_unit_needed="none"
// Expected: assignment_area="icwa_specialist"

// Test 5: texas_case_routing_p1_auto_assigns_lowest_caseload
// Input: admin_unit="Harris", allegation_type="physical_abuse", priority="P1",
//        caseload_tier="medium", icwa_flag=false, special_unit_needed="none"
// Expected: routing_decision="auto_assign", assignment_method="lowest_caseload",
//           max_response_hours=24

// Test 6: texas_case_routing_institutional_abuse_routes_to_special_unit
// Input: admin_unit="Dallas", allegation_type="physical_abuse", priority="P2",
//        caseload_tier="low", icwa_flag=false, special_unit_needed="institutional_abuse_unit"
// Expected: routing_decision="specialty_unit"

Use the existing test pattern from evaluate.rs:

let harness = TestHarness::new().await.unwrap();
let client = harness.admin_rules_client().await.unwrap();
let resp = client
    .post("/v1/rules/evaluate")
    .json(&json!({
        "rule_set": "georgia-case-routing",
        "input": { ... }
    }))
    .send()
    .await
    .unwrap();
assert_eq!(resp.status(), 200);
let body: Value = resp.json().await.unwrap();
assert_eq!(body["result"]["assignment_area"], "special_investigations");

Step 7: Verification and Documentation Updates

  1. cargo nextest run --workspace --lib — all unit tests pass

  2. cargo xtask dev reload — services pick up new rulesets

  3. cargo nextest run -p craig-rules — integration tests pass including new routing tests

  4. Manual evaluation via curl against /v1/rules/evaluate for both jurisdictions

  5. Verify rule ordering: sexual abuse routes to special investigations even for metro counties

Files Touched

File Change

rulesets/georgia/georgia-case-routing.json

New file: Georgia case routing JDM rule set (~20 rules)

rulesets/texas/texas-case-routing.json

New file: Texas case routing JDM rule set (~20 rules)

services/craig-rules/src/main.rs

Add case.referral_created → case-routing evaluation trigger

services/craig-rules/tests/api/evaluate.rs

Add 6 integration tests for routing evaluation

docs/modules/ROOT/pages/plans/case-routing-ruleset.adoc

This plan document

Verification

  1. cargo nextest run --workspace --lib — unit tests pass

  2. cargo xtask dev reload

  3. cargo nextest run -p craig-rules — all evaluation tests pass

  4. cargo xtask e2e — existing E2E tests still pass

  5. Manual verification: curl POST /v1/rules/evaluate with case routing inputs

Documentation Updates

  • .claude/docs/rulesets.md — add case-routing as 6th domain, document inputs/outputs

  • .claude/docs/services.md — add event trigger for case-routing evaluation

  • CHANGELOG.adoc — entry under == Unreleased

  • .claude/CLAUDE.md — update "5 Ruleset Domains" count to 6

Edit this page · latest